Add secrets infrastructure: DB tables, shared types, env binding model, and migration improvements
Introduce company_secrets and company_secret_versions tables for encrypted secret storage. Add EnvBinding discriminated union (plain vs secret_ref) to replace raw string env values in adapter configs. Add hiddenAt column to issues for soft-hiding. Improve migration system with journal-ordered application and manual fallback when Drizzle migrator can't reconcile history. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,7 @@ export const API = {
|
||||
issues: `${API_PREFIX}/issues`,
|
||||
goals: `${API_PREFIX}/goals`,
|
||||
approvals: `${API_PREFIX}/approvals`,
|
||||
secrets: `${API_PREFIX}/secrets`,
|
||||
costs: `${API_PREFIX}/costs`,
|
||||
activity: `${API_PREFIX}/activity`,
|
||||
dashboard: `${API_PREFIX}/dashboard`,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { z } from "zod";
|
||||
import { SECRET_PROVIDERS } from "./constants.js";
|
||||
|
||||
export const configMetaSchema = z.object({
|
||||
version: z.literal(1),
|
||||
@@ -28,12 +29,31 @@ export const serverConfigSchema = z.object({
|
||||
serveUi: z.boolean().default(true),
|
||||
});
|
||||
|
||||
export const secretsLocalEncryptedConfigSchema = z.object({
|
||||
keyFilePath: z.string().default("./data/secrets/master.key"),
|
||||
});
|
||||
|
||||
export const secretsConfigSchema = z.object({
|
||||
provider: z.enum(SECRET_PROVIDERS).default("local_encrypted"),
|
||||
strictMode: z.boolean().default(false),
|
||||
localEncrypted: secretsLocalEncryptedConfigSchema.default({
|
||||
keyFilePath: "./data/secrets/master.key",
|
||||
}),
|
||||
});
|
||||
|
||||
export const paperclipConfigSchema = z.object({
|
||||
$meta: configMetaSchema,
|
||||
llm: llmConfigSchema.optional(),
|
||||
database: databaseConfigSchema,
|
||||
logging: loggingConfigSchema,
|
||||
server: serverConfigSchema,
|
||||
secrets: secretsConfigSchema.default({
|
||||
provider: "local_encrypted",
|
||||
strictMode: false,
|
||||
localEncrypted: {
|
||||
keyFilePath: "./data/secrets/master.key",
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
export type PaperclipConfig = z.infer<typeof paperclipConfigSchema>;
|
||||
@@ -41,4 +61,6 @@ export type LlmConfig = z.infer<typeof llmConfigSchema>;
|
||||
export type DatabaseConfig = z.infer<typeof databaseConfigSchema>;
|
||||
export type LoggingConfig = z.infer<typeof loggingConfigSchema>;
|
||||
export type ServerConfig = z.infer<typeof serverConfigSchema>;
|
||||
export type SecretsConfig = z.infer<typeof secretsConfigSchema>;
|
||||
export type SecretsLocalEncryptedConfig = z.infer<typeof secretsLocalEncryptedConfigSchema>;
|
||||
export type ConfigMeta = z.infer<typeof configMetaSchema>;
|
||||
|
||||
@@ -71,6 +71,14 @@ export const APPROVAL_STATUSES = [
|
||||
] as const;
|
||||
export type ApprovalStatus = (typeof APPROVAL_STATUSES)[number];
|
||||
|
||||
export const SECRET_PROVIDERS = [
|
||||
"local_encrypted",
|
||||
"aws_secrets_manager",
|
||||
"gcp_secret_manager",
|
||||
"vault",
|
||||
] as const;
|
||||
export type SecretProvider = (typeof SECRET_PROVIDERS)[number];
|
||||
|
||||
export const HEARTBEAT_INVOCATION_SOURCES = [
|
||||
"timer",
|
||||
"assignment",
|
||||
|
||||
@@ -10,6 +10,7 @@ export {
|
||||
PROJECT_STATUSES,
|
||||
APPROVAL_TYPES,
|
||||
APPROVAL_STATUSES,
|
||||
SECRET_PROVIDERS,
|
||||
HEARTBEAT_INVOCATION_SOURCES,
|
||||
HEARTBEAT_RUN_STATUSES,
|
||||
WAKEUP_TRIGGER_DETAILS,
|
||||
@@ -26,6 +27,7 @@ export {
|
||||
type ProjectStatus,
|
||||
type ApprovalType,
|
||||
type ApprovalStatus,
|
||||
type SecretProvider,
|
||||
type HeartbeatInvocationSource,
|
||||
type HeartbeatRunStatus,
|
||||
type WakeupTriggerDetail,
|
||||
@@ -57,6 +59,10 @@ export type {
|
||||
DashboardSummary,
|
||||
ActivityEvent,
|
||||
SidebarBadges,
|
||||
EnvBinding,
|
||||
AgentEnvConfig,
|
||||
CompanySecret,
|
||||
SecretProviderDescriptor,
|
||||
} from "./types/index.js";
|
||||
|
||||
export {
|
||||
@@ -107,6 +113,16 @@ export {
|
||||
type RequestApprovalRevision,
|
||||
type ResubmitApproval,
|
||||
type AddApprovalComment,
|
||||
envBindingPlainSchema,
|
||||
envBindingSecretRefSchema,
|
||||
envBindingSchema,
|
||||
envConfigSchema,
|
||||
createSecretSchema,
|
||||
rotateSecretSchema,
|
||||
updateSecretSchema,
|
||||
type CreateSecret,
|
||||
type RotateSecret,
|
||||
type UpdateSecret,
|
||||
createCostEventSchema,
|
||||
updateBudgetSchema,
|
||||
type CreateCostEvent,
|
||||
@@ -122,10 +138,14 @@ export {
|
||||
databaseConfigSchema,
|
||||
loggingConfigSchema,
|
||||
serverConfigSchema,
|
||||
secretsConfigSchema,
|
||||
secretsLocalEncryptedConfigSchema,
|
||||
type PaperclipConfig,
|
||||
type LlmConfig,
|
||||
type DatabaseConfig,
|
||||
type LoggingConfig,
|
||||
type ServerConfig,
|
||||
type SecretsConfig,
|
||||
type SecretsLocalEncryptedConfig,
|
||||
type ConfigMeta,
|
||||
} from "./config-schema.js";
|
||||
|
||||
@@ -4,6 +4,16 @@ export type { Project } from "./project.js";
|
||||
export type { Issue, IssueComment, IssueAncestor } from "./issue.js";
|
||||
export type { Goal } from "./goal.js";
|
||||
export type { Approval, ApprovalComment } from "./approval.js";
|
||||
export type {
|
||||
SecretProvider,
|
||||
SecretVersionSelector,
|
||||
EnvPlainBinding,
|
||||
EnvSecretRefBinding,
|
||||
EnvBinding,
|
||||
AgentEnvConfig,
|
||||
CompanySecret,
|
||||
SecretProviderDescriptor,
|
||||
} from "./secrets.js";
|
||||
export type { CostEvent, CostSummary, CostByAgent } from "./cost.js";
|
||||
export type {
|
||||
HeartbeatRun,
|
||||
|
||||
@@ -32,6 +32,7 @@ export interface Issue {
|
||||
startedAt: Date | null;
|
||||
completedAt: Date | null;
|
||||
cancelledAt: Date | null;
|
||||
hiddenAt: Date | null;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
43
packages/shared/src/types/secrets.ts
Normal file
43
packages/shared/src/types/secrets.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
export type SecretProvider =
|
||||
| "local_encrypted"
|
||||
| "aws_secrets_manager"
|
||||
| "gcp_secret_manager"
|
||||
| "vault";
|
||||
|
||||
export type SecretVersionSelector = number | "latest";
|
||||
|
||||
export interface EnvPlainBinding {
|
||||
type: "plain";
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface EnvSecretRefBinding {
|
||||
type: "secret_ref";
|
||||
secretId: string;
|
||||
version?: SecretVersionSelector;
|
||||
}
|
||||
|
||||
// Backward-compatible: legacy plaintext string values are still accepted.
|
||||
export type EnvBinding = string | EnvPlainBinding | EnvSecretRefBinding;
|
||||
|
||||
export type AgentEnvConfig = Record<string, EnvBinding>;
|
||||
|
||||
export interface CompanySecret {
|
||||
id: string;
|
||||
companyId: string;
|
||||
name: string;
|
||||
provider: SecretProvider;
|
||||
externalRef: string | null;
|
||||
latestVersion: number;
|
||||
description: string | null;
|
||||
createdByAgentId: string | null;
|
||||
createdByUserId: string | null;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export interface SecretProviderDescriptor {
|
||||
id: SecretProvider;
|
||||
label: string;
|
||||
requiresExternalRef: boolean;
|
||||
}
|
||||
@@ -4,11 +4,25 @@ import {
|
||||
AGENT_ROLES,
|
||||
AGENT_STATUSES,
|
||||
} from "../constants.js";
|
||||
import { envConfigSchema } from "./secret.js";
|
||||
|
||||
export const agentPermissionsSchema = z.object({
|
||||
canCreateAgents: z.boolean().optional().default(false),
|
||||
});
|
||||
|
||||
const adapterConfigSchema = z.record(z.unknown()).superRefine((value, ctx) => {
|
||||
const envValue = value.env;
|
||||
if (envValue === undefined) return;
|
||||
const parsed = envConfigSchema.safeParse(envValue);
|
||||
if (!parsed.success) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "adapterConfig.env must be a map of valid env bindings",
|
||||
path: ["env"],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export const createAgentSchema = z.object({
|
||||
name: z.string().min(1),
|
||||
role: z.enum(AGENT_ROLES).optional().default("general"),
|
||||
@@ -16,7 +30,7 @@ export const createAgentSchema = z.object({
|
||||
reportsTo: z.string().uuid().optional().nullable(),
|
||||
capabilities: z.string().optional().nullable(),
|
||||
adapterType: z.enum(AGENT_ADAPTER_TYPES).optional().default("process"),
|
||||
adapterConfig: z.record(z.unknown()).optional().default({}),
|
||||
adapterConfig: adapterConfigSchema.optional().default({}),
|
||||
runtimeConfig: z.record(z.unknown()).optional().default({}),
|
||||
budgetMonthlyCents: z.number().int().nonnegative().optional().default(0),
|
||||
permissions: agentPermissionsSchema.optional(),
|
||||
|
||||
@@ -63,6 +63,19 @@ export {
|
||||
type AddApprovalComment,
|
||||
} from "./approval.js";
|
||||
|
||||
export {
|
||||
envBindingPlainSchema,
|
||||
envBindingSecretRefSchema,
|
||||
envBindingSchema,
|
||||
envConfigSchema,
|
||||
createSecretSchema,
|
||||
rotateSecretSchema,
|
||||
updateSecretSchema,
|
||||
type CreateSecret,
|
||||
type RotateSecret,
|
||||
type UpdateSecret,
|
||||
} from "./secret.js";
|
||||
|
||||
export {
|
||||
createCostEventSchema,
|
||||
updateBudgetSchema,
|
||||
|
||||
@@ -18,6 +18,7 @@ export type CreateIssue = z.infer<typeof createIssueSchema>;
|
||||
|
||||
export const updateIssueSchema = createIssueSchema.partial().extend({
|
||||
comment: z.string().min(1).optional(),
|
||||
hiddenAt: z.string().datetime().nullable().optional(),
|
||||
});
|
||||
|
||||
export type UpdateIssue = z.infer<typeof updateIssueSchema>;
|
||||
|
||||
47
packages/shared/src/validators/secret.ts
Normal file
47
packages/shared/src/validators/secret.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { z } from "zod";
|
||||
import { SECRET_PROVIDERS } from "../constants.js";
|
||||
|
||||
export const envBindingPlainSchema = z.object({
|
||||
type: z.literal("plain"),
|
||||
value: z.string(),
|
||||
});
|
||||
|
||||
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(),
|
||||
});
|
||||
|
||||
// Backward-compatible union that accepts legacy inline values.
|
||||
export const envBindingSchema = z.union([
|
||||
z.string(),
|
||||
envBindingPlainSchema,
|
||||
envBindingSecretRefSchema,
|
||||
]);
|
||||
|
||||
export const envConfigSchema = z.record(envBindingSchema);
|
||||
|
||||
export const createSecretSchema = z.object({
|
||||
name: z.string().min(1),
|
||||
provider: z.enum(SECRET_PROVIDERS).optional(),
|
||||
value: z.string().min(1),
|
||||
description: z.string().optional().nullable(),
|
||||
externalRef: z.string().optional().nullable(),
|
||||
});
|
||||
|
||||
export type CreateSecret = z.infer<typeof createSecretSchema>;
|
||||
|
||||
export const rotateSecretSchema = z.object({
|
||||
value: z.string().min(1),
|
||||
externalRef: z.string().optional().nullable(),
|
||||
});
|
||||
|
||||
export type RotateSecret = z.infer<typeof rotateSecretSchema>;
|
||||
|
||||
export const updateSecretSchema = z.object({
|
||||
name: z.string().min(1).optional(),
|
||||
description: z.string().optional().nullable(),
|
||||
externalRef: z.string().optional().nullable(),
|
||||
});
|
||||
|
||||
export type UpdateSecret = z.infer<typeof updateSecretSchema>;
|
||||
Reference in New Issue
Block a user