From 5ca3a5cfb30b8b059665e76b9683d48a60c5515d Mon Sep 17 00:00:00 2001 From: Dotta Date: Sat, 9 May 2026 16:55:44 -0500 Subject: [PATCH] Scope env binding sync deletes --- server/src/__tests__/secrets-service.test.ts | 41 ++++++++++++++++++++ server/src/routes/secrets.ts | 4 ++ server/src/services/secrets.ts | 3 +- 3 files changed, 47 insertions(+), 1 deletion(-) diff --git a/server/src/__tests__/secrets-service.test.ts b/server/src/__tests__/secrets-service.test.ts index af7ac8a7f0c..fcdb0311e9c 100644 --- a/server/src/__tests__/secrets-service.test.ts +++ b/server/src/__tests__/secrets-service.test.ts @@ -205,6 +205,47 @@ describeEmbeddedPostgres("secretService", () => { expect(JSON.stringify(events)).not.toContain("runtime-secret"); }); + it("scopes env binding sync deletes to the env path prefix", async () => { + const companyId = await seedCompany(); + const svc = secretService(db); + const runtimeSecret = await svc.create(companyId, { + name: `runtime-ref-${randomUUID()}`, + provider: "local_encrypted", + value: "runtime-secret", + }); + const envSecret = await svc.create(companyId, { + name: `env-ref-${randomUUID()}`, + provider: "local_encrypted", + value: "env-secret", + }); + + await svc.createBinding({ + companyId, + secretId: runtimeSecret.id, + targetType: "agent", + targetId: "agent-1", + configPath: "runtime.token", + }); + await svc.syncEnvBindingsForTarget( + companyId, + { targetType: "agent", targetId: "agent-1" }, + { + API_KEY: { type: "secret_ref", secretId: envSecret.id, version: "latest" }, + }, + ); + await svc.syncEnvBindingsForTarget( + companyId, + { targetType: "agent", targetId: "agent-1" }, + {}, + ); + + const bindings = await db + .select() + .from(companySecretBindings) + .where(eq(companySecretBindings.targetId, "agent-1")); + expect(bindings.map((binding) => binding.configPath)).toEqual(["runtime.token"]); + }); + it("returns resolved secrets even when success metadata writes fail", async () => { const companyId = await seedCompany(); const svc = secretService(db); diff --git a/server/src/routes/secrets.ts b/server/src/routes/secrets.ts index be9d503f3de..a7f28c78914 100644 --- a/server/src/routes/secrets.ts +++ b/server/src/routes/secrets.ts @@ -454,6 +454,10 @@ export function secretRoutes(db: Db) { return; } assertCompanyAccess(req, existing.companyId); + if (existing.status === "deleted") { + res.status(404).json({ error: "Secret not found" }); + return; + } const removed = await svc.remove(id); if (!removed) { diff --git a/server/src/services/secrets.ts b/server/src/services/secrets.ts index 63e4698e728..30d71446bd0 100644 --- a/server/src/services/secrets.ts +++ b/server/src/services/secrets.ts @@ -1,4 +1,4 @@ -import { and, desc, eq, inArray, ne, notInArray, sql } from "drizzle-orm"; +import { and, desc, eq, inArray, like, ne, notInArray, sql } from "drizzle-orm"; import type { Db } from "@paperclipai/db"; import { agents, @@ -1902,6 +1902,7 @@ export function secretService(db: Db) { eq(companySecretBindings.companyId, companyId), eq(companySecretBindings.targetType, target.targetType), eq(companySecretBindings.targetId, target.targetId), + like(companySecretBindings.configPath, `${pathPrefix}.%`), ), ); if (refs.length === 0) return;