Add user-specific secret contracts

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-07-01 16:43:12 +00:00
1 parent ac9a883f8b
commit 9346994b27
28 files changed
+629 -14

No files matched your search

@@ -0,0 +1,178 @@
CREATE TABLE IF NOT EXISTS "user_secret_definitions" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"company_id" uuid NOT NULL,
"key" text NOT NULL,
"name" text NOT NULL,
"description" text,
"status" text DEFAULT 'active' NOT NULL,
"provider" text DEFAULT 'local_encrypted' NOT NULL,
"managed_mode" text DEFAULT 'paperclip_managed' NOT NULL,
"provider_config_id" uuid,
"provider_metadata" jsonb,
"usage_guidance" text,
"created_by_agent_id" uuid,
"created_by_user_id" text,
"updated_by_agent_id" uuid,
"updated_by_user_id" text,
"deleted_at" timestamp with time zone,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "user_secret_declarations" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"company_id" uuid NOT NULL,
"user_secret_definition_id" uuid NOT NULL,
"target_type" text NOT NULL,
"target_id" text NOT NULL,
"config_path" text NOT NULL,
"env_key" text NOT NULL,
"version_selector" text DEFAULT 'latest' NOT NULL,
"required" boolean DEFAULT true NOT NULL,
"allow_missing_override" boolean DEFAULT false NOT NULL,
"label" text,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
ALTER TABLE "company_secrets" ADD COLUMN IF NOT EXISTS "scope" text DEFAULT 'company' NOT NULL;
--> statement-breakpoint
ALTER TABLE "company_secrets" ADD COLUMN IF NOT EXISTS "owner_user_id" text;
--> statement-breakpoint
ALTER TABLE "company_secrets" ADD COLUMN IF NOT EXISTS "user_secret_definition_id" uuid;
--> statement-breakpoint
UPDATE "company_secrets"
SET "scope" = 'company'
WHERE "scope" IS NULL;
--> statement-breakpoint
ALTER TABLE "secret_access_events" ALTER COLUMN "secret_id" DROP NOT NULL;
--> statement-breakpoint
ALTER TABLE "secret_access_events" ADD COLUMN IF NOT EXISTS "user_secret_definition_id" uuid;
--> statement-breakpoint
ALTER TABLE "secret_access_events" ADD COLUMN IF NOT EXISTS "secret_scope" text DEFAULT 'company' NOT NULL;
--> statement-breakpoint
ALTER TABLE "secret_access_events" ADD COLUMN IF NOT EXISTS "responsible_user_id" text;
--> statement-breakpoint
ALTER TABLE "secret_access_events" ADD COLUMN IF NOT EXISTS "credential_owner_user_id" text;
--> statement-breakpoint
ALTER TABLE "secret_access_events" ADD COLUMN IF NOT EXISTS "credential_subject_type" text;
--> statement-breakpoint
ALTER TABLE "secret_access_events" ADD COLUMN IF NOT EXISTS "credential_subject_id" text;
--> statement-breakpoint
ALTER TABLE "issues" ADD COLUMN IF NOT EXISTS "responsible_user_id" text;
--> statement-breakpoint
ALTER TABLE "heartbeat_runs" ADD COLUMN IF NOT EXISTS "responsible_user_id" text;
--> statement-breakpoint
ALTER TABLE "routines" ADD COLUMN IF NOT EXISTS "responsible_user_id" text;
--> statement-breakpoint
ALTER TABLE "routine_revisions" ADD COLUMN IF NOT EXISTS "responsible_user_id" text;
--> statement-breakpoint
ALTER TABLE "routine_runs" ADD COLUMN IF NOT EXISTS "responsible_user_id" text;
--> statement-breakpoint
DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'user_secret_definitions_company_id_companies_id_fk') THEN
ALTER TABLE "user_secret_definitions" ADD CONSTRAINT "user_secret_definitions_company_id_companies_id_fk" FOREIGN KEY ("company_id") REFERENCES "public"."companies"("id") ON DELETE cascade ON UPDATE no action;
END IF;
END $$;
--> statement-breakpoint
DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'user_secret_definitions_provider_config_id_company_secret_provider_configs_id_fk') THEN
ALTER TABLE "user_secret_definitions" ADD CONSTRAINT "user_secret_definitions_provider_config_id_company_secret_provider_configs_id_fk" FOREIGN KEY ("provider_config_id") REFERENCES "public"."company_secret_provider_configs"("id") ON DELETE set null ON UPDATE no action;
END IF;
END $$;
--> statement-breakpoint
DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'user_secret_definitions_created_by_agent_id_agents_id_fk') THEN
ALTER TABLE "user_secret_definitions" ADD CONSTRAINT "user_secret_definitions_created_by_agent_id_agents_id_fk" FOREIGN KEY ("created_by_agent_id") REFERENCES "public"."agents"("id") ON DELETE set null ON UPDATE no action;
END IF;
END $$;
--> statement-breakpoint
DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'user_secret_definitions_updated_by_agent_id_agents_id_fk') THEN
ALTER TABLE "user_secret_definitions" ADD CONSTRAINT "user_secret_definitions_updated_by_agent_id_agents_id_fk" FOREIGN KEY ("updated_by_agent_id") REFERENCES "public"."agents"("id") ON DELETE set null ON UPDATE no action;
END IF;
END $$;
--> statement-breakpoint
DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'user_secret_declarations_company_id_companies_id_fk') THEN
ALTER TABLE "user_secret_declarations" ADD CONSTRAINT "user_secret_declarations_company_id_companies_id_fk" FOREIGN KEY ("company_id") REFERENCES "public"."companies"("id") ON DELETE cascade ON UPDATE no action;
END IF;
END $$;
--> statement-breakpoint
DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'user_secret_declarations_user_secret_definition_id_user_secret_definitions_id_fk') THEN
ALTER TABLE "user_secret_declarations" ADD CONSTRAINT "user_secret_declarations_user_secret_definition_id_user_secret_definitions_id_fk" FOREIGN KEY ("user_secret_definition_id") REFERENCES "public"."user_secret_definitions"("id") ON DELETE cascade ON UPDATE no action;
END IF;
END $$;
--> statement-breakpoint
DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'company_secrets_user_secret_definition_id_user_secret_definitions_id_fk') THEN
ALTER TABLE "company_secrets" ADD CONSTRAINT "company_secrets_user_secret_definition_id_user_secret_definitions_id_fk" FOREIGN KEY ("user_secret_definition_id") REFERENCES "public"."user_secret_definitions"("id") ON DELETE set null ON UPDATE no action;
END IF;
END $$;
--> statement-breakpoint
DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'secret_access_events_user_secret_definition_id_user_secret_definitions_id_fk') THEN
ALTER TABLE "secret_access_events" ADD CONSTRAINT "secret_access_events_user_secret_definition_id_user_secret_definitions_id_fk" FOREIGN KEY ("user_secret_definition_id") REFERENCES "public"."user_secret_definitions"("id") ON DELETE set null ON UPDATE no action;
END IF;
END $$;
--> statement-breakpoint
DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'company_secrets_scope_shape_check') THEN
ALTER TABLE "company_secrets" ADD CONSTRAINT "company_secrets_scope_shape_check" CHECK (
("scope" = 'company' AND "owner_user_id" IS NULL AND "user_secret_definition_id" IS NULL)
OR
("scope" = 'user' AND "owner_user_id" IS NOT NULL AND "user_secret_definition_id" IS NOT NULL)
);
END IF;
END $$;
--> statement-breakpoint
DROP INDEX IF EXISTS "company_secrets_company_name_uq";
--> statement-breakpoint
DROP INDEX IF EXISTS "company_secrets_company_key_uq";
--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "user_secret_definitions_company_status_idx" ON "user_secret_definitions" USING btree ("company_id","status");
--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "user_secret_definitions_company_provider_idx" ON "user_secret_definitions" USING btree ("company_id","provider");
--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "user_secret_definitions_provider_config_idx" ON "user_secret_definitions" USING btree ("provider_config_id");
--> statement-breakpoint
CREATE UNIQUE INDEX IF NOT EXISTS "user_secret_definitions_company_key_uq" ON "user_secret_definitions" USING btree ("company_id","key") WHERE "user_secret_definitions"."deleted_at" IS NULL;
--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "user_secret_declarations_company_idx" ON "user_secret_declarations" USING btree ("company_id");
--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "user_secret_declarations_definition_idx" ON "user_secret_declarations" USING btree ("user_secret_definition_id");
--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "user_secret_declarations_target_idx" ON "user_secret_declarations" USING btree ("company_id","target_type","target_id");
--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "user_secret_declarations_company_required_idx" ON "user_secret_declarations" USING btree ("company_id","required");
--> statement-breakpoint
CREATE UNIQUE INDEX IF NOT EXISTS "user_secret_declarations_target_path_uq" ON "user_secret_declarations" USING btree ("company_id","target_type","target_id","config_path");
--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "user_secret_declarations_required_override_idx" ON "user_secret_declarations" USING btree ("company_id","allow_missing_override") WHERE "user_secret_declarations"."allow_missing_override" = true;
--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "company_secrets_company_scope_idx" ON "company_secrets" USING btree ("company_id","scope");
--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "company_secrets_company_owner_idx" ON "company_secrets" USING btree ("company_id","owner_user_id");
--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "company_secrets_user_definition_owner_idx" ON "company_secrets" USING btree ("company_id","user_secret_definition_id","owner_user_id");
--> statement-breakpoint
CREATE UNIQUE INDEX IF NOT EXISTS "company_secrets_company_name_uq" ON "company_secrets" USING btree ("company_id","name") WHERE "company_secrets"."scope" = 'company' AND "company_secrets"."deleted_at" IS NULL;
--> statement-breakpoint
CREATE UNIQUE INDEX IF NOT EXISTS "company_secrets_company_key_uq" ON "company_secrets" USING btree ("company_id","key") WHERE "company_secrets"."scope" = 'company' AND "company_secrets"."deleted_at" IS NULL;
--> statement-breakpoint
CREATE UNIQUE INDEX IF NOT EXISTS "company_secrets_user_definition_owner_uq" ON "company_secrets" USING btree ("company_id","user_secret_definition_id","owner_user_id") WHERE "company_secrets"."scope" = 'user' AND "company_secrets"."deleted_at" IS NULL;
--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "secret_access_events_user_definition_created_idx" ON "secret_access_events" USING btree ("user_secret_definition_id","created_at");
--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "secret_access_events_company_credential_owner_idx" ON "secret_access_events" USING btree ("company_id","credential_owner_user_id","created_at");
--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "issues_company_responsible_user_idx" ON "issues" USING btree ("company_id","responsible_user_id");
--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "heartbeat_runs_company_responsible_user_idx" ON "heartbeat_runs" USING btree ("company_id","responsible_user_id","created_at");
--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "routines_company_responsible_user_idx" ON "routines" USING btree ("company_id","responsible_user_id");
--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "routine_revisions_company_responsible_user_idx" ON "routine_revisions" USING btree ("company_id","responsible_user_id","created_at");
--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "routine_runs_company_responsible_user_idx" ON "routine_runs" USING btree ("company_id","responsible_user_id","created_at");
@@ -883,6 +883,13 @@
"when": 1782440100000,
"tag": "0125_environment_custom_image_templates",
"breakpoints": true
},
{
"idx": 126,
"version": "7",
"when": 1782923938661,
"tag": "0126_user_specific_secrets",
"breakpoints": true
}
]
}
+34 -3
View File
@@ -1,13 +1,18 @@
import { pgTable, uuid, text, timestamp, integer, jsonb, index, uniqueIndex } from "drizzle-orm/pg-core";
import { sql } from "drizzle-orm";
import { check, pgTable, uuid, text, timestamp, integer, jsonb, index, uniqueIndex } from "drizzle-orm/pg-core";
import { companies } from "./companies.js";
import { agents } from "./agents.js";
import { companySecretProviderConfigs } from "./company_secret_provider_configs.js";
import { userSecretDefinitions } from "./user_secret_definitions.js";
export const companySecrets = pgTable(
"company_secrets",
{
id: uuid("id").primaryKey().defaultRandom(),
companyId: uuid("company_id").notNull().references(() => companies.id),
scope: text("scope").notNull().default("company"),
ownerUserId: text("owner_user_id"),
userSecretDefinitionId: uuid("user_secret_definition_id").references(() => userSecretDefinitions.id, { onDelete: "set null" }),
key: text("key").notNull(),
name: text("name").notNull(),
provider: text("provider").notNull().default("local_encrypted"),
@@ -28,9 +33,35 @@ export const companySecrets = pgTable(
},
(table) => ({
companyIdx: index("company_secrets_company_idx").on(table.companyId),
companyScopeIdx: index("company_secrets_company_scope_idx").on(table.companyId, table.scope),
companyOwnerIdx: index("company_secrets_company_owner_idx").on(table.companyId, table.ownerUserId),
userDefinitionOwnerIdx: index("company_secrets_user_definition_owner_idx").on(
table.companyId,
table.userSecretDefinitionId,
table.ownerUserId,
),
companyProviderIdx: index("company_secrets_company_provider_idx").on(table.companyId, table.provider),
providerConfigIdx: index("company_secrets_provider_config_idx").on(table.providerConfigId),
companyNameUq: uniqueIndex("company_secrets_company_name_uq").on(table.companyId, table.name),
companyKeyUq: uniqueIndex("company_secrets_company_key_uq").on(table.companyId, table.key),
companyNameUq: uniqueIndex("company_secrets_company_name_uq")
.on(table.companyId, table.name)
.where(sql`${table.scope} = 'company' and ${table.deletedAt} is null`),
companyKeyUq: uniqueIndex("company_secrets_company_key_uq")
.on(table.companyId, table.key)
.where(sql`${table.scope} = 'company' and ${table.deletedAt} is null`),
userDefinitionOwnerUq: uniqueIndex("company_secrets_user_definition_owner_uq")
.on(table.companyId, table.userSecretDefinitionId, table.ownerUserId)
.where(sql`${table.scope} = 'user' and ${table.deletedAt} is null`),
scopeShapeCheck: check(
"company_secrets_scope_shape_check",
sql`(
${table.scope} = 'company'
and ${table.ownerUserId} is null
and ${table.userSecretDefinitionId} is null
) or (
${table.scope} = 'user'
and ${table.ownerUserId} is not null
and ${table.userSecretDefinitionId} is not null
)`,
),
}),
);
+6
View File
@@ -12,6 +12,7 @@ export const heartbeatRuns = pgTable(
invocationSource: text("invocation_source").notNull().default("on_demand"),
triggerDetail: text("trigger_detail"),
status: text("status").notNull().default("queued"),
responsibleUserId: text("responsible_user_id"),
startedAt: timestamp("started_at", { withTimezone: true }),
finishedAt: timestamp("finished_at", { withTimezone: true }),
error: text("error"),
@@ -63,6 +64,11 @@ export const heartbeatRuns = pgTable(
table.agentId,
table.startedAt,
),
companyResponsibleUserIdx: index("heartbeat_runs_company_responsible_user_idx").on(
table.companyId,
table.responsibleUserId,
table.createdAt,
),
companyLivenessIdx: index("heartbeat_runs_company_liveness_idx").on(
table.companyId,
table.livenessState,
+2
View File
@@ -84,9 +84,11 @@ export { approvals } from "./approvals.js";
export { approvalComments } from "./approval_comments.js";
export { activityLog } from "./activity_log.js";
export { companySecretProviderConfigs } from "./company_secret_provider_configs.js";
export { userSecretDefinitions } from "./user_secret_definitions.js";
export { companySecrets } from "./company_secrets.js";
export { companySecretVersions } from "./company_secret_versions.js";
export { companySecretBindings } from "./company_secret_bindings.js";
export { userSecretDeclarations } from "./user_secret_declarations.js";
export { secretAccessEvents } from "./secret_access_events.js";
export { companySkills, companySkillVersions, companySkillStars, companySkillComments } from "./company_skills.js";
export { plugins } from "./plugins.js";
+2
View File
@@ -41,6 +41,7 @@ export const issues = pgTable(
executionLockedAt: timestamp("execution_locked_at", { withTimezone: true }),
createdByAgentId: uuid("created_by_agent_id").references(() => agents.id),
createdByUserId: text("created_by_user_id"),
responsibleUserId: text("responsible_user_id"),
issueNumber: integer("issue_number"),
identifier: text("identifier"),
originKind: text("origin_kind").notNull().default("manual"),
@@ -82,6 +83,7 @@ export const issues = pgTable(
table.assigneeUserId,
table.status,
),
responsibleUserIdx: index("issues_company_responsible_user_idx").on(table.companyId, table.responsibleUserId),
parentIdx: index("issues_company_parent_idx").on(table.companyId, table.parentId),
projectIdx: index("issues_company_project_idx").on(table.companyId, table.projectId),
originIdx: index("issues_company_origin_idx").on(table.companyId, table.originKind, table.originId),
+14
View File
@@ -42,6 +42,7 @@ export const routines = pgTable(
latestRevisionNumber: integer("latest_revision_number").notNull().default(1),
createdByAgentId: uuid("created_by_agent_id").references(() => agents.id, { onDelete: "set null" }),
createdByUserId: text("created_by_user_id"),
responsibleUserId: text("responsible_user_id"),
updatedByAgentId: uuid("updated_by_agent_id").references(() => agents.id, { onDelete: "set null" }),
updatedByUserId: text("updated_by_user_id"),
lastTriggeredAt: timestamp("last_triggered_at", { withTimezone: true }),
@@ -53,6 +54,7 @@ export const routines = pgTable(
companyStatusIdx: index("routines_company_status_idx").on(table.companyId, table.status),
companyAssigneeIdx: index("routines_company_assignee_idx").on(table.companyId, table.assigneeAgentId),
companyProjectIdx: index("routines_company_project_idx").on(table.companyId, table.projectId),
companyResponsibleUserIdx: index("routines_company_responsible_user_idx").on(table.companyId, table.responsibleUserId),
companyOriginIdx: index("routines_company_origin_idx").on(table.companyId, table.originKind, table.originId),
}),
);
@@ -75,6 +77,7 @@ export const routineRevisions = pgTable(
createdByAgentId: uuid("created_by_agent_id").references(() => agents.id, { onDelete: "set null" }),
createdByUserId: text("created_by_user_id"),
createdByRunId: uuid("created_by_run_id").references(() => heartbeatRuns.id, { onDelete: "set null" }),
responsibleUserId: text("responsible_user_id"),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
},
(table) => ({
@@ -87,6 +90,11 @@ export const routineRevisions = pgTable(
table.routineId,
table.createdAt,
),
companyResponsibleUserIdx: index("routine_revisions_company_responsible_user_idx").on(
table.companyId,
table.responsibleUserId,
table.createdAt,
),
}),
);
@@ -136,6 +144,7 @@ export const routineRuns = pgTable(
status: text("status").notNull().default("received"),
triggeredAt: timestamp("triggered_at", { withTimezone: true }).notNull().defaultNow(),
routineRevisionId: uuid("routine_revision_id").references(() => routineRevisions.id, { onDelete: "set null" }),
responsibleUserId: text("responsible_user_id"),
idempotencyKey: text("idempotency_key"),
triggerPayload: jsonb("trigger_payload").$type<Record<string, unknown>>(),
dispatchFingerprint: text("dispatch_fingerprint"),
@@ -149,6 +158,11 @@ export const routineRuns = pgTable(
(table) => ({
companyRoutineIdx: index("routine_runs_company_routine_idx").on(table.companyId, table.routineId, table.createdAt),
routineRevisionIdx: index("routine_runs_revision_idx").on(table.routineRevisionId),
companyResponsibleUserIdx: index("routine_runs_company_responsible_user_idx").on(
table.companyId,
table.responsibleUserId,
table.createdAt,
),
triggerIdx: index("routine_runs_trigger_idx").on(table.triggerId, table.createdAt),
dispatchFingerprintIdx: index("routine_runs_dispatch_fingerprint_idx").on(table.routineId, table.dispatchFingerprint),
linkedIssueIdx: index("routine_runs_linked_issue_idx").on(table.linkedIssueId),
+17 -1
View File
@@ -4,15 +4,22 @@ import { companySecrets } from "./company_secrets.js";
import { heartbeatRuns } from "./heartbeat_runs.js";
import { issues } from "./issues.js";
import { plugins } from "./plugins.js";
import { userSecretDefinitions } from "./user_secret_definitions.js";
export const secretAccessEvents = pgTable(
"secret_access_events",
{
id: uuid("id").primaryKey().defaultRandom(),
companyId: uuid("company_id").notNull().references(() => companies.id),
secretId: uuid("secret_id").notNull().references(() => companySecrets.id, { onDelete: "cascade" }),
secretId: uuid("secret_id").references(() => companySecrets.id, { onDelete: "cascade" }),
userSecretDefinitionId: uuid("user_secret_definition_id").references(() => userSecretDefinitions.id, { onDelete: "set null" }),
secretScope: text("secret_scope").notNull().default("company"),
version: integer("version"),
provider: text("provider").notNull(),
responsibleUserId: text("responsible_user_id"),
credentialOwnerUserId: text("credential_owner_user_id"),
credentialSubjectType: text("credential_subject_type"),
credentialSubjectId: text("credential_subject_id"),
actorType: text("actor_type").notNull(),
actorId: text("actor_id"),
consumerType: text("consumer_type").notNull(),
@@ -28,6 +35,15 @@ export const secretAccessEvents = pgTable(
(table) => ({
companyCreatedIdx: index("secret_access_events_company_created_idx").on(table.companyId, table.createdAt),
secretCreatedIdx: index("secret_access_events_secret_created_idx").on(table.secretId, table.createdAt),
userDefinitionCreatedIdx: index("secret_access_events_user_definition_created_idx").on(
table.userSecretDefinitionId,
table.createdAt,
),
credentialOwnerIdx: index("secret_access_events_company_credential_owner_idx").on(
table.companyId,
table.credentialOwnerUserId,
table.createdAt,
),
consumerIdx: index("secret_access_events_consumer_idx").on(table.companyId, table.consumerType, table.consumerId),
runIdx: index("secret_access_events_run_idx").on(table.heartbeatRunId),
}),
@@ -0,0 +1,40 @@
import { sql } from "drizzle-orm";
import { boolean, index, pgTable, text, timestamp, uniqueIndex, uuid } from "drizzle-orm/pg-core";
import { companies } from "./companies.js";
import { userSecretDefinitions } from "./user_secret_definitions.js";
export const userSecretDeclarations = pgTable(
"user_secret_declarations",
{
id: uuid("id").primaryKey().defaultRandom(),
companyId: uuid("company_id").notNull().references(() => companies.id, { onDelete: "cascade" }),
userSecretDefinitionId: uuid("user_secret_definition_id")
.notNull()
.references(() => userSecretDefinitions.id, { onDelete: "cascade" }),
targetType: text("target_type").notNull(),
targetId: text("target_id").notNull(),
configPath: text("config_path").notNull(),
envKey: text("env_key").notNull(),
versionSelector: text("version_selector").notNull().default("latest"),
required: boolean("required").notNull().default(true),
allowMissingOverride: boolean("allow_missing_override").notNull().default(false),
label: text("label"),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
},
(table) => ({
companyIdx: index("user_secret_declarations_company_idx").on(table.companyId),
definitionIdx: index("user_secret_declarations_definition_idx").on(table.userSecretDefinitionId),
targetIdx: index("user_secret_declarations_target_idx").on(table.companyId, table.targetType, table.targetId),
companyRequiredIdx: index("user_secret_declarations_company_required_idx").on(table.companyId, table.required),
targetPathUq: uniqueIndex("user_secret_declarations_target_path_uq").on(
table.companyId,
table.targetType,
table.targetId,
table.configPath,
),
requiredOverrideCheck: index("user_secret_declarations_required_override_idx")
.on(table.companyId, table.allowMissingOverride)
.where(sql`${table.allowMissingOverride} = true`),
}),
);
@@ -0,0 +1,37 @@
import { sql } from "drizzle-orm";
import { index, jsonb, pgTable, text, timestamp, uniqueIndex, uuid } from "drizzle-orm/pg-core";
import { agents } from "./agents.js";
import { companies } from "./companies.js";
import { companySecretProviderConfigs } from "./company_secret_provider_configs.js";
export const userSecretDefinitions = pgTable(
"user_secret_definitions",
{
id: uuid("id").primaryKey().defaultRandom(),
companyId: uuid("company_id").notNull().references(() => companies.id, { onDelete: "cascade" }),
key: text("key").notNull(),
name: text("name").notNull(),
description: text("description"),
status: text("status").notNull().default("active"),
provider: text("provider").notNull().default("local_encrypted"),
managedMode: text("managed_mode").notNull().default("paperclip_managed"),
providerConfigId: uuid("provider_config_id").references(() => companySecretProviderConfigs.id, { onDelete: "set null" }),
providerMetadata: jsonb("provider_metadata").$type<Record<string, unknown>>(),
usageGuidance: text("usage_guidance"),
createdByAgentId: uuid("created_by_agent_id").references(() => agents.id, { onDelete: "set null" }),
createdByUserId: text("created_by_user_id"),
updatedByAgentId: uuid("updated_by_agent_id").references(() => agents.id, { onDelete: "set null" }),
updatedByUserId: text("updated_by_user_id"),
deletedAt: timestamp("deleted_at", { withTimezone: true }),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
},
(table) => ({
companyStatusIdx: index("user_secret_definitions_company_status_idx").on(table.companyId, table.status),
companyProviderIdx: index("user_secret_definitions_company_provider_idx").on(table.companyId, table.provider),
providerConfigIdx: index("user_secret_definitions_provider_config_idx").on(table.providerConfigId),
companyKeyUq: uniqueIndex("user_secret_definitions_company_key_uq")
.on(table.companyId, table.key)
.where(sql`${table.deletedAt} is null`),
}),
);
+1
View File
@@ -1574,6 +1574,7 @@ export function createTestHarness(options: TestHarnessOptions): TestHarness {
executionLockedAt: null,
createdByAgentId: null,
createdByUserId: null,
responsibleUserId: null,
issueNumber: null,
identifier: null,
originKind,
+5
View File
@@ -20,6 +20,11 @@ export const API = {
goals: `${API_PREFIX}/goals`,
approvals: `${API_PREFIX}/approvals`,
secrets: `${API_PREFIX}/secrets`,
userSecretDefinitions: `${API_PREFIX}/companies/:companyId/user-secret-definitions`,
userSecretDefinition: `${API_PREFIX}/companies/:companyId/user-secret-definitions/:definitionId`,
userSecretDefinitionCoverage: `${API_PREFIX}/companies/:companyId/user-secret-definitions/:definitionId/coverage`,
myUserSecrets: `${API_PREFIX}/companies/:companyId/me/user-secrets`,
myUserSecret: `${API_PREFIX}/companies/:companyId/me/user-secrets/:secretId`,
secretProviderConfigs: `${API_PREFIX}/secret-provider-configs`,
secretProviderConfigDiscoveryPreview: `${API_PREFIX}/companies/:companyId/secret-provider-configs/discovery/preview`,
costs: `${API_PREFIX}/costs`,
+12 -1
View File
@@ -618,6 +618,9 @@ export type SecretProviderConfigHealthStatus =
export const SECRET_STATUSES = ["active", "disabled", "archived", "deleted"] as const;
export type SecretStatus = (typeof SECRET_STATUSES)[number];
export const SECRET_SCOPES = ["company", "user"] as const;
export type SecretScope = (typeof SECRET_SCOPES)[number];
export const SECRET_MANAGED_MODES = ["paperclip_managed", "external_reference"] as const;
export type SecretManagedMode = (typeof SECRET_MANAGED_MODES)[number];
@@ -642,7 +645,15 @@ export const SECRET_BINDING_TARGET_TYPES = [
] as const;
export type SecretBindingTargetType = (typeof SECRET_BINDING_TARGET_TYPES)[number];
export const SECRET_ACCESS_OUTCOMES = ["success", "failure"] as const;
export const SECRET_ACCESS_OUTCOMES = [
"success",
"failure",
"missing",
"inactive",
"not_allowed",
"optional_omitted",
"provider_error",
] as const;
export type SecretAccessOutcome = (typeof SECRET_ACCESS_OUTCOMES)[number];
export const STORAGE_PROVIDERS = ["local_disk", "s3"] as const;
+12
View File
@@ -183,6 +183,7 @@ export {
SECRET_PROVIDERS,
SECRET_PROVIDER_CONFIG_STATUSES,
SECRET_PROVIDER_CONFIG_HEALTH_STATUSES,
SECRET_SCOPES,
STORAGE_PROVIDERS,
BILLING_TYPES,
FINANCE_EVENT_KINDS,
@@ -320,6 +321,7 @@ export {
type SecretProvider,
type SecretProviderConfigStatus,
type SecretProviderConfigHealthStatus,
type SecretScope,
type StorageProvider,
type BillingType,
type FinanceEventKind,
@@ -811,8 +813,12 @@ export type {
EnvBinding,
EnvPlainBinding,
EnvSecretRefBinding,
EnvUserSecretRefBinding,
AgentEnvConfig,
CompanySecret,
UserSecretDefinition,
UserSecretDeclaration,
UserSecretCoverageSummary,
CompanySecretProviderConfig,
SecretProviderConfigPayload,
SecretProviderConfigHealthDetails,
@@ -1279,9 +1285,15 @@ export {
type AddApprovalComment,
envBindingPlainSchema,
envBindingSecretRefSchema,
envBindingUserSecretRefSchema,
envBindingSchema,
envConfigSchema,
createSecretSchema,
createUserSecretDefinitionSchema,
updateUserSecretDefinitionSchema,
createUserSecretValueSchema,
updateUserSecretValueSchema,
createUserSecretDeclarationSchema,
createSecretProviderConfigSchema,
updateSecretProviderConfigSchema,
secretProviderConfigDiscoveryPreviewSchema,
+1
View File
@@ -15,6 +15,7 @@ export interface HeartbeatRun {
invocationSource: HeartbeatInvocationSource;
triggerDetail: WakeupTriggerDetail | null;
status: HeartbeatRunStatus;
responsibleUserId: string | null;
startedAt: Date | null;
finishedAt: Date | null;
error: string | null;
+5
View File
@@ -406,9 +406,13 @@ export type {
SecretVersionSelector,
EnvPlainBinding,
EnvSecretRefBinding,
EnvUserSecretRefBinding,
EnvBinding,
AgentEnvConfig,
CompanySecret,
UserSecretDefinition,
UserSecretDeclaration,
UserSecretCoverageSummary,
CompanySecretProviderConfig,
SecretProviderConfigPayload,
SecretProviderConfigHealthDetails,
@@ -432,6 +436,7 @@ export type {
SecretAccessOutcome,
SecretBindingTargetType,
SecretManagedMode,
SecretScope,
SecretProviderDescriptor,
SecretStatus,
SecretVersionStatus,
+1
View File
@@ -550,6 +550,7 @@ export interface Issue {
executionLockedAt: Date | null;
createdByAgentId: string | null;
createdByUserId: string | null;
responsibleUserId: string | null;
issueNumber: number | null;
identifier: string | null;
originKind?: IssueOriginKind;
+2
View File
@@ -88,6 +88,7 @@ export interface Routine {
latestRevisionNumber: number;
createdByAgentId: string | null;
createdByUserId: string | null;
responsibleUserId: string | null;
updatedByAgentId: string | null;
updatedByUserId: string | null;
lastTriggeredAt: Date | null;
@@ -126,6 +127,7 @@ export interface RoutineRevisionSnapshotRoutineV1 {
originId?: string | null;
variables: RoutineVariable[];
env: RoutineEnvConfig | null;
responsibleUserId: string | null;
}
export interface RoutineRevisionSnapshotTriggerV1 {
+65 -2
View File
@@ -5,6 +5,7 @@ import type {
SecretProvider,
SecretProviderConfigHealthStatus,
SecretProviderConfigStatus,
SecretScope,
SecretStatus,
SecretVersionStatus,
} from "../constants.js";
@@ -16,6 +17,7 @@ export type {
SecretProvider,
SecretProviderConfigHealthStatus,
SecretProviderConfigStatus,
SecretScope,
SecretStatus,
SecretVersionStatus,
};
@@ -33,14 +35,25 @@ export interface EnvSecretRefBinding {
version?: SecretVersionSelector;
}
export interface EnvUserSecretRefBinding {
type: "user_secret_ref";
key: string;
version?: SecretVersionSelector;
required?: boolean;
allowMissingOverride?: boolean;
}
// Backward-compatible: legacy plaintext string values are still accepted.
export type EnvBinding = string | EnvPlainBinding | EnvSecretRefBinding;
export type EnvBinding = string | EnvPlainBinding | EnvSecretRefBinding | EnvUserSecretRefBinding;
export type AgentEnvConfig = Record<string, EnvBinding>;
export interface CompanySecret {
id: string;
companyId: string;
scope: SecretScope;
ownerUserId: string | null;
userSecretDefinitionId: string | null;
key: string;
name: string;
provider: SecretProvider;
@@ -61,6 +74,50 @@ export interface CompanySecret {
updatedAt: Date;
}
export interface UserSecretDefinition {
id: string;
companyId: string;
key: string;
name: string;
description: string | null;
status: SecretStatus;
provider: SecretProvider;
managedMode: SecretManagedMode;
providerConfigId: string | null;
providerMetadata: Record<string, unknown> | null;
usageGuidance: string | null;
createdByAgentId: string | null;
createdByUserId: string | null;
updatedByAgentId: string | null;
updatedByUserId: string | null;
deletedAt: Date | null;
createdAt: Date;
updatedAt: Date;
}
export interface UserSecretDeclaration {
id: string;
companyId: string;
userSecretDefinitionId: string;
targetType: SecretBindingTargetType;
targetId: string;
configPath: string;
envKey: string;
versionSelector: SecretVersionSelector;
required: boolean;
allowMissingOverride: boolean;
label: string | null;
createdAt: Date;
updatedAt: Date;
}
export interface UserSecretCoverageSummary {
definitionId: string;
configuredCount: number;
missingCount: number;
inactiveCount: number;
}
export interface SecretProviderDescriptor {
id: SecretProvider;
label: string;
@@ -216,9 +273,15 @@ export interface CompanySecretUsageBinding extends CompanySecretBinding {
export interface SecretAccessEvent {
id: string;
companyId: string;
secretId: string;
secretId: string | null;
userSecretDefinitionId: string | null;
secretScope: SecretScope;
version: number | null;
provider: SecretProvider;
responsibleUserId: string | null;
credentialOwnerUserId: string | null;
credentialSubjectType: string | null;
credentialSubjectId: string | null;
actorType: "agent" | "user" | "system" | "plugin";
actorId: string | null;
consumerType: SecretBindingTargetType;
+11
View File
@@ -463,9 +463,15 @@ export {
export {
envBindingPlainSchema,
envBindingSecretRefSchema,
envBindingUserSecretRefSchema,
envBindingSchema,
envConfigSchema,
createSecretSchema,
createUserSecretDefinitionSchema,
updateUserSecretDefinitionSchema,
createUserSecretValueSchema,
updateUserSecretValueSchema,
createUserSecretDeclarationSchema,
createSecretProviderConfigSchema,
updateSecretProviderConfigSchema,
secretProviderConfigDiscoveryPreviewSchema,
@@ -483,6 +489,11 @@ export {
updateSecretSchema,
type CreateSecretBinding,
type CreateSecret,
type CreateUserSecretDefinition,
type UpdateUserSecretDefinition,
type CreateUserSecretValue,
type UpdateUserSecretValue,
type CreateUserSecretDeclaration,
type CreateSecretProviderConfig,
type UpdateSecretProviderConfig,
type SecretProviderConfigDiscoveryPreview,
@@ -96,6 +96,7 @@ export const routineRevisionSnapshotRoutineV1Schema = z.object({
catchUpPolicy: z.enum(ROUTINE_CATCH_UP_POLICIES),
variables: z.array(routineVariableSchema),
env: envConfigSchema.nullable().default(null),
responsibleUserId: z.string().nullable().default(null),
}).strict();
export const routineRevisionSnapshotTriggerV1Schema = z.object({
@@ -1,7 +1,10 @@
import { describe, expect, it } from "vitest";
import {
createUserSecretDeclarationSchema,
createUserSecretValueSchema,
createSecretProviderConfigSchema,
createSecretSchema,
envBindingUserSecretRefSchema,
remoteSecretImportPreviewSchema,
remoteSecretImportSchema,
secretProviderConfigDiscoveryPreviewSchema,
@@ -10,6 +13,42 @@ import {
} from "./secret.js";
describe("secret validators", () => {
it("defaults user secret refs to required and no missing override", () => {
expect(
envBindingUserSecretRefSchema.parse({
type: "user_secret_ref",
key: "github_api_token",
}),
).toEqual({
type: "user_secret_ref",
key: "github_api_token",
required: true,
allowMissingOverride: false,
});
});
it("validates user secret declarations and current-user value payloads", () => {
expect(
createUserSecretDeclarationSchema.parse({
targetType: "agent",
targetId: "agent-1",
configPath: "env.GITHUB_TOKEN",
envKey: "GITHUB_TOKEN",
definitionKey: "github_api_token",
}),
).toMatchObject({
versionSelector: "latest",
required: true,
allowMissingOverride: false,
});
expect(() =>
createUserSecretValueSchema.parse({
value: "secret-value",
}),
).toThrow(/definitionId or definitionKey/);
});
it("rejects externalRef on managed secrets", () => {
expect(() =>
createSecretSchema.parse({
+89 -4
View File
@@ -7,6 +7,9 @@ import {
SECRET_STATUSES,
} from "../constants.js";
const secretKeySchema = z.string().trim().min(1).max(120).regex(/^[a-zA-Z0-9_.-]+$/);
const secretVersionSelectorSchema = z.union([z.literal("latest"), z.number().int().positive()]);
export const envBindingPlainSchema = z.object({
type: z.literal("plain"),
value: z.string(),
@@ -15,7 +18,15 @@ export const envBindingPlainSchema = z.object({
export const envBindingSecretRefSchema = z.object({
type: z.literal("secret_ref"),
secretId: z.string().uuid(),
version: z.union([z.literal("latest"), z.number().int().positive()]).optional(),
version: secretVersionSelectorSchema.optional(),
});
export const envBindingUserSecretRefSchema = z.object({
type: z.literal("user_secret_ref"),
key: secretKeySchema,
version: secretVersionSelectorSchema.optional(),
required: z.boolean().optional().default(true),
allowMissingOverride: z.boolean().optional().default(false),
});
// Backward-compatible union that accepts legacy inline values.
@@ -23,13 +34,14 @@ export const envBindingSchema = z.union([
z.string(),
envBindingPlainSchema,
envBindingSecretRefSchema,
envBindingUserSecretRefSchema,
]);
export const envConfigSchema = z.record(z.string(), envBindingSchema);
export const createSecretSchema = z.object({
name: z.string().min(1),
key: z.string().min(1).regex(/^[a-zA-Z0-9_.-]+$/).optional(),
key: secretKeySchema.optional(),
provider: z.enum(SECRET_PROVIDERS).optional(),
providerConfigId: z.string().uuid().optional().nullable(),
managedMode: z.enum(SECRET_MANAGED_MODES).optional(),
@@ -78,7 +90,7 @@ export type RotateSecret = z.infer<typeof rotateSecretSchema>;
export const updateSecretSchema = z.object({
name: z.string().min(1).optional(),
key: z.string().min(1).regex(/^[a-zA-Z0-9_.-]+$/).optional(),
key: secretKeySchema.optional(),
status: z.enum(SECRET_STATUSES).optional(),
providerConfigId: z.string().uuid().optional().nullable(),
description: z.string().optional().nullable(),
@@ -96,13 +108,86 @@ export const secretBindingTargetSchema = z.object({
export const createSecretBindingSchema = secretBindingTargetSchema.extend({
secretId: z.string().uuid(),
versionSelector: z.union([z.literal("latest"), z.number().int().positive()]).default("latest"),
versionSelector: secretVersionSelectorSchema.default("latest"),
required: z.boolean().default(true),
label: z.string().optional().nullable(),
});
export type CreateSecretBinding = z.infer<typeof createSecretBindingSchema>;
export const createUserSecretDefinitionSchema = z.object({
key: secretKeySchema,
name: z.string().trim().min(1).max(160),
description: z.string().trim().max(500).optional().nullable(),
status: z.enum(SECRET_STATUSES).optional(),
provider: z.enum(SECRET_PROVIDERS).optional(),
providerConfigId: z.string().uuid().optional().nullable(),
managedMode: z.enum(SECRET_MANAGED_MODES).optional(),
providerMetadata: z.record(z.string(), z.unknown()).optional().nullable(),
usageGuidance: z.string().trim().max(1000).optional().nullable(),
});
export type CreateUserSecretDefinition = z.infer<typeof createUserSecretDefinitionSchema>;
export const updateUserSecretDefinitionSchema = z.object({
key: secretKeySchema.optional(),
name: z.string().trim().min(1).max(160).optional(),
description: z.string().trim().max(500).optional().nullable(),
status: z.enum(SECRET_STATUSES).optional(),
providerConfigId: z.string().uuid().optional().nullable(),
providerMetadata: z.record(z.string(), z.unknown()).optional().nullable(),
usageGuidance: z.string().trim().max(1000).optional().nullable(),
});
export type UpdateUserSecretDefinition = z.infer<typeof updateUserSecretDefinitionSchema>;
export const createUserSecretValueSchema = z.object({
definitionKey: secretKeySchema.optional(),
definitionId: z.string().uuid().optional(),
value: z.string().min(1).optional().nullable(),
externalRef: z.string().optional().nullable(),
providerVersionRef: z.string().optional().nullable(),
providerConfigId: z.string().uuid().optional().nullable(),
}).superRefine((value, ctx) => {
if (!value.definitionKey && !value.definitionId) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["definitionId"],
message: "User secret value requires definitionId or definitionKey",
});
}
if (!value.value?.trim() && !value.externalRef?.trim()) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["value"],
message: "User secret value requires value or externalRef",
});
}
});
export type CreateUserSecretValue = z.infer<typeof createUserSecretValueSchema>;
export const updateUserSecretValueSchema = z.object({
status: z.enum(SECRET_STATUSES).optional(),
value: z.string().min(1).optional().nullable(),
externalRef: z.string().optional().nullable(),
providerVersionRef: z.string().optional().nullable(),
providerConfigId: z.string().uuid().optional().nullable(),
});
export type UpdateUserSecretValue = z.infer<typeof updateUserSecretValueSchema>;
export const createUserSecretDeclarationSchema = secretBindingTargetSchema.extend({
definitionKey: secretKeySchema,
envKey: z.string().trim().min(1),
versionSelector: secretVersionSelectorSchema.default("latest"),
required: z.boolean().default(true),
allowMissingOverride: z.boolean().default(false),
label: z.string().optional().nullable(),
});
export type CreateUserSecretDeclaration = z.infer<typeof createUserSecretDeclarationSchema>;
const safeShortText = z.string().trim().min(1).max(160);
const optionalSafeShortText = safeShortText.optional().nullable();
+10
View File
@@ -54,6 +54,7 @@ function sanitizeValue(value: unknown): unknown {
if (value === null || value === undefined) return value;
if (Array.isArray(value)) return value.map(sanitizeValue);
if (isSecretRefBinding(value)) return value;
if (isUserSecretRefBinding(value)) return value;
if (isPlainBinding(value)) return { type: "plain", value: sanitizeValue(value.value) };
if (!isPlainObject(value)) return value;
return sanitizeRecord(value);
@@ -64,6 +65,11 @@ function isSecretRefBinding(value: unknown): value is { type: "secret_ref"; secr
return value.type === "secret_ref" && typeof value.secretId === "string";
}
function isUserSecretRefBinding(value: unknown): value is { type: "user_secret_ref"; key: string; version?: unknown } {
if (!isPlainObject(value)) return false;
return value.type === "user_secret_ref" && typeof value.key === "string";
}
function isPlainBinding(value: unknown): value is { type: "plain"; value: unknown } {
if (!isPlainObject(value)) return false;
return value.type === "plain" && "value" in value;
@@ -101,6 +107,10 @@ export function sanitizeRecord(record: Record<string, unknown>): Record<string,
redacted[key] = sanitizeValue(value);
continue;
}
if (isUserSecretRefBinding(value)) {
redacted[key] = sanitizeValue(value);
continue;
}
if (isPlainBinding(value)) {
redacted[key] = { type: "plain", value: REDACTED_EVENT_VALUE };
continue;
+1
View File
@@ -2091,6 +2091,7 @@ const issueListSelect = {
executionLockedAt: issues.executionLockedAt,
createdByAgentId: issues.createdByAgentId,
createdByUserId: issues.createdByUserId,
responsibleUserId: issues.responsibleUserId,
issueNumber: issues.issueNumber,
identifier: issues.identifier,
originKind: issues.originKind,
+1
View File
@@ -1177,6 +1177,7 @@ function routineRevisionSnapshotRoutine(routine: typeof routines.$inferSelect):
originId: routine.originId,
variables: routine.variables ?? [],
env: routine.env ?? null,
responsibleUserId: routine.responsibleUserId ?? null,
};
}
+1
View File
@@ -452,6 +452,7 @@ function routineRevisionSnapshotRoutine(routine: RoutineRow): RoutineRevisionSna
catchUpPolicy: routine.catchUpPolicy as RoutineRevisionSnapshotV1["routine"]["catchUpPolicy"],
variables: routine.variables ?? [],
env: routine.env ?? null,
responsibleUserId: routine.responsibleUserId ?? null,
};
}
+35 -3
View File
@@ -216,7 +216,14 @@ async function cleanupPreparedProviderWrite(input: {
type CanonicalEnvBinding =
| { type: "plain"; value: string }
| { type: "secret_ref"; secretId: string; version: number | "latest" };
| { type: "secret_ref"; secretId: string; version: number | "latest" }
| {
type: "user_secret_ref";
key: string;
version: number | "latest";
required: boolean;
allowMissingOverride: boolean;
};
type SecretConsumerContext = {
consumerType: SecretBindingTargetType;
@@ -303,6 +310,15 @@ function canonicalizeBinding(binding: EnvBinding): CanonicalEnvBinding {
if (binding.type === "plain") {
return { type: "plain", value: String(binding.value) };
}
if (binding.type === "user_secret_ref") {
return {
type: "user_secret_ref",
key: binding.key,
version: binding.version ?? "latest",
required: binding.required ?? true,
allowMissingOverride: binding.allowMissingOverride ?? false,
};
}
return {
type: "secret_ref",
secretId: binding.secretId,
@@ -310,6 +326,10 @@ function canonicalizeBinding(binding: EnvBinding): CanonicalEnvBinding {
};
}
function userSecretResolverUnavailable(key: string): never {
throw unprocessable(`User secret ref is declared but runtime resolution is not enabled yet: ${key}`);
}
function defaultProviderConfigStatus(provider: SecretProvider): SecretProviderConfigStatus {
return COMING_SOON_SECRET_PROVIDERS.has(provider) ? "coming_soon" : "ready";
}
@@ -807,6 +827,10 @@ export function secretService(db: Db) {
normalized[key] = binding;
continue;
}
if (binding.type === "user_secret_ref") {
normalized[key] = binding;
continue;
}
await assertSecretInCompany(companyId, binding.secretId);
normalized[key] = {
@@ -887,6 +911,9 @@ export function secretService(db: Db) {
version: binding.version,
};
}
if (binding.type === "user_secret_ref") {
throw unprocessable(`${input.key} must be a string, plain binding, or company secret reference`);
}
const value = binding.value.trim();
if (!value) return undefined;
if (value === REDACTED_SENTINEL) {
@@ -2620,7 +2647,7 @@ export function secretService(db: Db) {
const binding = canonicalizeBinding(parsed.data as EnvBinding);
if (binding.type === "plain") {
resolved[key] = binding.value;
} else {
} else if (binding.type === "secret_ref") {
const secretResolution = await resolveSecretValueInternal(
companyId,
binding.secretId,
@@ -2635,6 +2662,8 @@ export function secretService(db: Db) {
resolved[key] = secretResolution.value;
manifest.push(secretResolution.manifestEntry);
secretKeys.add(key);
} else {
userSecretResolverUnavailable(binding.key);
}
}
return { env: resolved, secretKeys, manifest };
@@ -2768,7 +2797,7 @@ export function secretService(db: Db) {
const binding = canonicalizeBinding(parsed.data as EnvBinding);
if (binding.type === "plain") {
env[key] = binding.value;
} else {
} else if (binding.type === "secret_ref") {
const secretResolution = await resolveSecretValueInternal(
companyId,
binding.secretId,
@@ -2783,6 +2812,8 @@ export function secretService(db: Db) {
env[key] = secretResolution.value;
manifest.push(secretResolution.manifestEntry);
secretKeys.add(key);
} else {
userSecretResolverUnavailable(binding.key);
}
}
resolved.env = env;
@@ -2794,6 +2825,7 @@ export function secretService(db: Db) {
if (!parsed.success) continue;
const binding = canonicalizeBinding(parsed.data as EnvBinding);
if (binding.type === "plain") continue;
if (binding.type === "user_secret_ref") userSecretResolverUnavailable(binding.key);
const secretResolution = await resolveSecretValueInternal(
companyId,
binding.secretId,