mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-25 02:26:12 +02:00
Add dynamic command secret validation schema
Introduce dynamic_command secret metadata and static argv binding persistence behind the existing experimental rollout path. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
17 files changed
+284
-30
No files matched your search
@@ -0,0 +1,5 @@
|
||||
ALTER TABLE "company_secrets"
|
||||
ADD COLUMN IF NOT EXISTS "dynamic_command" jsonb;
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "company_secret_bindings"
|
||||
ADD COLUMN IF NOT EXISTS "static_argv" jsonb NOT NULL DEFAULT '[]'::jsonb;
|
||||
@@ -792,6 +792,13 @@
|
||||
"when": 1781902600000,
|
||||
"tag": "0112_rename_skill_create_permission_key",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 113,
|
||||
"version": "7",
|
||||
"when": 1782417600000,
|
||||
"tag": "0113_dynamic_secret_command_config",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { boolean, index, pgTable, text, timestamp, uniqueIndex, uuid } from "drizzle-orm/pg-core";
|
||||
import { boolean, index, jsonb, pgTable, text, timestamp, uniqueIndex, uuid } from "drizzle-orm/pg-core";
|
||||
import { companies } from "./companies.js";
|
||||
import { companySecrets } from "./company_secrets.js";
|
||||
|
||||
@@ -14,6 +14,7 @@ export const companySecretBindings = pgTable(
|
||||
versionSelector: text("version_selector").notNull().default("latest"),
|
||||
required: boolean("required").notNull().default(true),
|
||||
label: text("label"),
|
||||
staticArgv: jsonb("static_argv").$type<string[]>().notNull().default([]),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
|
||||
@@ -14,6 +14,11 @@ export const companySecrets = pgTable(
|
||||
status: text("status").notNull().default("active"),
|
||||
managedMode: text("managed_mode").notNull().default("paperclip_managed"),
|
||||
externalRef: text("external_ref"),
|
||||
dynamicCommand: jsonb("dynamic_command").$type<{
|
||||
provider: "host-command";
|
||||
command: string;
|
||||
ttlSeconds: number;
|
||||
}>(),
|
||||
providerConfigId: uuid("provider_config_id").references(() => companySecretProviderConfigs.id, { onDelete: "set null" }),
|
||||
providerMetadata: jsonb("provider_metadata").$type<Record<string, unknown>>(),
|
||||
latestVersion: integer("latest_version").notNull().default(1),
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
BIND_MODES,
|
||||
DEPLOYMENT_EXPOSURES,
|
||||
DEPLOYMENT_MODES,
|
||||
SECRET_PROVIDERS,
|
||||
SECRET_STORAGE_PROVIDERS,
|
||||
STORAGE_PROVIDERS,
|
||||
} from "./constants.js";
|
||||
import { validateConfiguredBindMode } from "./network-bind.js";
|
||||
@@ -92,7 +92,7 @@ export const secretsLocalEncryptedConfigSchema = z.object({
|
||||
});
|
||||
|
||||
export const secretsConfigSchema = z.object({
|
||||
provider: z.enum(SECRET_PROVIDERS).default("local_encrypted"),
|
||||
provider: z.enum(SECRET_STORAGE_PROVIDERS).default("local_encrypted"),
|
||||
strictMode: z.boolean().default(false),
|
||||
localEncrypted: secretsLocalEncryptedConfigSchema.default({
|
||||
keyFilePath: "~/.paperclip/instances/default/secrets/master.key",
|
||||
|
||||
@@ -546,12 +546,18 @@ export const APPROVAL_STATUSES = [
|
||||
] as const;
|
||||
export type ApprovalStatus = (typeof APPROVAL_STATUSES)[number];
|
||||
|
||||
export const SECRET_PROVIDERS = [
|
||||
export const SECRET_STORAGE_PROVIDERS = [
|
||||
"local_encrypted",
|
||||
"aws_secrets_manager",
|
||||
"gcp_secret_manager",
|
||||
"vault",
|
||||
] as const;
|
||||
export type SecretStorageProvider = (typeof SECRET_STORAGE_PROVIDERS)[number];
|
||||
|
||||
export const SECRET_PROVIDERS = [
|
||||
...SECRET_STORAGE_PROVIDERS,
|
||||
"host_command",
|
||||
] as const;
|
||||
export type SecretProvider = (typeof SECRET_PROVIDERS)[number];
|
||||
|
||||
export const SECRET_PROVIDER_CONFIG_STATUSES = [
|
||||
@@ -575,7 +581,7 @@ export type SecretProviderConfigHealthStatus =
|
||||
export const SECRET_STATUSES = ["active", "disabled", "archived", "deleted"] as const;
|
||||
export type SecretStatus = (typeof SECRET_STATUSES)[number];
|
||||
|
||||
export const SECRET_MANAGED_MODES = ["paperclip_managed", "external_reference"] as const;
|
||||
export const SECRET_MANAGED_MODES = ["paperclip_managed", "external_reference", "dynamic_command"] as const;
|
||||
export type SecretManagedMode = (typeof SECRET_MANAGED_MODES)[number];
|
||||
|
||||
export const SECRET_VERSION_STATUSES = [
|
||||
|
||||
@@ -126,6 +126,7 @@ export {
|
||||
PROJECT_COLORS,
|
||||
APPROVAL_TYPES,
|
||||
APPROVAL_STATUSES,
|
||||
SECRET_STORAGE_PROVIDERS,
|
||||
SECRET_PROVIDERS,
|
||||
SECRET_PROVIDER_CONFIG_STATUSES,
|
||||
SECRET_PROVIDER_CONFIG_HEALTH_STATUSES,
|
||||
@@ -255,6 +256,7 @@ export {
|
||||
type PauseReason,
|
||||
type ApprovalType,
|
||||
type ApprovalStatus,
|
||||
type SecretStorageProvider,
|
||||
type SecretProvider,
|
||||
type SecretProviderConfigStatus,
|
||||
type SecretProviderConfigHealthStatus,
|
||||
@@ -730,6 +732,7 @@ export type {
|
||||
EnvPlainBinding,
|
||||
EnvSecretRefBinding,
|
||||
AgentEnvConfig,
|
||||
DynamicSecretCommandConfig,
|
||||
CompanySecret,
|
||||
CompanySecretProviderConfig,
|
||||
SecretProviderConfigPayload,
|
||||
@@ -1188,6 +1191,8 @@ export {
|
||||
envBindingSecretRefSchema,
|
||||
envBindingSchema,
|
||||
envConfigSchema,
|
||||
dynamicSecretCommandSchema,
|
||||
staticArgvSchema,
|
||||
createSecretSchema,
|
||||
createSecretProviderConfigSchema,
|
||||
updateSecretProviderConfigSchema,
|
||||
|
||||
@@ -410,6 +410,7 @@ export type {
|
||||
CompanySecretBindingTarget,
|
||||
CompanySecretUsageBinding,
|
||||
CompanySecretVersion,
|
||||
DynamicSecretCommandConfig,
|
||||
SecretAccessEvent,
|
||||
RemoteSecretImportCandidate,
|
||||
RemoteSecretImportCandidateStatus,
|
||||
|
||||
@@ -38,6 +38,12 @@ export type EnvBinding = string | EnvPlainBinding | EnvSecretRefBinding;
|
||||
|
||||
export type AgentEnvConfig = Record<string, EnvBinding>;
|
||||
|
||||
export interface DynamicSecretCommandConfig {
|
||||
provider: "host-command";
|
||||
command: string;
|
||||
ttlSeconds: number;
|
||||
}
|
||||
|
||||
export interface CompanySecret {
|
||||
id: string;
|
||||
companyId: string;
|
||||
@@ -47,6 +53,7 @@ export interface CompanySecret {
|
||||
status: SecretStatus;
|
||||
managedMode: SecretManagedMode;
|
||||
externalRef: string | null;
|
||||
dynamicCommand: DynamicSecretCommandConfig | null;
|
||||
providerConfigId: string | null;
|
||||
providerMetadata: Record<string, unknown> | null;
|
||||
latestVersion: number;
|
||||
@@ -197,6 +204,7 @@ export interface CompanySecretBinding {
|
||||
versionSelector: SecretVersionSelector;
|
||||
required: boolean;
|
||||
label: string | null;
|
||||
staticArgv: string[];
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
@@ -439,6 +439,8 @@ export {
|
||||
envBindingSecretRefSchema,
|
||||
envBindingSchema,
|
||||
envConfigSchema,
|
||||
dynamicSecretCommandSchema,
|
||||
staticArgvSchema,
|
||||
createSecretSchema,
|
||||
createSecretProviderConfigSchema,
|
||||
updateSecretProviderConfigSchema,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
createSecretBindingSchema,
|
||||
createSecretProviderConfigSchema,
|
||||
createSecretSchema,
|
||||
remoteSecretImportPreviewSchema,
|
||||
@@ -10,6 +11,61 @@ import {
|
||||
} from "./secret.js";
|
||||
|
||||
describe("secret validators", () => {
|
||||
it("allows dynamic command secrets with host-command generator config", () => {
|
||||
const parsed = createSecretSchema.parse({
|
||||
name: "GitHub App token",
|
||||
managedMode: "dynamic_command",
|
||||
dynamicCommand: {
|
||||
provider: "host-command",
|
||||
command: "/usr/local/bin/mint-github-token",
|
||||
ttlSeconds: 3600,
|
||||
},
|
||||
});
|
||||
|
||||
expect(parsed.dynamicCommand).toEqual({
|
||||
provider: "host-command",
|
||||
command: "/usr/local/bin/mint-github-token",
|
||||
ttlSeconds: 3600,
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects value and externalRef on dynamic command secrets", () => {
|
||||
expect(() =>
|
||||
createSecretSchema.parse({
|
||||
name: "Invalid dynamic secret",
|
||||
managedMode: "dynamic_command",
|
||||
value: "static-secret",
|
||||
externalRef: "arn:aws:secretsmanager:us-east-1:123456789012:secret:shared/other",
|
||||
dynamicCommand: {
|
||||
provider: "host-command",
|
||||
command: "/usr/local/bin/mint-token",
|
||||
ttlSeconds: 300,
|
||||
},
|
||||
}),
|
||||
).toThrow(/Dynamic command secrets cannot set value/);
|
||||
});
|
||||
|
||||
it("requires generator config for dynamic command secrets", () => {
|
||||
expect(() =>
|
||||
createSecretSchema.parse({
|
||||
name: "Missing generator",
|
||||
managedMode: "dynamic_command",
|
||||
}),
|
||||
).toThrow(/require generator command config/);
|
||||
});
|
||||
|
||||
it("accepts operator-set static argv params on secret bindings", () => {
|
||||
const parsed = createSecretBindingSchema.parse({
|
||||
secretId: "11111111-1111-4111-8111-111111111111",
|
||||
targetType: "agent",
|
||||
targetId: "agent-1",
|
||||
configPath: "env.GITHUB_TOKEN",
|
||||
staticArgv: ["--installation", "12345", "--repo", "paperclipai/paperclip"],
|
||||
});
|
||||
|
||||
expect(parsed.staticArgv).toEqual(["--installation", "12345", "--repo", "paperclipai/paperclip"]);
|
||||
});
|
||||
|
||||
it("rejects externalRef on managed secrets", () => {
|
||||
expect(() =>
|
||||
createSecretSchema.parse({
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
SECRET_MANAGED_MODES,
|
||||
SECRET_PROVIDER_CONFIG_STATUSES,
|
||||
SECRET_PROVIDERS,
|
||||
SECRET_STORAGE_PROVIDERS,
|
||||
SECRET_STATUSES,
|
||||
} from "../constants.js";
|
||||
|
||||
@@ -27,6 +28,14 @@ export const envBindingSchema = z.union([
|
||||
|
||||
export const envConfigSchema = z.record(z.string(), envBindingSchema);
|
||||
|
||||
export const dynamicSecretCommandSchema = z.object({
|
||||
provider: z.literal("host-command"),
|
||||
command: z.string().trim().min(1).max(2048),
|
||||
ttlSeconds: z.number().int().min(1).max(86_400),
|
||||
}).strict();
|
||||
|
||||
export const staticArgvSchema = z.array(z.string().max(4096)).max(64).default([]);
|
||||
|
||||
export const createSecretSchema = z.object({
|
||||
name: z.string().min(1),
|
||||
key: z.string().min(1).regex(/^[a-zA-Z0-9_.-]+$/).optional(),
|
||||
@@ -34,12 +43,21 @@ export const createSecretSchema = z.object({
|
||||
providerConfigId: z.string().uuid().optional().nullable(),
|
||||
managedMode: z.enum(SECRET_MANAGED_MODES).optional(),
|
||||
value: z.string().min(1).optional().nullable(),
|
||||
dynamicCommand: dynamicSecretCommandSchema.optional().nullable(),
|
||||
description: z.string().optional().nullable(),
|
||||
externalRef: z.string().optional().nullable(),
|
||||
providerMetadata: z.record(z.string(), z.unknown()).optional().nullable(),
|
||||
providerVersionRef: z.string().optional().nullable(),
|
||||
}).superRefine((value, ctx) => {
|
||||
if ((value.managedMode ?? "paperclip_managed") === "external_reference") {
|
||||
const managedMode = value.managedMode ?? "paperclip_managed";
|
||||
if (managedMode === "external_reference") {
|
||||
if (value.provider === "host_command") {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["provider"],
|
||||
message: "Host command provider requires dynamic_command mode",
|
||||
});
|
||||
}
|
||||
if (!value.externalRef?.trim()) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
@@ -49,6 +67,44 @@ export const createSecretSchema = z.object({
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (managedMode === "dynamic_command") {
|
||||
if ((value.provider ?? "host_command") !== "host_command") {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["provider"],
|
||||
message: "Dynamic command secrets require host_command provider",
|
||||
});
|
||||
}
|
||||
if (value.value?.trim()) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["value"],
|
||||
message: "Dynamic command secrets cannot set value",
|
||||
});
|
||||
}
|
||||
if (value.externalRef?.trim()) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["externalRef"],
|
||||
message: "Dynamic command secrets cannot set externalRef",
|
||||
});
|
||||
}
|
||||
if (!value.dynamicCommand) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["dynamicCommand"],
|
||||
message: "Dynamic command secrets require generator command config",
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (value.provider === "host_command") {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["provider"],
|
||||
message: "Host command provider requires dynamic_command mode",
|
||||
});
|
||||
}
|
||||
if (value.externalRef?.trim()) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
@@ -99,6 +155,7 @@ export const createSecretBindingSchema = secretBindingTargetSchema.extend({
|
||||
versionSelector: z.union([z.literal("latest"), z.number().int().positive()]).default("latest"),
|
||||
required: z.boolean().default(true),
|
||||
label: z.string().optional().nullable(),
|
||||
staticArgv: staticArgvSchema,
|
||||
});
|
||||
|
||||
export type CreateSecretBinding = z.infer<typeof createSecretBindingSchema>;
|
||||
@@ -194,7 +251,7 @@ export const secretProviderConfigPayloadSchema = z.discriminatedUnion("provider"
|
||||
]);
|
||||
|
||||
export const createSecretProviderConfigSchema = z.object({
|
||||
provider: z.enum(SECRET_PROVIDERS),
|
||||
provider: z.enum(SECRET_STORAGE_PROVIDERS),
|
||||
displayName: z.string().trim().min(1).max(120),
|
||||
status: z.enum(SECRET_PROVIDER_CONFIG_STATUSES).optional(),
|
||||
isDefault: z.boolean().optional(),
|
||||
@@ -263,7 +320,7 @@ export const remoteSecretImportPreviewSchema = z.object({
|
||||
export type RemoteSecretImportPreview = z.infer<typeof remoteSecretImportPreviewSchema>;
|
||||
|
||||
export const secretProviderConfigDiscoveryPreviewSchema = z.object({
|
||||
provider: z.enum(SECRET_PROVIDERS),
|
||||
provider: z.enum(SECRET_STORAGE_PROVIDERS),
|
||||
config: z.record(z.unknown()).default({}),
|
||||
query: z.string().trim().max(200).optional().nullable(),
|
||||
nextToken: z.string().trim().min(1).max(4096).optional().nullable(),
|
||||
|
||||
@@ -145,6 +145,50 @@ describeEmbeddedPostgres("secretService", () => {
|
||||
).rejects.toThrow(/already exists/i);
|
||||
});
|
||||
|
||||
it("persists dynamic command secrets and operator static argv bindings without secret material", async () => {
|
||||
const companyId = await seedCompany();
|
||||
const svc = secretService(db);
|
||||
const secret = await svc.create(companyId, {
|
||||
name: `dynamic-${randomUUID()}`,
|
||||
provider: "host_command",
|
||||
managedMode: "dynamic_command",
|
||||
dynamicCommand: {
|
||||
provider: "host-command",
|
||||
command: "/usr/local/bin/mint-github-token",
|
||||
ttlSeconds: 3600,
|
||||
},
|
||||
});
|
||||
|
||||
expect(secret).toMatchObject({
|
||||
managedMode: "dynamic_command",
|
||||
provider: "host_command",
|
||||
externalRef: null,
|
||||
latestVersion: 0,
|
||||
dynamicCommand: {
|
||||
provider: "host-command",
|
||||
command: "/usr/local/bin/mint-github-token",
|
||||
ttlSeconds: 3600,
|
||||
},
|
||||
});
|
||||
|
||||
const versions = await db
|
||||
.select()
|
||||
.from(companySecretVersions)
|
||||
.where(eq(companySecretVersions.secretId, secret.id));
|
||||
expect(versions).toHaveLength(0);
|
||||
|
||||
const binding = await svc.createBinding({
|
||||
companyId,
|
||||
secretId: secret.id,
|
||||
targetType: "agent",
|
||||
targetId: "agent-1",
|
||||
configPath: "env.GITHUB_TOKEN",
|
||||
staticArgv: ["--installation", "12345"],
|
||||
});
|
||||
|
||||
expect(binding.staticArgv).toEqual(["--installation", "12345"]);
|
||||
});
|
||||
|
||||
it("syncs top-level secret refs idempotently", async () => {
|
||||
const companyId = await seedCompany();
|
||||
const svc = secretService(db);
|
||||
|
||||
@@ -10,13 +10,13 @@ import {
|
||||
BIND_MODES,
|
||||
DEPLOYMENT_EXPOSURES,
|
||||
DEPLOYMENT_MODES,
|
||||
SECRET_PROVIDERS,
|
||||
SECRET_STORAGE_PROVIDERS,
|
||||
STORAGE_PROVIDERS,
|
||||
type BindMode,
|
||||
type AuthBaseUrlMode,
|
||||
type DeploymentExposure,
|
||||
type DeploymentMode,
|
||||
type SecretProvider,
|
||||
type SecretStorageProvider,
|
||||
type StorageProvider,
|
||||
inferBindModeFromHost,
|
||||
resolveRuntimeBind,
|
||||
@@ -71,7 +71,7 @@ export interface Config {
|
||||
databaseBackupDir: string;
|
||||
serveUi: boolean;
|
||||
uiDevMiddleware: boolean;
|
||||
secretsProvider: SecretProvider;
|
||||
secretsProvider: SecretStorageProvider;
|
||||
secretsStrictMode: boolean;
|
||||
secretsMasterKeyFilePath: string;
|
||||
storageProvider: StorageProvider;
|
||||
@@ -123,11 +123,11 @@ export function loadConfig(): Config {
|
||||
|
||||
const providerFromEnvRaw = process.env.PAPERCLIP_SECRETS_PROVIDER;
|
||||
const providerFromEnv =
|
||||
providerFromEnvRaw && SECRET_PROVIDERS.includes(providerFromEnvRaw as SecretProvider)
|
||||
? (providerFromEnvRaw as SecretProvider)
|
||||
providerFromEnvRaw && SECRET_STORAGE_PROVIDERS.includes(providerFromEnvRaw as SecretStorageProvider)
|
||||
? (providerFromEnvRaw as SecretStorageProvider)
|
||||
: null;
|
||||
const providerFromFile = fileSecrets?.provider;
|
||||
const secretsProvider: SecretProvider = providerFromEnv ?? providerFromFile ?? "local_encrypted";
|
||||
const secretsProvider: SecretStorageProvider = providerFromEnv ?? providerFromFile ?? "local_encrypted";
|
||||
|
||||
const storageProviderFromEnvRaw = process.env.PAPERCLIP_STORAGE_PROVIDER;
|
||||
const storageProviderFromEnv =
|
||||
|
||||
@@ -279,10 +279,11 @@ export function secretRoutes(db: Db) {
|
||||
{
|
||||
name: req.body.name,
|
||||
key: req.body.key,
|
||||
provider: req.body.provider ?? defaultProvider,
|
||||
provider: req.body.provider ?? (req.body.managedMode === "dynamic_command" ? "host_command" : defaultProvider),
|
||||
providerConfigId: req.body.providerConfigId,
|
||||
managedMode: req.body.managedMode,
|
||||
value: req.body.value,
|
||||
dynamicCommand: req.body.dynamicCommand,
|
||||
description: req.body.description,
|
||||
externalRef: req.body.externalRef,
|
||||
providerVersionRef: req.body.providerVersionRef,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { SECRET_PROVIDERS, type SecretProvider } from "@paperclipai/shared";
|
||||
import { SECRET_STORAGE_PROVIDERS, type SecretStorageProvider } from "@paperclipai/shared";
|
||||
|
||||
export function getConfiguredSecretProvider(): SecretProvider {
|
||||
export function getConfiguredSecretProvider(): SecretStorageProvider {
|
||||
const configuredProvider = process.env.PAPERCLIP_SECRETS_PROVIDER;
|
||||
return configuredProvider && SECRET_PROVIDERS.includes(configuredProvider as SecretProvider)
|
||||
? configuredProvider as SecretProvider
|
||||
return configuredProvider && SECRET_STORAGE_PROVIDERS.includes(configuredProvider as SecretStorageProvider)
|
||||
? configuredProvider as SecretStorageProvider
|
||||
: "local_encrypted";
|
||||
}
|
||||
@@ -27,6 +27,7 @@ import type {
|
||||
SecretProviderConfigHealthStatus,
|
||||
SecretProviderConfigStatus,
|
||||
SecretVersionSelector,
|
||||
DynamicSecretCommandConfig,
|
||||
} from "@paperclipai/shared";
|
||||
import {
|
||||
createSecretProviderConfigSchema,
|
||||
@@ -64,6 +65,7 @@ const COMING_SOON_SECRET_PROVIDERS: ReadonlySet<SecretProvider> = new Set([
|
||||
"gcp_secret_manager",
|
||||
"vault",
|
||||
]);
|
||||
const EMPTY_STATIC_ARGV: string[] = [];
|
||||
type DbTransaction = Parameters<Parameters<Db["transaction"]>[0]>[0];
|
||||
type SecretBindingDb = Pick<Db | DbTransaction, "select" | "delete" | "insert">;
|
||||
|
||||
@@ -1700,7 +1702,8 @@ export function secretService(db: Db) {
|
||||
providerConfigId?: string | null;
|
||||
value?: string | null;
|
||||
key?: string | null;
|
||||
managedMode?: "paperclip_managed" | "external_reference";
|
||||
managedMode?: "paperclip_managed" | "external_reference" | "dynamic_command";
|
||||
dynamicCommand?: DynamicSecretCommandConfig | null;
|
||||
description?: string | null;
|
||||
externalRef?: string | null;
|
||||
providerVersionRef?: string | null;
|
||||
@@ -1724,21 +1727,61 @@ export function secretService(db: Db) {
|
||||
if (duplicateKey) throw conflict(`Secret key already exists: ${key}`);
|
||||
|
||||
const managedMode = input.managedMode ?? "paperclip_managed";
|
||||
if (managedMode === "external_reference" && !input.externalRef?.trim()) {
|
||||
throw unprocessable("External reference secrets require externalRef");
|
||||
}
|
||||
if (managedMode === "external_reference" && input.provider === "host_command") {
|
||||
throw unprocessable("Host command provider requires dynamic_command mode");
|
||||
}
|
||||
if (managedMode === "paperclip_managed" && input.externalRef?.trim()) {
|
||||
throw unprocessable("Managed secrets cannot override externalRef");
|
||||
}
|
||||
if (managedMode === "paperclip_managed" && input.provider === "host_command") {
|
||||
throw unprocessable("Host command provider requires dynamic_command mode");
|
||||
}
|
||||
if (managedMode === "paperclip_managed" && !input.value?.trim()) {
|
||||
throw unprocessable("Managed secrets require value");
|
||||
}
|
||||
if (managedMode === "dynamic_command") {
|
||||
if (input.provider !== "host_command") {
|
||||
throw unprocessable("Dynamic command secrets require host_command provider");
|
||||
}
|
||||
if (!input.dynamicCommand) {
|
||||
throw unprocessable("Dynamic command secrets require generator command config");
|
||||
}
|
||||
if (input.value?.trim()) {
|
||||
throw unprocessable("Dynamic command secrets cannot set value");
|
||||
}
|
||||
if (input.externalRef?.trim()) {
|
||||
throw unprocessable("Dynamic command secrets cannot set externalRef");
|
||||
}
|
||||
return db
|
||||
.insert(companySecrets)
|
||||
.values({
|
||||
companyId,
|
||||
key,
|
||||
name: input.name,
|
||||
provider: input.provider,
|
||||
providerConfigId: input.providerConfigId ?? null,
|
||||
status: "active",
|
||||
managedMode,
|
||||
externalRef: null,
|
||||
dynamicCommand: input.dynamicCommand,
|
||||
providerMetadata: input.providerMetadata ?? null,
|
||||
latestVersion: 0,
|
||||
description: input.description ?? null,
|
||||
createdByAgentId: actor?.agentId ?? null,
|
||||
createdByUserId: actor?.userId ?? null,
|
||||
})
|
||||
.returning()
|
||||
.then((rows) => rows[0]);
|
||||
}
|
||||
const provider = getSecretProvider(input.provider);
|
||||
const providerConfig = await getSelectableRuntimeProviderConfig({
|
||||
companyId,
|
||||
provider: input.provider,
|
||||
providerConfigId: input.providerConfigId,
|
||||
});
|
||||
if (managedMode === "external_reference" && !input.externalRef?.trim()) {
|
||||
throw unprocessable("External reference secrets require externalRef");
|
||||
}
|
||||
if (managedMode === "paperclip_managed" && input.externalRef?.trim()) {
|
||||
throw unprocessable("Managed secrets cannot override externalRef");
|
||||
}
|
||||
if (managedMode === "paperclip_managed" && !input.value?.trim()) {
|
||||
throw unprocessable("Managed secrets require value");
|
||||
}
|
||||
const providerWriteContext = {
|
||||
companyId,
|
||||
secretKey: key,
|
||||
@@ -1756,6 +1799,7 @@ export function secretService(db: Db) {
|
||||
status: "archived",
|
||||
managedMode,
|
||||
externalRef: null,
|
||||
dynamicCommand: null,
|
||||
providerMetadata: input.providerMetadata ?? null,
|
||||
latestVersion: 0,
|
||||
description: input.description ?? null,
|
||||
@@ -1894,6 +1938,9 @@ export function secretService(db: Db) {
|
||||
providerConfigId,
|
||||
});
|
||||
const nextVersion = secret.latestVersion + 1;
|
||||
if (secret.managedMode === "dynamic_command") {
|
||||
throw unprocessable("Dynamic command secrets do not support rotation; update generator config instead");
|
||||
}
|
||||
if (secret.managedMode === "external_reference" && !(input.externalRef ?? secret.externalRef)?.trim()) {
|
||||
throw unprocessable("External reference secrets require externalRef");
|
||||
}
|
||||
@@ -2049,7 +2096,10 @@ export function secretService(db: Db) {
|
||||
if (deleting && secret.managedMode === "paperclip_managed") {
|
||||
throw unprocessable("Managed secrets must be deleted through DELETE /secrets/:id");
|
||||
}
|
||||
if (secret.managedMode !== "external_reference" && patch.externalRef !== undefined) {
|
||||
if (secret.managedMode === "dynamic_command" && patch.externalRef !== undefined) {
|
||||
throw unprocessable("Dynamic command secrets cannot override externalRef");
|
||||
}
|
||||
if (secret.managedMode === "paperclip_managed" && patch.externalRef !== undefined) {
|
||||
throw unprocessable("Managed secrets cannot override externalRef");
|
||||
}
|
||||
if (
|
||||
@@ -2118,6 +2168,7 @@ export function secretService(db: Db) {
|
||||
versionSelector?: SecretVersionSelector;
|
||||
required?: boolean;
|
||||
label?: string | null;
|
||||
staticArgv?: string[];
|
||||
}) => {
|
||||
await assertSecretInCompany(input.companyId, input.secretId);
|
||||
const existing = await db
|
||||
@@ -2144,6 +2195,7 @@ export function secretService(db: Db) {
|
||||
versionSelector: String(input.versionSelector ?? "latest"),
|
||||
required: input.required ?? true,
|
||||
label: input.label ?? null,
|
||||
staticArgv: input.staticArgv ?? EMPTY_STATIC_ARGV,
|
||||
})
|
||||
.returning()
|
||||
.then((rows) => rows[0]);
|
||||
@@ -2158,6 +2210,7 @@ export function secretService(db: Db) {
|
||||
versionSelector?: SecretVersionSelector;
|
||||
required?: boolean;
|
||||
label?: string | null;
|
||||
staticArgv?: string[];
|
||||
}>,
|
||||
) => {
|
||||
const normalizedRefs: Array<{
|
||||
@@ -2166,6 +2219,7 @@ export function secretService(db: Db) {
|
||||
versionSelector: SecretVersionSelector;
|
||||
required: boolean;
|
||||
label: string | null;
|
||||
staticArgv: string[];
|
||||
}> = [];
|
||||
for (const ref of refs) {
|
||||
await assertSecretInCompany(companyId, ref.secretId);
|
||||
@@ -2175,6 +2229,7 @@ export function secretService(db: Db) {
|
||||
versionSelector: ref.versionSelector ?? "latest",
|
||||
required: ref.required ?? true,
|
||||
label: ref.label ?? null,
|
||||
staticArgv: ref.staticArgv ?? EMPTY_STATIC_ARGV,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2219,6 +2274,7 @@ export function secretService(db: Db) {
|
||||
versionSelector: String(ref.versionSelector),
|
||||
required: ref.required,
|
||||
label: ref.label,
|
||||
staticArgv: ref.staticArgv,
|
||||
})),
|
||||
);
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user