mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-26 18:31:34 +01:00
Add user-specific secret contracts
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
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,
|
"when": 1782440100000,
|
||||||
"tag": "0125_environment_custom_image_templates",
|
"tag": "0125_environment_custom_image_templates",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 126,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1782923938661,
|
||||||
|
"tag": "0126_user_specific_secrets",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -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 { companies } from "./companies.js";
|
||||||
import { agents } from "./agents.js";
|
import { agents } from "./agents.js";
|
||||||
import { companySecretProviderConfigs } from "./company_secret_provider_configs.js";
|
import { companySecretProviderConfigs } from "./company_secret_provider_configs.js";
|
||||||
|
import { userSecretDefinitions } from "./user_secret_definitions.js";
|
||||||
|
|
||||||
export const companySecrets = pgTable(
|
export const companySecrets = pgTable(
|
||||||
"company_secrets",
|
"company_secrets",
|
||||||
{
|
{
|
||||||
id: uuid("id").primaryKey().defaultRandom(),
|
id: uuid("id").primaryKey().defaultRandom(),
|
||||||
companyId: uuid("company_id").notNull().references(() => companies.id),
|
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(),
|
key: text("key").notNull(),
|
||||||
name: text("name").notNull(),
|
name: text("name").notNull(),
|
||||||
provider: text("provider").notNull().default("local_encrypted"),
|
provider: text("provider").notNull().default("local_encrypted"),
|
||||||
@@ -28,9 +33,35 @@ export const companySecrets = pgTable(
|
|||||||
},
|
},
|
||||||
(table) => ({
|
(table) => ({
|
||||||
companyIdx: index("company_secrets_company_idx").on(table.companyId),
|
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),
|
companyProviderIdx: index("company_secrets_company_provider_idx").on(table.companyId, table.provider),
|
||||||
providerConfigIdx: index("company_secrets_provider_config_idx").on(table.providerConfigId),
|
providerConfigIdx: index("company_secrets_provider_config_idx").on(table.providerConfigId),
|
||||||
companyNameUq: uniqueIndex("company_secrets_company_name_uq").on(table.companyId, table.name),
|
companyNameUq: uniqueIndex("company_secrets_company_name_uq")
|
||||||
companyKeyUq: uniqueIndex("company_secrets_company_key_uq").on(table.companyId, table.key),
|
.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
|
||||||
|
)`,
|
||||||
|
),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@@ -12,6 +12,7 @@ export const heartbeatRuns = pgTable(
|
|||||||
invocationSource: text("invocation_source").notNull().default("on_demand"),
|
invocationSource: text("invocation_source").notNull().default("on_demand"),
|
||||||
triggerDetail: text("trigger_detail"),
|
triggerDetail: text("trigger_detail"),
|
||||||
status: text("status").notNull().default("queued"),
|
status: text("status").notNull().default("queued"),
|
||||||
|
responsibleUserId: text("responsible_user_id"),
|
||||||
startedAt: timestamp("started_at", { withTimezone: true }),
|
startedAt: timestamp("started_at", { withTimezone: true }),
|
||||||
finishedAt: timestamp("finished_at", { withTimezone: true }),
|
finishedAt: timestamp("finished_at", { withTimezone: true }),
|
||||||
error: text("error"),
|
error: text("error"),
|
||||||
@@ -63,6 +64,11 @@ export const heartbeatRuns = pgTable(
|
|||||||
table.agentId,
|
table.agentId,
|
||||||
table.startedAt,
|
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(
|
companyLivenessIdx: index("heartbeat_runs_company_liveness_idx").on(
|
||||||
table.companyId,
|
table.companyId,
|
||||||
table.livenessState,
|
table.livenessState,
|
||||||
|
|||||||
@@ -84,9 +84,11 @@ export { approvals } from "./approvals.js";
|
|||||||
export { approvalComments } from "./approval_comments.js";
|
export { approvalComments } from "./approval_comments.js";
|
||||||
export { activityLog } from "./activity_log.js";
|
export { activityLog } from "./activity_log.js";
|
||||||
export { companySecretProviderConfigs } from "./company_secret_provider_configs.js";
|
export { companySecretProviderConfigs } from "./company_secret_provider_configs.js";
|
||||||
|
export { userSecretDefinitions } from "./user_secret_definitions.js";
|
||||||
export { companySecrets } from "./company_secrets.js";
|
export { companySecrets } from "./company_secrets.js";
|
||||||
export { companySecretVersions } from "./company_secret_versions.js";
|
export { companySecretVersions } from "./company_secret_versions.js";
|
||||||
export { companySecretBindings } from "./company_secret_bindings.js";
|
export { companySecretBindings } from "./company_secret_bindings.js";
|
||||||
|
export { userSecretDeclarations } from "./user_secret_declarations.js";
|
||||||
export { secretAccessEvents } from "./secret_access_events.js";
|
export { secretAccessEvents } from "./secret_access_events.js";
|
||||||
export { companySkills, companySkillVersions, companySkillStars, companySkillComments } from "./company_skills.js";
|
export { companySkills, companySkillVersions, companySkillStars, companySkillComments } from "./company_skills.js";
|
||||||
export { plugins } from "./plugins.js";
|
export { plugins } from "./plugins.js";
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ export const issues = pgTable(
|
|||||||
executionLockedAt: timestamp("execution_locked_at", { withTimezone: true }),
|
executionLockedAt: timestamp("execution_locked_at", { withTimezone: true }),
|
||||||
createdByAgentId: uuid("created_by_agent_id").references(() => agents.id),
|
createdByAgentId: uuid("created_by_agent_id").references(() => agents.id),
|
||||||
createdByUserId: text("created_by_user_id"),
|
createdByUserId: text("created_by_user_id"),
|
||||||
|
responsibleUserId: text("responsible_user_id"),
|
||||||
issueNumber: integer("issue_number"),
|
issueNumber: integer("issue_number"),
|
||||||
identifier: text("identifier"),
|
identifier: text("identifier"),
|
||||||
originKind: text("origin_kind").notNull().default("manual"),
|
originKind: text("origin_kind").notNull().default("manual"),
|
||||||
@@ -82,6 +83,7 @@ export const issues = pgTable(
|
|||||||
table.assigneeUserId,
|
table.assigneeUserId,
|
||||||
table.status,
|
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),
|
parentIdx: index("issues_company_parent_idx").on(table.companyId, table.parentId),
|
||||||
projectIdx: index("issues_company_project_idx").on(table.companyId, table.projectId),
|
projectIdx: index("issues_company_project_idx").on(table.companyId, table.projectId),
|
||||||
originIdx: index("issues_company_origin_idx").on(table.companyId, table.originKind, table.originId),
|
originIdx: index("issues_company_origin_idx").on(table.companyId, table.originKind, table.originId),
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ export const routines = pgTable(
|
|||||||
latestRevisionNumber: integer("latest_revision_number").notNull().default(1),
|
latestRevisionNumber: integer("latest_revision_number").notNull().default(1),
|
||||||
createdByAgentId: uuid("created_by_agent_id").references(() => agents.id, { onDelete: "set null" }),
|
createdByAgentId: uuid("created_by_agent_id").references(() => agents.id, { onDelete: "set null" }),
|
||||||
createdByUserId: text("created_by_user_id"),
|
createdByUserId: text("created_by_user_id"),
|
||||||
|
responsibleUserId: text("responsible_user_id"),
|
||||||
updatedByAgentId: uuid("updated_by_agent_id").references(() => agents.id, { onDelete: "set null" }),
|
updatedByAgentId: uuid("updated_by_agent_id").references(() => agents.id, { onDelete: "set null" }),
|
||||||
updatedByUserId: text("updated_by_user_id"),
|
updatedByUserId: text("updated_by_user_id"),
|
||||||
lastTriggeredAt: timestamp("last_triggered_at", { withTimezone: true }),
|
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),
|
companyStatusIdx: index("routines_company_status_idx").on(table.companyId, table.status),
|
||||||
companyAssigneeIdx: index("routines_company_assignee_idx").on(table.companyId, table.assigneeAgentId),
|
companyAssigneeIdx: index("routines_company_assignee_idx").on(table.companyId, table.assigneeAgentId),
|
||||||
companyProjectIdx: index("routines_company_project_idx").on(table.companyId, table.projectId),
|
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),
|
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" }),
|
createdByAgentId: uuid("created_by_agent_id").references(() => agents.id, { onDelete: "set null" }),
|
||||||
createdByUserId: text("created_by_user_id"),
|
createdByUserId: text("created_by_user_id"),
|
||||||
createdByRunId: uuid("created_by_run_id").references(() => heartbeatRuns.id, { onDelete: "set null" }),
|
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(),
|
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||||
},
|
},
|
||||||
(table) => ({
|
(table) => ({
|
||||||
@@ -87,6 +90,11 @@ export const routineRevisions = pgTable(
|
|||||||
table.routineId,
|
table.routineId,
|
||||||
table.createdAt,
|
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"),
|
status: text("status").notNull().default("received"),
|
||||||
triggeredAt: timestamp("triggered_at", { withTimezone: true }).notNull().defaultNow(),
|
triggeredAt: timestamp("triggered_at", { withTimezone: true }).notNull().defaultNow(),
|
||||||
routineRevisionId: uuid("routine_revision_id").references(() => routineRevisions.id, { onDelete: "set null" }),
|
routineRevisionId: uuid("routine_revision_id").references(() => routineRevisions.id, { onDelete: "set null" }),
|
||||||
|
responsibleUserId: text("responsible_user_id"),
|
||||||
idempotencyKey: text("idempotency_key"),
|
idempotencyKey: text("idempotency_key"),
|
||||||
triggerPayload: jsonb("trigger_payload").$type<Record<string, unknown>>(),
|
triggerPayload: jsonb("trigger_payload").$type<Record<string, unknown>>(),
|
||||||
dispatchFingerprint: text("dispatch_fingerprint"),
|
dispatchFingerprint: text("dispatch_fingerprint"),
|
||||||
@@ -149,6 +158,11 @@ export const routineRuns = pgTable(
|
|||||||
(table) => ({
|
(table) => ({
|
||||||
companyRoutineIdx: index("routine_runs_company_routine_idx").on(table.companyId, table.routineId, table.createdAt),
|
companyRoutineIdx: index("routine_runs_company_routine_idx").on(table.companyId, table.routineId, table.createdAt),
|
||||||
routineRevisionIdx: index("routine_runs_revision_idx").on(table.routineRevisionId),
|
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),
|
triggerIdx: index("routine_runs_trigger_idx").on(table.triggerId, table.createdAt),
|
||||||
dispatchFingerprintIdx: index("routine_runs_dispatch_fingerprint_idx").on(table.routineId, table.dispatchFingerprint),
|
dispatchFingerprintIdx: index("routine_runs_dispatch_fingerprint_idx").on(table.routineId, table.dispatchFingerprint),
|
||||||
linkedIssueIdx: index("routine_runs_linked_issue_idx").on(table.linkedIssueId),
|
linkedIssueIdx: index("routine_runs_linked_issue_idx").on(table.linkedIssueId),
|
||||||
|
|||||||
@@ -4,15 +4,22 @@ import { companySecrets } from "./company_secrets.js";
|
|||||||
import { heartbeatRuns } from "./heartbeat_runs.js";
|
import { heartbeatRuns } from "./heartbeat_runs.js";
|
||||||
import { issues } from "./issues.js";
|
import { issues } from "./issues.js";
|
||||||
import { plugins } from "./plugins.js";
|
import { plugins } from "./plugins.js";
|
||||||
|
import { userSecretDefinitions } from "./user_secret_definitions.js";
|
||||||
|
|
||||||
export const secretAccessEvents = pgTable(
|
export const secretAccessEvents = pgTable(
|
||||||
"secret_access_events",
|
"secret_access_events",
|
||||||
{
|
{
|
||||||
id: uuid("id").primaryKey().defaultRandom(),
|
id: uuid("id").primaryKey().defaultRandom(),
|
||||||
companyId: uuid("company_id").notNull().references(() => companies.id),
|
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"),
|
version: integer("version"),
|
||||||
provider: text("provider").notNull(),
|
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(),
|
actorType: text("actor_type").notNull(),
|
||||||
actorId: text("actor_id"),
|
actorId: text("actor_id"),
|
||||||
consumerType: text("consumer_type").notNull(),
|
consumerType: text("consumer_type").notNull(),
|
||||||
@@ -28,6 +35,15 @@ export const secretAccessEvents = pgTable(
|
|||||||
(table) => ({
|
(table) => ({
|
||||||
companyCreatedIdx: index("secret_access_events_company_created_idx").on(table.companyId, table.createdAt),
|
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),
|
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),
|
consumerIdx: index("secret_access_events_consumer_idx").on(table.companyId, table.consumerType, table.consumerId),
|
||||||
runIdx: index("secret_access_events_run_idx").on(table.heartbeatRunId),
|
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`),
|
||||||
|
}),
|
||||||
|
);
|
||||||
@@ -1574,6 +1574,7 @@ export function createTestHarness(options: TestHarnessOptions): TestHarness {
|
|||||||
executionLockedAt: null,
|
executionLockedAt: null,
|
||||||
createdByAgentId: null,
|
createdByAgentId: null,
|
||||||
createdByUserId: null,
|
createdByUserId: null,
|
||||||
|
responsibleUserId: null,
|
||||||
issueNumber: null,
|
issueNumber: null,
|
||||||
identifier: null,
|
identifier: null,
|
||||||
originKind,
|
originKind,
|
||||||
|
|||||||
@@ -20,6 +20,11 @@ export const API = {
|
|||||||
goals: `${API_PREFIX}/goals`,
|
goals: `${API_PREFIX}/goals`,
|
||||||
approvals: `${API_PREFIX}/approvals`,
|
approvals: `${API_PREFIX}/approvals`,
|
||||||
secrets: `${API_PREFIX}/secrets`,
|
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`,
|
secretProviderConfigs: `${API_PREFIX}/secret-provider-configs`,
|
||||||
secretProviderConfigDiscoveryPreview: `${API_PREFIX}/companies/:companyId/secret-provider-configs/discovery/preview`,
|
secretProviderConfigDiscoveryPreview: `${API_PREFIX}/companies/:companyId/secret-provider-configs/discovery/preview`,
|
||||||
costs: `${API_PREFIX}/costs`,
|
costs: `${API_PREFIX}/costs`,
|
||||||
|
|||||||
@@ -618,6 +618,9 @@ export type SecretProviderConfigHealthStatus =
|
|||||||
export const SECRET_STATUSES = ["active", "disabled", "archived", "deleted"] as const;
|
export const SECRET_STATUSES = ["active", "disabled", "archived", "deleted"] as const;
|
||||||
export type SecretStatus = (typeof SECRET_STATUSES)[number];
|
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 const SECRET_MANAGED_MODES = ["paperclip_managed", "external_reference"] as const;
|
||||||
export type SecretManagedMode = (typeof SECRET_MANAGED_MODES)[number];
|
export type SecretManagedMode = (typeof SECRET_MANAGED_MODES)[number];
|
||||||
|
|
||||||
@@ -642,7 +645,15 @@ export const SECRET_BINDING_TARGET_TYPES = [
|
|||||||
] as const;
|
] as const;
|
||||||
export type SecretBindingTargetType = (typeof SECRET_BINDING_TARGET_TYPES)[number];
|
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 type SecretAccessOutcome = (typeof SECRET_ACCESS_OUTCOMES)[number];
|
||||||
|
|
||||||
export const STORAGE_PROVIDERS = ["local_disk", "s3"] as const;
|
export const STORAGE_PROVIDERS = ["local_disk", "s3"] as const;
|
||||||
|
|||||||
@@ -183,6 +183,7 @@ export {
|
|||||||
SECRET_PROVIDERS,
|
SECRET_PROVIDERS,
|
||||||
SECRET_PROVIDER_CONFIG_STATUSES,
|
SECRET_PROVIDER_CONFIG_STATUSES,
|
||||||
SECRET_PROVIDER_CONFIG_HEALTH_STATUSES,
|
SECRET_PROVIDER_CONFIG_HEALTH_STATUSES,
|
||||||
|
SECRET_SCOPES,
|
||||||
STORAGE_PROVIDERS,
|
STORAGE_PROVIDERS,
|
||||||
BILLING_TYPES,
|
BILLING_TYPES,
|
||||||
FINANCE_EVENT_KINDS,
|
FINANCE_EVENT_KINDS,
|
||||||
@@ -320,6 +321,7 @@ export {
|
|||||||
type SecretProvider,
|
type SecretProvider,
|
||||||
type SecretProviderConfigStatus,
|
type SecretProviderConfigStatus,
|
||||||
type SecretProviderConfigHealthStatus,
|
type SecretProviderConfigHealthStatus,
|
||||||
|
type SecretScope,
|
||||||
type StorageProvider,
|
type StorageProvider,
|
||||||
type BillingType,
|
type BillingType,
|
||||||
type FinanceEventKind,
|
type FinanceEventKind,
|
||||||
@@ -811,8 +813,12 @@ export type {
|
|||||||
EnvBinding,
|
EnvBinding,
|
||||||
EnvPlainBinding,
|
EnvPlainBinding,
|
||||||
EnvSecretRefBinding,
|
EnvSecretRefBinding,
|
||||||
|
EnvUserSecretRefBinding,
|
||||||
AgentEnvConfig,
|
AgentEnvConfig,
|
||||||
CompanySecret,
|
CompanySecret,
|
||||||
|
UserSecretDefinition,
|
||||||
|
UserSecretDeclaration,
|
||||||
|
UserSecretCoverageSummary,
|
||||||
CompanySecretProviderConfig,
|
CompanySecretProviderConfig,
|
||||||
SecretProviderConfigPayload,
|
SecretProviderConfigPayload,
|
||||||
SecretProviderConfigHealthDetails,
|
SecretProviderConfigHealthDetails,
|
||||||
@@ -1279,9 +1285,15 @@ export {
|
|||||||
type AddApprovalComment,
|
type AddApprovalComment,
|
||||||
envBindingPlainSchema,
|
envBindingPlainSchema,
|
||||||
envBindingSecretRefSchema,
|
envBindingSecretRefSchema,
|
||||||
|
envBindingUserSecretRefSchema,
|
||||||
envBindingSchema,
|
envBindingSchema,
|
||||||
envConfigSchema,
|
envConfigSchema,
|
||||||
createSecretSchema,
|
createSecretSchema,
|
||||||
|
createUserSecretDefinitionSchema,
|
||||||
|
updateUserSecretDefinitionSchema,
|
||||||
|
createUserSecretValueSchema,
|
||||||
|
updateUserSecretValueSchema,
|
||||||
|
createUserSecretDeclarationSchema,
|
||||||
createSecretProviderConfigSchema,
|
createSecretProviderConfigSchema,
|
||||||
updateSecretProviderConfigSchema,
|
updateSecretProviderConfigSchema,
|
||||||
secretProviderConfigDiscoveryPreviewSchema,
|
secretProviderConfigDiscoveryPreviewSchema,
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export interface HeartbeatRun {
|
|||||||
invocationSource: HeartbeatInvocationSource;
|
invocationSource: HeartbeatInvocationSource;
|
||||||
triggerDetail: WakeupTriggerDetail | null;
|
triggerDetail: WakeupTriggerDetail | null;
|
||||||
status: HeartbeatRunStatus;
|
status: HeartbeatRunStatus;
|
||||||
|
responsibleUserId: string | null;
|
||||||
startedAt: Date | null;
|
startedAt: Date | null;
|
||||||
finishedAt: Date | null;
|
finishedAt: Date | null;
|
||||||
error: string | null;
|
error: string | null;
|
||||||
|
|||||||
@@ -406,9 +406,13 @@ export type {
|
|||||||
SecretVersionSelector,
|
SecretVersionSelector,
|
||||||
EnvPlainBinding,
|
EnvPlainBinding,
|
||||||
EnvSecretRefBinding,
|
EnvSecretRefBinding,
|
||||||
|
EnvUserSecretRefBinding,
|
||||||
EnvBinding,
|
EnvBinding,
|
||||||
AgentEnvConfig,
|
AgentEnvConfig,
|
||||||
CompanySecret,
|
CompanySecret,
|
||||||
|
UserSecretDefinition,
|
||||||
|
UserSecretDeclaration,
|
||||||
|
UserSecretCoverageSummary,
|
||||||
CompanySecretProviderConfig,
|
CompanySecretProviderConfig,
|
||||||
SecretProviderConfigPayload,
|
SecretProviderConfigPayload,
|
||||||
SecretProviderConfigHealthDetails,
|
SecretProviderConfigHealthDetails,
|
||||||
@@ -432,6 +436,7 @@ export type {
|
|||||||
SecretAccessOutcome,
|
SecretAccessOutcome,
|
||||||
SecretBindingTargetType,
|
SecretBindingTargetType,
|
||||||
SecretManagedMode,
|
SecretManagedMode,
|
||||||
|
SecretScope,
|
||||||
SecretProviderDescriptor,
|
SecretProviderDescriptor,
|
||||||
SecretStatus,
|
SecretStatus,
|
||||||
SecretVersionStatus,
|
SecretVersionStatus,
|
||||||
|
|||||||
@@ -550,6 +550,7 @@ export interface Issue {
|
|||||||
executionLockedAt: Date | null;
|
executionLockedAt: Date | null;
|
||||||
createdByAgentId: string | null;
|
createdByAgentId: string | null;
|
||||||
createdByUserId: string | null;
|
createdByUserId: string | null;
|
||||||
|
responsibleUserId: string | null;
|
||||||
issueNumber: number | null;
|
issueNumber: number | null;
|
||||||
identifier: string | null;
|
identifier: string | null;
|
||||||
originKind?: IssueOriginKind;
|
originKind?: IssueOriginKind;
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ export interface Routine {
|
|||||||
latestRevisionNumber: number;
|
latestRevisionNumber: number;
|
||||||
createdByAgentId: string | null;
|
createdByAgentId: string | null;
|
||||||
createdByUserId: string | null;
|
createdByUserId: string | null;
|
||||||
|
responsibleUserId: string | null;
|
||||||
updatedByAgentId: string | null;
|
updatedByAgentId: string | null;
|
||||||
updatedByUserId: string | null;
|
updatedByUserId: string | null;
|
||||||
lastTriggeredAt: Date | null;
|
lastTriggeredAt: Date | null;
|
||||||
@@ -126,6 +127,7 @@ export interface RoutineRevisionSnapshotRoutineV1 {
|
|||||||
originId?: string | null;
|
originId?: string | null;
|
||||||
variables: RoutineVariable[];
|
variables: RoutineVariable[];
|
||||||
env: RoutineEnvConfig | null;
|
env: RoutineEnvConfig | null;
|
||||||
|
responsibleUserId: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RoutineRevisionSnapshotTriggerV1 {
|
export interface RoutineRevisionSnapshotTriggerV1 {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import type {
|
|||||||
SecretProvider,
|
SecretProvider,
|
||||||
SecretProviderConfigHealthStatus,
|
SecretProviderConfigHealthStatus,
|
||||||
SecretProviderConfigStatus,
|
SecretProviderConfigStatus,
|
||||||
|
SecretScope,
|
||||||
SecretStatus,
|
SecretStatus,
|
||||||
SecretVersionStatus,
|
SecretVersionStatus,
|
||||||
} from "../constants.js";
|
} from "../constants.js";
|
||||||
@@ -16,6 +17,7 @@ export type {
|
|||||||
SecretProvider,
|
SecretProvider,
|
||||||
SecretProviderConfigHealthStatus,
|
SecretProviderConfigHealthStatus,
|
||||||
SecretProviderConfigStatus,
|
SecretProviderConfigStatus,
|
||||||
|
SecretScope,
|
||||||
SecretStatus,
|
SecretStatus,
|
||||||
SecretVersionStatus,
|
SecretVersionStatus,
|
||||||
};
|
};
|
||||||
@@ -33,14 +35,25 @@ export interface EnvSecretRefBinding {
|
|||||||
version?: SecretVersionSelector;
|
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.
|
// 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 type AgentEnvConfig = Record<string, EnvBinding>;
|
||||||
|
|
||||||
export interface CompanySecret {
|
export interface CompanySecret {
|
||||||
id: string;
|
id: string;
|
||||||
companyId: string;
|
companyId: string;
|
||||||
|
scope: SecretScope;
|
||||||
|
ownerUserId: string | null;
|
||||||
|
userSecretDefinitionId: string | null;
|
||||||
key: string;
|
key: string;
|
||||||
name: string;
|
name: string;
|
||||||
provider: SecretProvider;
|
provider: SecretProvider;
|
||||||
@@ -61,6 +74,50 @@ export interface CompanySecret {
|
|||||||
updatedAt: Date;
|
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 {
|
export interface SecretProviderDescriptor {
|
||||||
id: SecretProvider;
|
id: SecretProvider;
|
||||||
label: string;
|
label: string;
|
||||||
@@ -216,9 +273,15 @@ export interface CompanySecretUsageBinding extends CompanySecretBinding {
|
|||||||
export interface SecretAccessEvent {
|
export interface SecretAccessEvent {
|
||||||
id: string;
|
id: string;
|
||||||
companyId: string;
|
companyId: string;
|
||||||
secretId: string;
|
secretId: string | null;
|
||||||
|
userSecretDefinitionId: string | null;
|
||||||
|
secretScope: SecretScope;
|
||||||
version: number | null;
|
version: number | null;
|
||||||
provider: SecretProvider;
|
provider: SecretProvider;
|
||||||
|
responsibleUserId: string | null;
|
||||||
|
credentialOwnerUserId: string | null;
|
||||||
|
credentialSubjectType: string | null;
|
||||||
|
credentialSubjectId: string | null;
|
||||||
actorType: "agent" | "user" | "system" | "plugin";
|
actorType: "agent" | "user" | "system" | "plugin";
|
||||||
actorId: string | null;
|
actorId: string | null;
|
||||||
consumerType: SecretBindingTargetType;
|
consumerType: SecretBindingTargetType;
|
||||||
|
|||||||
@@ -463,9 +463,15 @@ export {
|
|||||||
export {
|
export {
|
||||||
envBindingPlainSchema,
|
envBindingPlainSchema,
|
||||||
envBindingSecretRefSchema,
|
envBindingSecretRefSchema,
|
||||||
|
envBindingUserSecretRefSchema,
|
||||||
envBindingSchema,
|
envBindingSchema,
|
||||||
envConfigSchema,
|
envConfigSchema,
|
||||||
createSecretSchema,
|
createSecretSchema,
|
||||||
|
createUserSecretDefinitionSchema,
|
||||||
|
updateUserSecretDefinitionSchema,
|
||||||
|
createUserSecretValueSchema,
|
||||||
|
updateUserSecretValueSchema,
|
||||||
|
createUserSecretDeclarationSchema,
|
||||||
createSecretProviderConfigSchema,
|
createSecretProviderConfigSchema,
|
||||||
updateSecretProviderConfigSchema,
|
updateSecretProviderConfigSchema,
|
||||||
secretProviderConfigDiscoveryPreviewSchema,
|
secretProviderConfigDiscoveryPreviewSchema,
|
||||||
@@ -483,6 +489,11 @@ export {
|
|||||||
updateSecretSchema,
|
updateSecretSchema,
|
||||||
type CreateSecretBinding,
|
type CreateSecretBinding,
|
||||||
type CreateSecret,
|
type CreateSecret,
|
||||||
|
type CreateUserSecretDefinition,
|
||||||
|
type UpdateUserSecretDefinition,
|
||||||
|
type CreateUserSecretValue,
|
||||||
|
type UpdateUserSecretValue,
|
||||||
|
type CreateUserSecretDeclaration,
|
||||||
type CreateSecretProviderConfig,
|
type CreateSecretProviderConfig,
|
||||||
type UpdateSecretProviderConfig,
|
type UpdateSecretProviderConfig,
|
||||||
type SecretProviderConfigDiscoveryPreview,
|
type SecretProviderConfigDiscoveryPreview,
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ export const routineRevisionSnapshotRoutineV1Schema = z.object({
|
|||||||
catchUpPolicy: z.enum(ROUTINE_CATCH_UP_POLICIES),
|
catchUpPolicy: z.enum(ROUTINE_CATCH_UP_POLICIES),
|
||||||
variables: z.array(routineVariableSchema),
|
variables: z.array(routineVariableSchema),
|
||||||
env: envConfigSchema.nullable().default(null),
|
env: envConfigSchema.nullable().default(null),
|
||||||
|
responsibleUserId: z.string().nullable().default(null),
|
||||||
}).strict();
|
}).strict();
|
||||||
|
|
||||||
export const routineRevisionSnapshotTriggerV1Schema = z.object({
|
export const routineRevisionSnapshotTriggerV1Schema = z.object({
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import {
|
import {
|
||||||
|
createUserSecretDeclarationSchema,
|
||||||
|
createUserSecretValueSchema,
|
||||||
createSecretProviderConfigSchema,
|
createSecretProviderConfigSchema,
|
||||||
createSecretSchema,
|
createSecretSchema,
|
||||||
|
envBindingUserSecretRefSchema,
|
||||||
remoteSecretImportPreviewSchema,
|
remoteSecretImportPreviewSchema,
|
||||||
remoteSecretImportSchema,
|
remoteSecretImportSchema,
|
||||||
secretProviderConfigDiscoveryPreviewSchema,
|
secretProviderConfigDiscoveryPreviewSchema,
|
||||||
@@ -10,6 +13,42 @@ import {
|
|||||||
} from "./secret.js";
|
} from "./secret.js";
|
||||||
|
|
||||||
describe("secret validators", () => {
|
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", () => {
|
it("rejects externalRef on managed secrets", () => {
|
||||||
expect(() =>
|
expect(() =>
|
||||||
createSecretSchema.parse({
|
createSecretSchema.parse({
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ import {
|
|||||||
SECRET_STATUSES,
|
SECRET_STATUSES,
|
||||||
} from "../constants.js";
|
} 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({
|
export const envBindingPlainSchema = z.object({
|
||||||
type: z.literal("plain"),
|
type: z.literal("plain"),
|
||||||
value: z.string(),
|
value: z.string(),
|
||||||
@@ -15,7 +18,15 @@ export const envBindingPlainSchema = z.object({
|
|||||||
export const envBindingSecretRefSchema = z.object({
|
export const envBindingSecretRefSchema = z.object({
|
||||||
type: z.literal("secret_ref"),
|
type: z.literal("secret_ref"),
|
||||||
secretId: z.string().uuid(),
|
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.
|
// Backward-compatible union that accepts legacy inline values.
|
||||||
@@ -23,13 +34,14 @@ export const envBindingSchema = z.union([
|
|||||||
z.string(),
|
z.string(),
|
||||||
envBindingPlainSchema,
|
envBindingPlainSchema,
|
||||||
envBindingSecretRefSchema,
|
envBindingSecretRefSchema,
|
||||||
|
envBindingUserSecretRefSchema,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const envConfigSchema = z.record(z.string(), envBindingSchema);
|
export const envConfigSchema = z.record(z.string(), envBindingSchema);
|
||||||
|
|
||||||
export const createSecretSchema = z.object({
|
export const createSecretSchema = z.object({
|
||||||
name: z.string().min(1),
|
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(),
|
provider: z.enum(SECRET_PROVIDERS).optional(),
|
||||||
providerConfigId: z.string().uuid().optional().nullable(),
|
providerConfigId: z.string().uuid().optional().nullable(),
|
||||||
managedMode: z.enum(SECRET_MANAGED_MODES).optional(),
|
managedMode: z.enum(SECRET_MANAGED_MODES).optional(),
|
||||||
@@ -78,7 +90,7 @@ export type RotateSecret = z.infer<typeof rotateSecretSchema>;
|
|||||||
|
|
||||||
export const updateSecretSchema = z.object({
|
export const updateSecretSchema = z.object({
|
||||||
name: z.string().min(1).optional(),
|
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(),
|
status: z.enum(SECRET_STATUSES).optional(),
|
||||||
providerConfigId: z.string().uuid().optional().nullable(),
|
providerConfigId: z.string().uuid().optional().nullable(),
|
||||||
description: z.string().optional().nullable(),
|
description: z.string().optional().nullable(),
|
||||||
@@ -96,13 +108,86 @@ export const secretBindingTargetSchema = z.object({
|
|||||||
|
|
||||||
export const createSecretBindingSchema = secretBindingTargetSchema.extend({
|
export const createSecretBindingSchema = secretBindingTargetSchema.extend({
|
||||||
secretId: z.string().uuid(),
|
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),
|
required: z.boolean().default(true),
|
||||||
label: z.string().optional().nullable(),
|
label: z.string().optional().nullable(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type CreateSecretBinding = z.infer<typeof createSecretBindingSchema>;
|
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 safeShortText = z.string().trim().min(1).max(160);
|
||||||
const optionalSafeShortText = safeShortText.optional().nullable();
|
const optionalSafeShortText = safeShortText.optional().nullable();
|
||||||
|
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ function sanitizeValue(value: unknown): unknown {
|
|||||||
if (value === null || value === undefined) return value;
|
if (value === null || value === undefined) return value;
|
||||||
if (Array.isArray(value)) return value.map(sanitizeValue);
|
if (Array.isArray(value)) return value.map(sanitizeValue);
|
||||||
if (isSecretRefBinding(value)) return value;
|
if (isSecretRefBinding(value)) return value;
|
||||||
|
if (isUserSecretRefBinding(value)) return value;
|
||||||
if (isPlainBinding(value)) return { type: "plain", value: sanitizeValue(value.value) };
|
if (isPlainBinding(value)) return { type: "plain", value: sanitizeValue(value.value) };
|
||||||
if (!isPlainObject(value)) return value;
|
if (!isPlainObject(value)) return value;
|
||||||
return sanitizeRecord(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";
|
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 } {
|
function isPlainBinding(value: unknown): value is { type: "plain"; value: unknown } {
|
||||||
if (!isPlainObject(value)) return false;
|
if (!isPlainObject(value)) return false;
|
||||||
return value.type === "plain" && "value" in value;
|
return value.type === "plain" && "value" in value;
|
||||||
@@ -101,6 +107,10 @@ export function sanitizeRecord(record: Record<string, unknown>): Record<string,
|
|||||||
redacted[key] = sanitizeValue(value);
|
redacted[key] = sanitizeValue(value);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (isUserSecretRefBinding(value)) {
|
||||||
|
redacted[key] = sanitizeValue(value);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (isPlainBinding(value)) {
|
if (isPlainBinding(value)) {
|
||||||
redacted[key] = { type: "plain", value: REDACTED_EVENT_VALUE };
|
redacted[key] = { type: "plain", value: REDACTED_EVENT_VALUE };
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@@ -2091,6 +2091,7 @@ const issueListSelect = {
|
|||||||
executionLockedAt: issues.executionLockedAt,
|
executionLockedAt: issues.executionLockedAt,
|
||||||
createdByAgentId: issues.createdByAgentId,
|
createdByAgentId: issues.createdByAgentId,
|
||||||
createdByUserId: issues.createdByUserId,
|
createdByUserId: issues.createdByUserId,
|
||||||
|
responsibleUserId: issues.responsibleUserId,
|
||||||
issueNumber: issues.issueNumber,
|
issueNumber: issues.issueNumber,
|
||||||
identifier: issues.identifier,
|
identifier: issues.identifier,
|
||||||
originKind: issues.originKind,
|
originKind: issues.originKind,
|
||||||
|
|||||||
@@ -1177,6 +1177,7 @@ function routineRevisionSnapshotRoutine(routine: typeof routines.$inferSelect):
|
|||||||
originId: routine.originId,
|
originId: routine.originId,
|
||||||
variables: routine.variables ?? [],
|
variables: routine.variables ?? [],
|
||||||
env: routine.env ?? null,
|
env: routine.env ?? null,
|
||||||
|
responsibleUserId: routine.responsibleUserId ?? null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -452,6 +452,7 @@ function routineRevisionSnapshotRoutine(routine: RoutineRow): RoutineRevisionSna
|
|||||||
catchUpPolicy: routine.catchUpPolicy as RoutineRevisionSnapshotV1["routine"]["catchUpPolicy"],
|
catchUpPolicy: routine.catchUpPolicy as RoutineRevisionSnapshotV1["routine"]["catchUpPolicy"],
|
||||||
variables: routine.variables ?? [],
|
variables: routine.variables ?? [],
|
||||||
env: routine.env ?? null,
|
env: routine.env ?? null,
|
||||||
|
responsibleUserId: routine.responsibleUserId ?? null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -216,7 +216,14 @@ async function cleanupPreparedProviderWrite(input: {
|
|||||||
|
|
||||||
type CanonicalEnvBinding =
|
type CanonicalEnvBinding =
|
||||||
| { type: "plain"; value: string }
|
| { 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 = {
|
type SecretConsumerContext = {
|
||||||
consumerType: SecretBindingTargetType;
|
consumerType: SecretBindingTargetType;
|
||||||
@@ -303,6 +310,15 @@ function canonicalizeBinding(binding: EnvBinding): CanonicalEnvBinding {
|
|||||||
if (binding.type === "plain") {
|
if (binding.type === "plain") {
|
||||||
return { type: "plain", value: String(binding.value) };
|
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 {
|
return {
|
||||||
type: "secret_ref",
|
type: "secret_ref",
|
||||||
secretId: binding.secretId,
|
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 {
|
function defaultProviderConfigStatus(provider: SecretProvider): SecretProviderConfigStatus {
|
||||||
return COMING_SOON_SECRET_PROVIDERS.has(provider) ? "coming_soon" : "ready";
|
return COMING_SOON_SECRET_PROVIDERS.has(provider) ? "coming_soon" : "ready";
|
||||||
}
|
}
|
||||||
@@ -807,6 +827,10 @@ export function secretService(db: Db) {
|
|||||||
normalized[key] = binding;
|
normalized[key] = binding;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (binding.type === "user_secret_ref") {
|
||||||
|
normalized[key] = binding;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
await assertSecretInCompany(companyId, binding.secretId);
|
await assertSecretInCompany(companyId, binding.secretId);
|
||||||
normalized[key] = {
|
normalized[key] = {
|
||||||
@@ -887,6 +911,9 @@ export function secretService(db: Db) {
|
|||||||
version: binding.version,
|
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();
|
const value = binding.value.trim();
|
||||||
if (!value) return undefined;
|
if (!value) return undefined;
|
||||||
if (value === REDACTED_SENTINEL) {
|
if (value === REDACTED_SENTINEL) {
|
||||||
@@ -2620,7 +2647,7 @@ export function secretService(db: Db) {
|
|||||||
const binding = canonicalizeBinding(parsed.data as EnvBinding);
|
const binding = canonicalizeBinding(parsed.data as EnvBinding);
|
||||||
if (binding.type === "plain") {
|
if (binding.type === "plain") {
|
||||||
resolved[key] = binding.value;
|
resolved[key] = binding.value;
|
||||||
} else {
|
} else if (binding.type === "secret_ref") {
|
||||||
const secretResolution = await resolveSecretValueInternal(
|
const secretResolution = await resolveSecretValueInternal(
|
||||||
companyId,
|
companyId,
|
||||||
binding.secretId,
|
binding.secretId,
|
||||||
@@ -2635,6 +2662,8 @@ export function secretService(db: Db) {
|
|||||||
resolved[key] = secretResolution.value;
|
resolved[key] = secretResolution.value;
|
||||||
manifest.push(secretResolution.manifestEntry);
|
manifest.push(secretResolution.manifestEntry);
|
||||||
secretKeys.add(key);
|
secretKeys.add(key);
|
||||||
|
} else {
|
||||||
|
userSecretResolverUnavailable(binding.key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return { env: resolved, secretKeys, manifest };
|
return { env: resolved, secretKeys, manifest };
|
||||||
@@ -2768,7 +2797,7 @@ export function secretService(db: Db) {
|
|||||||
const binding = canonicalizeBinding(parsed.data as EnvBinding);
|
const binding = canonicalizeBinding(parsed.data as EnvBinding);
|
||||||
if (binding.type === "plain") {
|
if (binding.type === "plain") {
|
||||||
env[key] = binding.value;
|
env[key] = binding.value;
|
||||||
} else {
|
} else if (binding.type === "secret_ref") {
|
||||||
const secretResolution = await resolveSecretValueInternal(
|
const secretResolution = await resolveSecretValueInternal(
|
||||||
companyId,
|
companyId,
|
||||||
binding.secretId,
|
binding.secretId,
|
||||||
@@ -2783,6 +2812,8 @@ export function secretService(db: Db) {
|
|||||||
env[key] = secretResolution.value;
|
env[key] = secretResolution.value;
|
||||||
manifest.push(secretResolution.manifestEntry);
|
manifest.push(secretResolution.manifestEntry);
|
||||||
secretKeys.add(key);
|
secretKeys.add(key);
|
||||||
|
} else {
|
||||||
|
userSecretResolverUnavailable(binding.key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
resolved.env = env;
|
resolved.env = env;
|
||||||
@@ -2794,6 +2825,7 @@ export function secretService(db: Db) {
|
|||||||
if (!parsed.success) continue;
|
if (!parsed.success) continue;
|
||||||
const binding = canonicalizeBinding(parsed.data as EnvBinding);
|
const binding = canonicalizeBinding(parsed.data as EnvBinding);
|
||||||
if (binding.type === "plain") continue;
|
if (binding.type === "plain") continue;
|
||||||
|
if (binding.type === "user_secret_ref") userSecretResolverUnavailable(binding.key);
|
||||||
const secretResolution = await resolveSecretValueInternal(
|
const secretResolution = await resolveSecretValueInternal(
|
||||||
companyId,
|
companyId,
|
||||||
binding.secretId,
|
binding.secretId,
|
||||||
|
|||||||
Reference in new issue
Block a user