import { Router, type Response } from "express"; import type { Db } from "@paperclipai/db"; import { createSecretProviderConfigSchema, createSecretSchema, createUserSecretDefinitionSchema, createUserSecretValueSchema, remoteSecretImportPreviewSchema, remoteSecretImportSchema, rotateSecretSchema, rotateUserSecretValueSchema, secretProviderConfigDiscoveryPreviewSchema, updateSecretProviderConfigSchema, updateSecretSchema, updateUserSecretDefinitionSchema, updateUserSecretValueSchema, } from "@paperclipai/shared"; import { validate } from "../middleware/validate.js"; import { assertBoard, assertBoardOrAgent, assertCompanyAccess, getAccessibleResource } from "./authz.js"; import { logActivity, secretService } from "../services/index.js"; import { createSecretProposalsService } from "../services/secret-proposals.js"; import { getConfiguredSecretProvider } from "../secrets/configured-provider.js"; import { forbidden, notFound, unauthorized, unprocessable } from "../errors.js"; import { authorizationDeniedDetails } from "../services/authorization.js"; import { accessService } from "../services/access.js"; import { heartbeatService } from "../services/heartbeat.js"; import { issueService } from "../services/issues.js"; import type { IssueAssignmentWakeupDeps } from "../services/issue-assignment-wakeup.js"; import { createRunSecretRedactionRegistry } from "../services/run-secret-redaction.js"; import { notifySecretProposalResolution } from "../services/secret-proposal-notifications.js"; import { assertCanResolveProposal } from "../services/secret-proposal-authorization.js"; type SecretRoutesDeps = { heartbeat?: IssueAssignmentWakeupDeps; issues?: Pick, "getById" | "addComment">; }; const DEFAULT_PROPOSAL_LIST_LIMIT = 100; const MAX_PROPOSAL_LIST_LIMIT = 200; function proposalListPage(query: Record) { const rawLimit = query.limit; const rawOffset = query.offset; if (rawLimit !== undefined && (typeof rawLimit !== "string" || !/^\d+$/.test(rawLimit))) { throw unprocessable("limit must be a positive integer"); } if (rawOffset !== undefined && (typeof rawOffset !== "string" || !/^\d+$/.test(rawOffset))) { throw unprocessable("offset must be a non-negative integer"); } const requestedLimit = rawLimit === undefined ? DEFAULT_PROPOSAL_LIST_LIMIT : Number.parseInt(rawLimit, 10); if (requestedLimit < 1) throw unprocessable("limit must be a positive integer"); return { limit: Math.min(requestedLimit, MAX_PROPOSAL_LIST_LIMIT), offset: rawOffset === undefined ? 0 : Number.parseInt(rawOffset, 10), }; } function setProposalPaginationHeaders( res: Response, page: { limit: number; offset: number }, hasMore: boolean, ) { res.setHeader("X-Page-Limit", String(page.limit)); res.setHeader("X-Page-Offset", String(page.offset)); if (hasMore) res.setHeader("X-Next-Offset", String(page.offset + page.limit)); } function hasSecretDefinitionAdminAccess(req: Parameters[0], companyId: string) { assertBoard(req); assertCompanyAccess(req, companyId); if (req.actor.source === "local_implicit" || req.actor.isInstanceAdmin) return true; const membership = req.actor.memberships?.find((item) => item.companyId === companyId); return membership?.status === "active" && ["owner", "admin"].includes(String(membership.membershipRole)); } function assertSecretDefinitionAdmin(req: Parameters[0], companyId: string) { if (hasSecretDefinitionAdminAccess(req, companyId)) return; throw forbidden("Company admin access required"); } function assertCompanySecretWrite(req: Parameters[0], companyId: string) { assertBoard(req); assertCompanyAccess(req, companyId); } function currentUserId(req: Parameters[0]) { assertBoard(req); if (req.actor.userId) return req.actor.userId; throw unauthorized("User identity required for user-specific secrets"); } function boardActorUser(req: Parameters[0]) { assertBoard(req); return { userId: req.actor.userId ?? null, agentId: null }; } function userSecretDefinitionActivityActor(req: Parameters[0]) { assertBoard(req); if (req.actor.userId) { return { actorType: "user" as const, actorId: req.actor.userId }; } return { actorType: "system" as const, actorId: req.actor.source ?? "board" }; } function isCompanyScopedSecret(secret: { scope?: string | null }) { return (secret.scope ?? "company") === "company"; } export function secretRoutes(db: Db, deps: SecretRoutesDeps = {}) { const router = Router(); const svc = secretService(db); const proposals = createSecretProposalsService(db); const access = accessService(db); const issues = deps.issues ?? issueService(db); const heartbeat = deps.heartbeat ?? heartbeatService(db); const runRedactions = createRunSecretRedactionRegistry(db); const defaultProvider = getConfiguredSecretProvider(); async function assertSecretCatalogReadAllowed(req: Parameters[0], companyId: string) { const decision = await access.decide({ actor: req.actor, action: "secrets:read", resource: { type: "company", companyId }, }); if (decision.allowed) return; throw forbidden(decision.explanation, authorizationDeniedDetails(decision)); } function agentSecretContext(req: Parameters[0]) { if (req.actor.type !== "agent" || !req.actor.agentId || !req.actor.companyId || !req.actor.runId) { throw forbidden("Run-bound agent authentication required"); } return { companyId: req.actor.companyId, agentId: req.actor.agentId, actorSource: req.actor.source === "agent_jwt" ? "agent_jwt" as const : "agent_key" as const, keyId: req.actor.keyId ?? null, keyScope: req.actor.keyScope ?? null, heartbeatRunId: req.actor.runId, responsibleUserId: req.actor.onBehalfOfUserId ?? null, }; } async function proposalAgentContext(req: Parameters[0]) { const context = agentSecretContext(req); if (req.actor.source !== "agent_jwt" || req.actor.keyScope?.kind === "task_bridge" || req.actor.keyScope?.kind === "skill_test") { throw forbidden("Secret proposals require a verified run-bound agent token"); } const decision = await access.decide({ actor: req.actor, action: "secrets:propose", resource: { type: "company", companyId: context.companyId }, }); if (!decision.allowed) { throw forbidden(decision.explanation, authorizationDeniedDetails(decision)); } return context; } function agentProposalView(row: Awaited>[number]) { const { valueFingerprintSha256: _fingerprint, valueLength: _length, ...safe } = row; return safe; } async function bindingApprovalDecision(req: Parameters[0], proposal: { kind: string; targetId: string | null; }) { if (proposal.kind !== "binding") return null; if (!proposal.targetId) throw unprocessable("Binding proposal target is missing"); return access.decide({ actor: req.actor, action: "agent_config:update", resource: { type: "agent", companyId: req.params.companyId as string, agentId: proposal.targetId }, scope: { requiresChangeGrant: true }, }); } async function boardProposalView(req: Parameters[0], proposal: Awaited>[number]) { if (proposal.status !== "pending") { return { ...proposal, viewerCanApprove: false, approveBlockReason: "Proposal is no longer pending" }; } if (proposal.kind === "secret" && !hasSecretDefinitionAdminAccess(req, req.params.companyId as string)) { return { ...proposal, viewerCanApprove: false, approveBlockReason: "Company admin access required" }; } const decision = await bindingApprovalDecision(req, proposal); return { ...proposal, viewerCanApprove: decision?.allowed ?? true, approveBlockReason: decision && !decision.allowed ? decision.explanation : null, }; } router.post("/agents/me/secret-proposals", async (req, res) => { const context = await proposalAgentContext(req); const body = req.body ?? {}; const proposal = body.kind === "secret" ? await proposals.createSecret({ companyId: context.companyId, heartbeatRunId: context.heartbeatRunId, registerForRedaction: (value) => runRedactions.register(context.companyId, context.heartbeatRunId, value), }, { name: body.name, key: body.key, description: body.description, value: body.value, justification: body.justification, }) : body.kind === "binding" ? await proposals.createBinding({ companyId: context.companyId, heartbeatRunId: context.heartbeatRunId }, { secretId: body.secretId, sourceConfigPath: body.sourceConfigPath, secretProposalId: body.secretProposalId, targetAgentId: body.targetAgentId, configPath: body.configPath, justification: body.justification, bindingTargetPolicy: "self_and_reports", }) : (() => { throw unprocessable("kind must be secret or binding"); })(); res.status(201).json(agentProposalView(await proposals.view(proposal))); }); router.get("/agents/me/secret-proposals", async (req, res) => { const context = await proposalAgentContext(req); const page = proposalListPage(req.query); const rows = await proposals.listForAgent(context.companyId, context.agentId, { limit: page.limit + 1, offset: page.offset, }); const hasMore = rows.length > page.limit; setProposalPaginationHeaders(res, page, hasMore); res.json({ proposals: rows.slice(0, page.limit).map(agentProposalView), nextOffset: hasMore ? page.offset + page.limit : null, }); }); router.delete("/agents/me/secret-proposals/:id", async (req, res) => { const context = await proposalAgentContext(req); const proposal = await proposals.transition(context.companyId, req.params.id as string, "withdrawn", { proposerAgentId: context.agentId }); res.json(agentProposalView(await proposals.view(proposal))); }); router.get("/companies/:companyId/secret-proposals", async (req, res) => { assertBoard(req); const companyId = req.params.companyId as string; assertCompanyAccess(req, companyId); const status = typeof req.query.status === "string" ? req.query.status : null; const page = proposalListPage(req.query); const rows = await proposals.listForBoard(companyId, status, { limit: page.limit + 1, offset: page.offset, }); const hasMore = rows.length > page.limit; setProposalPaginationHeaders(res, page, hasMore); res.json(await Promise.all(rows.slice(0, page.limit).map((proposal) => boardProposalView(req, proposal)))); }); router.post("/companies/:companyId/secret-proposals/:id/approve", async (req, res) => { const companyId = req.params.companyId as string; assertCompanySecretWrite(req, companyId); const proposal = await proposals.getById(companyId, req.params.id as string); if (!proposal) throw notFound("Secret proposal not found"); await assertCanResolveProposal({ db, actor: req.actor, companyId, proposal, assertSecretDefinitionAdmin: () => assertSecretDefinitionAdmin(req, companyId), }); const resolvedByUserId = req.actor.userId ?? "board"; const approved = await proposals.approve(companyId, proposal.id, { resolvedByUserId, cascade: req.body?.cascade === true, overrides: req.body?.overrides, assertCanResolve: (lockedProposal, txDb) => assertCanResolveProposal({ db: txDb, actor: req.actor, companyId, proposal: lockedProposal, assertSecretDefinitionAdmin: () => assertSecretDefinitionAdmin(req, companyId), }), }); await notifySecretProposalResolution({ proposal, status: "approved", userId: resolvedByUserId, issues, heartbeat }); res.json(await boardProposalView(req, await proposals.view(approved))); }); router.post("/companies/:companyId/secret-proposals/:id/reject", async (req, res) => { assertBoard(req); const companyId = req.params.companyId as string; assertCompanyAccess(req, companyId); const reason = typeof req.body?.reason === "string" ? req.body.reason.trim() : ""; if (!reason) throw unprocessable("Rejection reason is required"); const existing = await proposals.getById(companyId, req.params.id as string); if (!existing) throw notFound("Secret proposal not found"); await assertCanResolveProposal({ db, actor: req.actor, companyId, proposal: existing, assertSecretDefinitionAdmin: () => assertSecretDefinitionAdmin(req, companyId), }); const resolvedByUserId = req.actor.userId ?? "board"; const proposal = await proposals.transition(companyId, req.params.id as string, "rejected", { resolvedByUserId, reason, }); await notifySecretProposalResolution({ proposal: existing, status: "rejected", userId: resolvedByUserId, reason, issues, heartbeat, }); res.json(await boardProposalView(req, await proposals.view(proposal))); }); router.get("/agents/me/secrets", async (req, res) => { const context = agentSecretContext(req); const secrets = await svc.listAgentSecretAccess(context.companyId, context); await logActivity(db, { companyId: context.companyId, actorType: "agent", actorId: context.agentId, action: "secret.access.listed", entityType: "agent", entityId: context.agentId, agentId: context.agentId, runId: context.heartbeatRunId, details: { count: secrets.length }, }); res.json({ secrets: secrets.map(({ secretId, bindingId: _bindingId, configPath: _configPath, ...secret }) => ({ ...secret, secretRef: secretId, })), }); }); router.post("/agents/me/secrets/:key/value", async (req, res) => { const context = agentSecretContext(req); const available = await svc.listAgentSecretAccess(context.companyId, context); const secret = available.find((entry) => entry.key === req.params.key); if (!secret) throw forbidden("Secret access is not granted for this agent"); const resolution = await svc.resolveSecretValueForAgentAccess( context.companyId, secret.secretId, secret.versionSelector, { ...context, configPath: secret.configPath, bindingId: secret.bindingId, issueId: null, registerForRedaction: (value) => runRedactions.register(context.companyId, context.heartbeatRunId, value), }, ); res.set("Cache-Control", "no-store"); res.json({ key: secret.key, value: resolution.value, version: resolution.version, }); }); router.get("/companies/:companyId/secret-providers", (req, res) => { assertBoard(req); const companyId = req.params.companyId as string; assertCompanyAccess(req, companyId); res.json(svc.listProviders()); }); router.get("/companies/:companyId/secret-providers/health", async (req, res) => { assertBoard(req); const companyId = req.params.companyId as string; assertCompanyAccess(req, companyId); const checks = await svc.checkProviders(); res.json({ providers: checks }); }); router.get("/companies/:companyId/secret-provider-configs", async (req, res) => { assertBoard(req); const companyId = req.params.companyId as string; assertCompanyAccess(req, companyId); res.json(await svc.listProviderConfigs(companyId)); }); router.post( "/companies/:companyId/secret-provider-configs/discovery/preview", validate(secretProviderConfigDiscoveryPreviewSchema), async (req, res) => { assertBoard(req); const companyId = req.params.companyId as string; assertCompanyAccess(req, companyId); const preview = await svc.previewProviderConfigDiscovery(companyId, { provider: req.body.provider, config: req.body.config, query: req.body.query, nextToken: req.body.nextToken, pageSize: req.body.pageSize, }); await logActivity(db, { companyId, actorType: "user", actorId: req.actor.userId ?? "board", action: "secret_provider_config.discovery_previewed", entityType: "secret_provider_config_discovery", entityId: companyId, details: { provider: preview.provider, candidateCount: preview.candidates.length, sampledSecretCount: preview.sampledSecretCount, warningCount: preview.warnings.length, }, }); res.json(preview); }, ); router.post("/companies/:companyId/secret-provider-configs", validate(createSecretProviderConfigSchema), async (req, res) => { assertBoard(req); const companyId = req.params.companyId as string; assertCompanyAccess(req, companyId); const created = await svc.createProviderConfig( companyId, { provider: req.body.provider, displayName: req.body.displayName, status: req.body.status, isDefault: req.body.isDefault, config: req.body.config, }, { userId: req.actor.userId ?? "board", agentId: null }, ); await logActivity(db, { companyId, actorType: "user", actorId: req.actor.userId ?? "board", action: "secret_provider_config.created", entityType: "secret_provider_config", entityId: created.id, details: { provider: created.provider, displayName: created.displayName, status: created.status, isDefault: created.isDefault, }, }); res.status(201).json(created); }); router.get("/secret-provider-configs/:id", async (req, res) => { assertBoard(req); const existing = await getAccessibleResource(req, res, svc.getProviderConfigById(req.params.id as string), "Provider vault not found"); if (!existing) return; res.json(existing); }); router.patch("/secret-provider-configs/:id", validate(updateSecretProviderConfigSchema), async (req, res) => { assertBoard(req); const id = req.params.id as string; const existing = await getAccessibleResource(req, res, svc.getProviderConfigById(id), "Provider vault not found"); if (!existing) return; const updated = await svc.updateProviderConfig(id, { displayName: req.body.displayName, status: req.body.status, isDefault: req.body.isDefault, config: req.body.config, }); if (!updated) { res.status(404).json({ error: "Provider vault not found" }); return; } await logActivity(db, { companyId: updated.companyId, actorType: "user", actorId: req.actor.userId ?? "board", action: "secret_provider_config.updated", entityType: "secret_provider_config", entityId: updated.id, details: { provider: updated.provider, displayName: updated.displayName, status: updated.status, isDefault: updated.isDefault, }, }); res.json(updated); }); router.delete("/secret-provider-configs/:id", async (req, res) => { assertBoard(req); const id = req.params.id as string; const existing = await getAccessibleResource(req, res, svc.getProviderConfigById(id), "Provider vault not found"); if (!existing) return; const removed = await svc.removeProviderConfig(id); if (!removed) { res.status(404).json({ error: "Provider vault not found" }); return; } await logActivity(db, { companyId: removed.companyId, actorType: "user", actorId: req.actor.userId ?? "board", action: "secret_provider_config.removed", entityType: "secret_provider_config", entityId: removed.id, details: { provider: removed.provider, displayName: removed.displayName, remoteDeleted: false, }, }); res.json(removed); }); router.post("/secret-provider-configs/:id/default", async (req, res) => { assertBoard(req); const id = req.params.id as string; const existing = await getAccessibleResource(req, res, svc.getProviderConfigById(id), "Provider vault not found"); if (!existing) return; const updated = await svc.setDefaultProviderConfig(id); if (!updated) { res.status(404).json({ error: "Provider vault not found" }); return; } await logActivity(db, { companyId: updated.companyId, actorType: "user", actorId: req.actor.userId ?? "board", action: "secret_provider_config.default_set", entityType: "secret_provider_config", entityId: updated.id, details: { provider: updated.provider, displayName: updated.displayName, isDefault: updated.isDefault, }, }); res.json(updated); }); router.post("/secret-provider-configs/:id/health", async (req, res) => { assertBoard(req); const id = req.params.id as string; const existing = await getAccessibleResource(req, res, svc.getProviderConfigById(id), "Provider vault not found"); if (!existing) return; const health = await svc.checkProviderConfigHealth(id); if (!health) { res.status(404).json({ error: "Provider vault not found" }); return; } await logActivity(db, { companyId: existing.companyId, actorType: "user", actorId: req.actor.userId ?? "board", action: "secret_provider_config.health_checked", entityType: "secret_provider_config", entityId: existing.id, details: { provider: existing.provider, status: health.status, code: health.details.code, }, }); res.json(health); }); // Agent-readable catalog: returns only non-sensitive metadata (id, name, key, status). // Agents need this to resolve a secret name to its UUID when wiring env bindings, // without requiring board access or exposing any secret values. router.get("/companies/:companyId/secrets/catalog", async (req, res) => { assertBoardOrAgent(req); const companyId = req.params.companyId as string; assertCompanyAccess(req, companyId); await assertSecretCatalogReadAllowed(req, companyId); const secrets = await svc.list(companyId); res.json(secrets.map(({ id, name, key, status }) => ({ id, name, key, status }))); }); router.get("/companies/:companyId/secrets", async (req, res) => { assertBoard(req); const companyId = req.params.companyId as string; assertCompanyAccess(req, companyId); const secrets = await svc.list(companyId); res.json(secrets); }); router.get("/companies/:companyId/user-secret-definitions", async (req, res) => { const companyId = req.params.companyId as string; assertSecretDefinitionAdmin(req, companyId); res.json(await svc.listUserSecretDefinitions(companyId)); }); router.post( "/companies/:companyId/user-secret-definitions", validate(createUserSecretDefinitionSchema), async (req, res) => { const companyId = req.params.companyId as string; assertSecretDefinitionAdmin(req, companyId); const created = await svc.createUserSecretDefinition( companyId, { key: req.body.key, name: req.body.name, description: req.body.description, status: req.body.status, provider: req.body.provider ?? defaultProvider, providerConfigId: req.body.providerConfigId, managedMode: req.body.managedMode, providerMetadata: req.body.providerMetadata, usageGuidance: req.body.usageGuidance, }, boardActorUser(req), ); const activityActor = userSecretDefinitionActivityActor(req); await logActivity(db, { companyId, actorType: activityActor.actorType, actorId: activityActor.actorId, action: "user_secret_definition.created", entityType: "user_secret_definition", entityId: created.id, details: { key: created.key, provider: created.provider }, }); res.status(201).json(created); }, ); router.patch( "/companies/:companyId/user-secret-definitions/:definitionId", validate(updateUserSecretDefinitionSchema), async (req, res) => { const companyId = req.params.companyId as string; const definitionId = req.params.definitionId as string; assertSecretDefinitionAdmin(req, companyId); const updated = await svc.updateUserSecretDefinition( companyId, definitionId, { key: req.body.key, name: req.body.name, description: req.body.description, status: req.body.status, providerConfigId: req.body.providerConfigId, providerMetadata: req.body.providerMetadata, usageGuidance: req.body.usageGuidance, }, boardActorUser(req), ); if (!updated) { res.status(404).json({ error: "User secret definition not found" }); return; } const activityActor = userSecretDefinitionActivityActor(req); const activityAction = req.body.status === "deleted" ? "user_secret_definition.deleted" : "user_secret_definition.updated"; await logActivity(db, { companyId, actorType: activityActor.actorType, actorId: activityActor.actorId, action: activityAction, entityType: "user_secret_definition", entityId: updated.id, details: { key: updated.key, status: updated.status }, }); res.json(updated); }, ); router.delete("/companies/:companyId/user-secret-definitions/:definitionId", async (req, res) => { const companyId = req.params.companyId as string; const definitionId = req.params.definitionId as string; assertSecretDefinitionAdmin(req, companyId); const removed = await svc.removeUserSecretDefinition( companyId, definitionId, boardActorUser(req), ); if (!removed) { res.status(404).json({ error: "User secret definition not found" }); return; } const activityActor = userSecretDefinitionActivityActor(req); await logActivity(db, { companyId, actorType: activityActor.actorType, actorId: activityActor.actorId, action: "user_secret_definition.deleted", entityType: "user_secret_definition", entityId: removed.id, details: { key: removed.key }, }); res.json({ ok: true }); }); router.get("/companies/:companyId/user-secret-definitions/:definitionId/coverage", async (req, res) => { const companyId = req.params.companyId as string; const definitionId = req.params.definitionId as string; assertSecretDefinitionAdmin(req, companyId); res.json(await svc.getUserSecretDefinitionCoverage(companyId, definitionId)); }); router.get("/companies/:companyId/me/user-secrets", async (req, res) => { assertBoard(req); const companyId = req.params.companyId as string; assertCompanyAccess(req, companyId); res.json(await svc.listCurrentUserSecretValues(companyId, currentUserId(req))); }); router.post( "/companies/:companyId/me/user-secrets", validate(createUserSecretValueSchema), async (req, res) => { assertBoard(req); const companyId = req.params.companyId as string; assertCompanyAccess(req, companyId); const ownerUserId = currentUserId(req); const created = await svc.createCurrentUserSecretValue( companyId, ownerUserId, { definitionKey: req.body.definitionKey, definitionId: req.body.definitionId, value: req.body.value, externalRef: req.body.externalRef, providerVersionRef: req.body.providerVersionRef, providerConfigId: req.body.providerConfigId, }, { userId: ownerUserId, agentId: null }, ); await logActivity(db, { companyId, actorType: "user", actorId: ownerUserId, action: "user_secret_value.created", entityType: "secret", entityId: created.id, details: { userSecretDefinitionId: created.userSecretDefinitionId, ownerUserId: created.ownerUserId, provider: created.provider, }, }); res.status(201).json(created); }, ); router.patch( "/companies/:companyId/me/user-secrets/:secretId", validate(updateUserSecretValueSchema), async (req, res) => { assertBoard(req); const companyId = req.params.companyId as string; const secretId = req.params.secretId as string; assertCompanyAccess(req, companyId); const ownerUserId = currentUserId(req); const updated = await svc.updateCurrentUserSecretValue( companyId, ownerUserId, secretId, { status: req.body.status, value: req.body.value, externalRef: req.body.externalRef, providerVersionRef: req.body.providerVersionRef, providerConfigId: req.body.providerConfigId, }, { userId: ownerUserId, agentId: null }, ); if (!updated) { res.status(404).json({ error: "User secret value not found" }); return; } await logActivity(db, { companyId, actorType: "user", actorId: ownerUserId, action: "user_secret_value.updated", entityType: "secret", entityId: updated.id, details: { userSecretDefinitionId: updated.userSecretDefinitionId, ownerUserId: updated.ownerUserId, status: updated.status, }, }); res.json(updated); }, ); router.post( "/companies/:companyId/me/user-secrets/:secretId/rotate", validate(rotateUserSecretValueSchema), async (req, res) => { assertBoard(req); const companyId = req.params.companyId as string; const secretId = req.params.secretId as string; assertCompanyAccess(req, companyId); const ownerUserId = currentUserId(req); const rotated = await svc.rotateCurrentUserSecretValue( companyId, ownerUserId, secretId, { value: req.body.value, externalRef: req.body.externalRef, providerVersionRef: req.body.providerVersionRef, providerConfigId: req.body.providerConfigId, }, { userId: ownerUserId, agentId: null }, ); await logActivity(db, { companyId, actorType: "user", actorId: ownerUserId, action: "user_secret_value.rotated", entityType: "secret", entityId: rotated.id, details: { userSecretDefinitionId: rotated.userSecretDefinitionId, ownerUserId: rotated.ownerUserId, version: rotated.latestVersion, }, }); res.json(rotated); }, ); router.delete("/companies/:companyId/me/user-secrets/:secretId", async (req, res) => { assertBoard(req); const companyId = req.params.companyId as string; const secretId = req.params.secretId as string; assertCompanyAccess(req, companyId); const ownerUserId = currentUserId(req); const removed = await svc.removeCurrentUserSecretValue(companyId, ownerUserId, secretId); if (!removed) { res.status(404).json({ error: "User secret value not found" }); return; } await logActivity(db, { companyId, actorType: "user", actorId: ownerUserId, action: "user_secret_value.deleted", entityType: "secret", entityId: removed.id, details: { userSecretDefinitionId: removed.userSecretDefinitionId, ownerUserId: removed.ownerUserId, }, }); res.json({ ok: true }); }); router.post("/companies/:companyId/secrets", validate(createSecretSchema), async (req, res) => { const companyId = req.params.companyId as string; assertCompanySecretWrite(req, companyId); const created = await svc.create( companyId, { name: req.body.name, key: req.body.key, provider: req.body.provider ?? defaultProvider, providerConfigId: req.body.providerConfigId, managedMode: req.body.managedMode, value: req.body.value, description: req.body.description, externalRef: req.body.externalRef, providerVersionRef: req.body.providerVersionRef, providerMetadata: req.body.providerMetadata, }, { userId: req.actor.userId ?? "board", agentId: null }, ); await logActivity(db, { companyId, actorType: "user", actorId: req.actor.userId ?? "board", action: "secret.created", entityType: "secret", entityId: created.id, details: { name: created.name, provider: created.provider }, }); res.status(201).json(created); }); router.post( "/companies/:companyId/secrets/remote-import/preview", validate(remoteSecretImportPreviewSchema), async (req, res) => { assertBoard(req); const companyId = req.params.companyId as string; assertCompanyAccess(req, companyId); const preview = await svc.previewRemoteImport(companyId, { providerConfigId: req.body.providerConfigId, query: req.body.query, nextToken: req.body.nextToken, pageSize: req.body.pageSize, }); await logActivity(db, { companyId, actorType: "user", actorId: req.actor.userId ?? "board", action: "secret.remote_import.previewed", entityType: "secret_provider_config", entityId: preview.providerConfigId, details: { provider: preview.provider, candidateCount: preview.candidates.length, readyCount: preview.candidates.filter((candidate) => candidate.status === "ready").length, duplicateCount: preview.candidates.filter((candidate) => candidate.status === "duplicate").length, conflictCount: preview.candidates.filter((candidate) => candidate.status === "conflict").length, }, }); res.json(preview); }, ); router.post( "/companies/:companyId/secrets/remote-import", validate(remoteSecretImportSchema), async (req, res) => { assertBoard(req); const companyId = req.params.companyId as string; assertCompanyAccess(req, companyId); const result = await svc.importRemoteSecrets( companyId, { providerConfigId: req.body.providerConfigId, secrets: req.body.secrets, }, { userId: req.actor.userId ?? "board", agentId: null }, ); await logActivity(db, { companyId, actorType: "user", actorId: req.actor.userId ?? "board", action: "secret.remote_import.completed", entityType: "secret_provider_config", entityId: result.providerConfigId, details: { provider: result.provider, importedCount: result.importedCount, skippedCount: result.skippedCount, errorCount: result.errorCount, }, }); res.json(result); }, ); router.post("/secrets/:id/rotate", validate(rotateSecretSchema), async (req, res) => { assertBoard(req); const id = req.params.id as string; const fetched = await svc.getById(id); const existing = await getAccessibleResource( req, res, fetched && isCompanyScopedSecret(fetched) ? fetched : null, "Secret not found", ); if (!existing) return; if (existing.status === "deleted") { res.status(404).json({ error: "Secret not found" }); return; } const rotated = await svc.rotate( id, { value: req.body.value, externalRef: req.body.externalRef, providerVersionRef: req.body.providerVersionRef, providerConfigId: req.body.providerConfigId, }, { userId: req.actor.userId ?? "board", agentId: null }, ); await logActivity(db, { companyId: rotated.companyId, actorType: "user", actorId: req.actor.userId ?? "board", action: "secret.rotated", entityType: "secret", entityId: rotated.id, details: { version: rotated.latestVersion }, }); res.json(rotated); }); router.patch("/secrets/:id", validate(updateSecretSchema), async (req, res) => { assertBoard(req); const id = req.params.id as string; const fetched = await svc.getById(id); const existing = await getAccessibleResource( req, res, fetched && isCompanyScopedSecret(fetched) ? fetched : null, "Secret not found", ); if (!existing) return; if (existing.status === "deleted") { res.status(404).json({ error: "Secret not found" }); return; } const updated = await svc.update(id, { name: req.body.name, key: req.body.key, status: req.body.status, providerConfigId: req.body.providerConfigId, description: req.body.description, externalRef: req.body.externalRef, providerMetadata: req.body.providerMetadata, }); if (!updated) { res.status(404).json({ error: "Secret not found" }); return; } await logActivity(db, { companyId: updated.companyId, actorType: "user", actorId: req.actor.userId ?? "board", action: "secret.updated", entityType: "secret", entityId: updated.id, details: { name: updated.name }, }); res.json(updated); }); router.get("/secrets/:id/usage", async (req, res) => { assertBoard(req); const id = req.params.id as string; const fetched = await svc.getById(id); const existing = await getAccessibleResource( req, res, fetched && isCompanyScopedSecret(fetched) ? fetched : null, "Secret not found", ); if (!existing) return; const bindings = await svc.listBindingReferences(existing.companyId, existing.id); res.json({ secretId: existing.id, bindings }); }); router.get("/secrets/:id/access-events", async (req, res) => { assertBoard(req); const id = req.params.id as string; const fetched = await svc.getById(id); const existing = await getAccessibleResource( req, res, fetched && isCompanyScopedSecret(fetched) ? fetched : null, "Secret not found", ); if (!existing) return; const events = await svc.listAccessEvents(existing.companyId, existing.id); res.json(events); }); router.delete("/secrets/:id", async (req, res) => { assertBoard(req); const id = req.params.id as string; const fetched = await svc.getById(id); const existing = await getAccessibleResource( req, res, fetched && isCompanyScopedSecret(fetched) ? fetched : null, "Secret not found", ); if (!existing) return; const removed = await svc.remove(id); if (!removed) { res.status(404).json({ error: "Secret not found" }); return; } await logActivity(db, { companyId: removed.companyId, actorType: "user", actorId: req.actor.userId ?? "board", action: "secret.deleted", entityType: "secret", entityId: removed.id, details: { name: removed.name }, }); res.json({ ok: true }); }); return router; }