diff --git a/server/src/__tests__/secrets-service.test.ts b/server/src/__tests__/secrets-service.test.ts index 4d7ffd5a68..3bc48eb908 100644 --- a/server/src/__tests__/secrets-service.test.ts +++ b/server/src/__tests__/secrets-service.test.ts @@ -145,6 +145,45 @@ describeEmbeddedPostgres("secretService", () => { ).rejects.toThrow(/already exists/i); }); + it("syncs top-level secret refs idempotently", async () => { + const companyId = await seedCompany(); + const svc = secretService(db); + const firstSecret = await svc.create(companyId, { + name: `top-level-first-${randomUUID()}`, + provider: "local_encrypted", + value: "one", + }); + const secondSecret = await svc.create(companyId, { + name: `top-level-second-${randomUUID()}`, + provider: "local_encrypted", + value: "two", + }); + const target = { targetType: "environment" as const, targetId: "env-1" }; + + await svc.syncSecretRefsForTarget(companyId, target, [ + { secretId: firstSecret.id, configPath: "apiKey" }, + ]); + await svc.syncSecretRefsForTarget(companyId, target, [ + { secretId: firstSecret.id, configPath: "apiKey" }, + ]); + await svc.syncSecretRefsForTarget(companyId, target, [ + { secretId: secondSecret.id, configPath: "apiKey" }, + ]); + + const bindings = await db + .select() + .from(companySecretBindings) + .where(eq(companySecretBindings.targetId, target.targetId)); + expect(bindings).toHaveLength(1); + expect(bindings[0]).toMatchObject({ + companyId, + targetType: "environment", + targetId: target.targetId, + configPath: "apiKey", + secretId: secondSecret.id, + }); + }); + it("reports reference counts and resolves binding target labels", async () => { const companyId = await seedCompany(); const svc = secretService(db); diff --git a/server/src/services/secrets.ts b/server/src/services/secrets.ts index 1f0bf36d65..25b1228d8e 100644 --- a/server/src/services/secrets.ts +++ b/server/src/services/secrets.ts @@ -1,4 +1,4 @@ -import { and, desc, eq, inArray, like, ne, notInArray, sql } from "drizzle-orm"; +import { and, desc, eq, inArray, like, ne, notInArray, or, sql } from "drizzle-orm"; import type { Db } from "@paperclipai/db"; import { agents, @@ -2190,7 +2190,10 @@ export function secretService(db: Db) { eq(companySecretBindings.companyId, companyId), eq(companySecretBindings.targetType, target.targetType), eq(companySecretBindings.targetId, target.targetId), - like(companySecretBindings.configPath, `${pathPrefix}.%`), + or( + eq(companySecretBindings.configPath, pathPrefix), + like(companySecretBindings.configPath, `${pathPrefix}.%`), + ), ), ); }