diff --git a/packages/shared/src/types/secrets.ts b/packages/shared/src/types/secrets.ts index 4b9474adb3b..7a4f0ae3243 100644 --- a/packages/shared/src/types/secrets.ts +++ b/packages/shared/src/types/secrets.ts @@ -198,7 +198,7 @@ export interface SecretAccessEvent { export type RemoteSecretImportCandidateStatus = "ready" | "duplicate" | "conflict"; export interface RemoteSecretImportConflict { - type: "exact_reference" | "name" | "key"; + type: "exact_reference" | "name" | "key" | "provider_guardrail"; message: string; existingSecretId?: string; } diff --git a/server/src/__tests__/aws-secrets-manager-provider.test.ts b/server/src/__tests__/aws-secrets-manager-provider.test.ts index 2e0075c39bb..19d122f377e 100644 --- a/server/src/__tests__/aws-secrets-manager-provider.test.ts +++ b/server/src/__tests__/aws-secrets-manager-provider.test.ts @@ -269,7 +269,18 @@ describe("awsSecretsManagerProvider", () => { }); it("stores linked external references as metadata-only provider material", async () => { - const provider = createAwsSecretsManagerProvider(); + const provider = createAwsSecretsManagerProvider({ + config: { + region: "us-east-1", + endpoint: "https://secretsmanager.us-east-1.amazonaws.com", + deploymentId: "prod-use1", + prefix: "paperclip", + kmsKeyId: "arn:aws:kms:us-east-1:123456789012:key/test", + environmentTag: "production", + providerOwnerTag: "paperclip", + deleteRecoveryWindowDays: 30, + }, + }); const prepared = await provider.linkExternalSecret({ externalRef: "arn:aws:secretsmanager:us-east-1:123456789012:secret:shared/external", @@ -283,6 +294,29 @@ describe("awsSecretsManagerProvider", () => { expect(prepared.valueSha256).toBeTruthy(); }); + it("rejects linked external references under the Paperclip-managed namespace", async () => { + const provider = createAwsSecretsManagerProvider({ + config: { + region: "us-east-1", + endpoint: "https://secretsmanager.us-east-1.amazonaws.com", + deploymentId: "prod-use1", + prefix: "paperclip", + kmsKeyId: "arn:aws:kms:us-east-1:123456789012:key/test", + environmentTag: "production", + providerOwnerTag: "paperclip", + deleteRecoveryWindowDays: 30, + }, + }); + + await expect( + provider.linkExternalSecret({ + externalRef: + "arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company-2/openai-api-key", + providerVersionRef: "linked-version-7", + }), + ).rejects.toThrow(/Paperclip-managed namespace/i); + }); + it("lists remote AWS secrets with metadata only and never resolves plaintext", async () => { const calls: Array<{ op: string; input: Record }> = []; const provider = createAwsSecretsManagerProvider({ @@ -352,15 +386,16 @@ describe("awsSecretsManagerProvider", () => { name: "prod/openai", providerVersionRef: null, metadata: expect.objectContaining({ - arn: "arn:aws:secretsmanager:us-east-1:123456789012:secret:prod/openai", - name: "prod/openai", - description: "OpenAI API key", createdDate: "2026-05-06T00:00:00.000Z", + hasDescription: true, + tagCount: 1, }), }, ], }); expect(JSON.stringify(listed)).not.toContain("SecretString"); + expect(JSON.stringify(listed)).not.toContain("OpenAI API key"); + expect(JSON.stringify(listed)).not.toContain("team"); }); it("resolves AWS secret values by provider version reference", async () => { diff --git a/server/src/__tests__/secrets-routes.test.ts b/server/src/__tests__/secrets-routes.test.ts index 1a3cd557346..a6a28163e64 100644 --- a/server/src/__tests__/secrets-routes.test.ts +++ b/server/src/__tests__/secrets-routes.test.ts @@ -3,6 +3,7 @@ import request from "supertest"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { secretRoutes } from "../routes/secrets.js"; import { errorHandler } from "../middleware/error-handler.js"; +import { unprocessable } from "../errors.js"; const mockSecretService = vi.hoisted(() => ({ listProviders: vi.fn(), @@ -14,7 +15,9 @@ const mockSecretService = vi.hoisted(() => ({ disableProviderConfig: vi.fn(), setDefaultProviderConfig: vi.fn(), checkProviderConfigHealth: vi.fn(), + getById: vi.fn(), create: vi.fn(), + update: vi.fn(), previewRemoteImport: vi.fn(), importRemoteSecrets: vi.fn(), })); @@ -357,4 +360,35 @@ describe("secret routes", () => { })); expect(JSON.stringify(mockLogActivity.mock.calls)).not.toContain("prod/openai"); }); + + it("surfaces update-route externalRef retarget rejection without logging raw refs", async () => { + mockSecretService.getById.mockResolvedValue({ + id: "22222222-2222-4222-8222-222222222222", + companyId: "company-1", + name: "OpenAI API key", + key: "openai-api-key", + provider: "aws_secrets_manager", + managedMode: "external_reference", + externalRef: "arn:aws:secretsmanager:us-east-1:123456789012:secret:shared/original", + }); + mockSecretService.update.mockRejectedValue( + unprocessable("External reference secrets cannot be retargeted through generic update"), + ); + + const res = await request(createApp()) + .patch("/api/secrets/22222222-2222-4222-8222-222222222222") + .send({ + externalRef: "arn:aws:secretsmanager:us-east-1:123456789012:secret:shared/repointed", + }); + + expect(res.status).toBe(422); + expect(mockSecretService.update).toHaveBeenCalledWith( + "22222222-2222-4222-8222-222222222222", + expect.objectContaining({ + externalRef: "arn:aws:secretsmanager:us-east-1:123456789012:secret:shared/repointed", + }), + ); + expect(mockLogActivity).not.toHaveBeenCalled(); + expect(JSON.stringify(mockLogActivity.mock.calls)).not.toContain("shared/repointed"); + }); }); diff --git a/server/src/__tests__/secrets-service.test.ts b/server/src/__tests__/secrets-service.test.ts index 2f54f228dc7..8d6059bb8e5 100644 --- a/server/src/__tests__/secrets-service.test.ts +++ b/server/src/__tests__/secrets-service.test.ts @@ -206,17 +206,23 @@ describeEmbeddedPostgres("secretService", () => { it("stores external references without requiring or persisting secret values", async () => { const companyId = await seedCompany(); const svc = secretService(db); + const awsVault = await svc.createProviderConfig(companyId, { + provider: "aws_secrets_manager", + displayName: "AWS production", + config: { region: "us-east-1", namespace: "prod-use1" }, + }); const secret = await svc.create(companyId, { name: `external-${randomUUID()}`, provider: "aws_secrets_manager", + providerConfigId: awsVault.id, managedMode: "external_reference", - externalRef: "arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/test", + externalRef: "arn:aws:secretsmanager:us-east-1:123456789012:secret:shared/test", providerVersionRef: "version-1", }); expect(secret.managedMode).toBe("external_reference"); - expect(secret.externalRef).toBe("arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/test"); + expect(secret.externalRef).toBe("arn:aws:secretsmanager:us-east-1:123456789012:secret:shared/test"); const versions = await db .select() @@ -439,6 +445,7 @@ describeEmbeddedPostgres("secretService", () => { name: "prod/ready", key: "prod-ready", }); + expect(preview.candidates[2]?.providerMetadata).toBeNull(); }); it("imports AWS remote references row-by-row without fetching plaintext", async () => { @@ -500,6 +507,7 @@ describeEmbeddedPostgres("secretService", () => { managedMode: "external_reference", externalRef: "arn:aws:secretsmanager:us-east-1:123456789012:secret:prod/openai", createdByUserId: "user-1", + providerMetadata: null, }); const versions = await db @@ -511,6 +519,112 @@ describeEmbeddedPostgres("secretService", () => { expect(JSON.stringify(versions[0])).not.toContain("sk-"); }); + it("rejects Paperclip-managed AWS namespace refs during preview and import commit", async () => { + const companyId = await seedCompany(); + const svc = secretService(db); + const awsVault = await svc.createProviderConfig(companyId, { + provider: "aws_secrets_manager", + displayName: "AWS production", + config: { region: "us-east-1", namespace: "prod-use1" }, + }); + + vi.spyOn(awsSecretsManagerProvider, "listRemoteSecrets").mockResolvedValue({ + secrets: [ + { + externalRef: + "arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company-b/openai", + name: "paperclip/prod-use1/company-b/openai", + providerVersionRef: null, + metadata: { + arn: "arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company-b/openai", + description: "must not leak", + tags: [{ Key: "paperclip:company-id", Value: "company-b" }], + }, + }, + ], + }); + + const preview = await svc.previewRemoteImport(companyId, { + providerConfigId: awsVault.id, + }); + + expect(preview.candidates[0]).toMatchObject({ + status: "conflict", + importable: false, + conflicts: [expect.objectContaining({ type: "provider_guardrail" })], + providerMetadata: null, + }); + expect(JSON.stringify(preview)).not.toContain("must not leak"); + expect(JSON.stringify(preview)).not.toContain("paperclip:company-id"); + + const result = await svc.importRemoteSecrets(companyId, { + providerConfigId: awsVault.id, + secrets: [ + { + externalRef: + "arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod-use1/company-b/openai", + name: "Foreign managed secret", + key: "foreign-managed-secret", + providerMetadata: { + description: "client-submitted metadata must not persist", + tags: [{ Key: "paperclip:company-id", Value: "company-b" }], + }, + }, + ], + }); + + expect(result).toMatchObject({ + importedCount: 0, + skippedCount: 0, + errorCount: 1, + results: [expect.objectContaining({ status: "error" })], + }); + expect(result.results[0]?.reason).toMatch(/Paperclip-managed namespace/i); + const imported = await db.select().from(companySecrets).where(eq(companySecrets.key, "foreign-managed-secret")); + expect(imported).toHaveLength(0); + }); + + it("skips duplicate AWS remote imports for the same provider vault and canonical ref", async () => { + const companyId = await seedCompany(); + const svc = secretService(db); + const awsVault = await svc.createProviderConfig(companyId, { + provider: "aws_secrets_manager", + displayName: "AWS production", + config: { region: "us-east-1", namespace: "prod-use1" }, + }); + + const first = await svc.importRemoteSecrets(companyId, { + providerConfigId: awsVault.id, + secrets: [ + { + externalRef: "arn:aws:secretsmanager:us-east-1:123456789012:secret:prod/openai", + name: "OpenAI API key", + key: "openai-api-key", + }, + ], + }); + const second = await svc.importRemoteSecrets(companyId, { + providerConfigId: awsVault.id, + secrets: [ + { + externalRef: "arn:aws:secretsmanager:us-east-1:123456789012:secret:prod/openai", + name: "OpenAI API key duplicate", + key: "openai-api-key-duplicate", + }, + ], + }); + + expect(first.importedCount).toBe(1); + expect(second).toMatchObject({ + importedCount: 0, + skippedCount: 1, + errorCount: 0, + results: [expect.objectContaining({ reason: "exact_reference_duplicate" })], + }); + const imported = await db.select().from(companySecrets).where(eq(companySecrets.providerConfigId, awsVault.id)); + expect(imported).toHaveLength(1); + }); + it("rejects remote import for disabled or cross-company provider vaults", async () => { const companyA = await seedCompany("A"); const companyB = await seedCompany("B"); @@ -558,6 +672,34 @@ describeEmbeddedPostgres("secretService", () => { ).rejects.toThrow(/Managed secrets cannot override externalRef/i); }); + it("rejects generic update retargeting for external reference secrets", async () => { + const companyId = await seedCompany(); + const svc = secretService(db); + const awsVault = await svc.createProviderConfig(companyId, { + provider: "aws_secrets_manager", + displayName: "AWS production", + config: { region: "us-east-1", namespace: "prod-use1" }, + }); + const secret = await svc.create(companyId, { + name: `external-${randomUUID()}`, + provider: "aws_secrets_manager", + providerConfigId: awsVault.id, + managedMode: "external_reference", + externalRef: "arn:aws:secretsmanager:us-east-1:123456789012:secret:shared/original", + }); + + await expect( + svc.update(secret.id, { + externalRef: "arn:aws:secretsmanager:us-east-1:123456789012:secret:shared/repointed", + }), + ).rejects.toThrow(/cannot be retargeted/i); + + const persisted = await svc.getById(secret.id); + expect(persisted?.externalRef).toBe( + "arn:aws:secretsmanager:us-east-1:123456789012:secret:shared/original", + ); + }); + it("passes managed AWS secret context into provider delete during removal", async () => { const companyId = await seedCompany(); const svc = secretService(db); diff --git a/server/src/secrets/aws-secrets-manager-provider.ts b/server/src/secrets/aws-secrets-manager-provider.ts index 4e8425e2ff9..c1f0a08ed7a 100644 --- a/server/src/secrets/aws-secrets-manager-provider.ts +++ b/server/src/secrets/aws-secrets-manager-provider.ts @@ -337,6 +337,32 @@ function isManagedSecretRefForContext( return new RegExp(`^${escapeRegExp(expectedName)}(?:-[A-Za-z0-9]{6})?$`).test(actualName); } +function isManagedSecretNamespaceRef( + config: AwsSecretsManagerConfig, + externalRef: string | null | undefined, +) { + if (!externalRef?.trim()) return false; + const namespacePrefix = [ + sanitizePathSegment(config.prefix), + sanitizePathSegment(config.deploymentId), + ] + .filter(Boolean) + .join("/"); + if (!namespacePrefix) return false; + const actualName = extractAwsSecretName(externalRef); + return actualName === namespacePrefix || actualName.startsWith(`${namespacePrefix}/`); +} + +function assertNotManagedNamespaceExternalRef( + config: AwsSecretsManagerConfig, + externalRef: string, +) { + if (!isManagedSecretNamespaceRef(config, externalRef)) return; + throw unprocessable( + "AWS Paperclip-managed namespace secrets cannot be imported as external references", + ); +} + function resolveManagedSecretRef(input: { config: AwsSecretsManagerConfig; context: ManagedSecretNamespaceContext | undefined; @@ -414,15 +440,13 @@ function serializeAwsDate(value: string | number | Date | undefined): string | n function createRemoteSecretMetadata(entry: AwsSecretsManagerListSecretEntry): Record { return { - arn: entry.ARN ?? null, - name: entry.Name ?? null, - description: entry.Description ?? null, - kmsKeyId: entry.KmsKeyId ?? null, createdDate: serializeAwsDate(entry.CreatedDate), lastAccessedDate: serializeAwsDate(entry.LastAccessedDate), lastChangedDate: serializeAwsDate(entry.LastChangedDate), deletedDate: serializeAwsDate(entry.DeletedDate), - tags: entry.Tags ?? [], + hasDescription: Boolean(entry.Description), + hasKmsKey: Boolean(entry.KmsKeyId), + tagCount: Array.isArray(entry.Tags) ? entry.Tags.length : 0, }; } @@ -740,6 +764,8 @@ export function createAwsSecretsManagerProvider( } }, async linkExternalSecret(input) { + const config = resolveConfig(input.providerConfig); + assertNotManagedNamespaceExternalRef(config, input.externalRef); return createExternalReferenceMaterial(input.externalRef, input.providerVersionRef ?? null); }, async listRemoteSecrets(input): Promise { diff --git a/server/src/services/secrets.ts b/server/src/services/secrets.ts index 387afdb8c9e..5503dc41424 100644 --- a/server/src/services/secrets.ts +++ b/server/src/services/secrets.ts @@ -42,7 +42,11 @@ import { getSecretProvider, listSecretProviders, } from "../secrets/provider-registry.js"; -import type { SecretProviderHealthCheck, SecretProviderVaultRuntimeConfig } from "../secrets/types.js"; +import type { + PreparedSecretVersion, + SecretProviderHealthCheck, + SecretProviderVaultRuntimeConfig, +} from "../secrets/types.js"; const ENV_KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/; const SENSITIVE_ENV_KEY_RE = @@ -689,29 +693,61 @@ export function secretService(db: Db) { name: companySecrets.name, key: companySecrets.key, provider: companySecrets.provider, + providerConfigId: companySecrets.providerConfigId, externalRef: companySecrets.externalRef, }) .from(companySecrets) .where(eq(companySecrets.companyId, companyId)); return { - byExternalRef: new Map( + byProviderConfigExternalRef: new Map( existingSecrets - .filter((secret) => secret.provider === provider && typeof secret.externalRef === "string" && secret.externalRef.trim()) - .map((secret) => [secret.externalRef!.trim(), secret]), + .filter((secret) => + secret.provider === provider && + typeof secret.externalRef === "string" && + secret.externalRef.trim() + ) + .map((secret) => [ + remoteImportExternalRefKey(secret.providerConfigId, secret.externalRef!), + secret, + ]), ), byName: new Map(existingSecrets.map((secret) => [secret.name, secret])), byKey: new Map(existingSecrets.map((secret) => [secret.key, secret])), }; } + function remoteImportExternalRefKey(providerConfigId: string | null | undefined, externalRef: string) { + return `${providerConfigId ?? "default"}\0${externalRef.trim()}`; + } + + function sanitizeRemoteProviderMetadata( + provider: SecretProvider, + metadata: Record | null | undefined, + ): Record | null { + if (!metadata || provider !== "aws_secrets_manager") return null; + const safe: Record = {}; + for (const key of ["createdDate", "lastAccessedDate", "lastChangedDate", "deletedDate"]) { + const value = metadata[key]; + if (typeof value === "string" || value === null) safe[key] = value; + } + for (const key of ["hasDescription", "hasKmsKey", "tagCount"]) { + const value = metadata[key]; + if (typeof value === "boolean" || typeof value === "number") safe[key] = value; + } + return Object.keys(safe).length > 0 ? safe : null; + } + function remoteImportConflictsFor(input: { + providerConfigId: string | null; externalRef: string; name: string; key: string; maps: Awaited>; }): RemoteSecretImportConflict[] { const conflicts: RemoteSecretImportConflict[] = []; - const duplicate = input.maps.byExternalRef.get(input.externalRef); + const duplicate = input.maps.byProviderConfigExternalRef.get( + remoteImportExternalRefKey(input.providerConfigId, input.externalRef), + ); if (duplicate) { conflicts.push({ type: "exact_reference", @@ -1009,26 +1045,54 @@ export function secretService(db: Db) { pageSize: input.pageSize, }); const maps = await buildRemoteImportConflictMaps(companyId, providerId); - const candidates: RemoteSecretImportCandidate[] = listed.secrets.map((remote) => { + const candidates: RemoteSecretImportCandidate[] = []; + for (const remote of listed.secrets) { const externalRef = remote.externalRef.trim(); const remoteName = remote.name.trim() || deriveSecretNameFromExternalRef(externalRef); const name = remoteName || deriveSecretNameFromExternalRef(externalRef); const key = normalizeSecretKey(name); - const conflicts = remoteImportConflictsFor({ externalRef, name, key, maps }); + let canonicalExternalRef = externalRef; + const conflicts: RemoteSecretImportConflict[] = []; + try { + const prepared = await provider.linkExternalSecret({ + externalRef, + providerVersionRef: remote.providerVersionRef ?? null, + providerConfig: runtimeConfig, + context: { + companyId, + secretKey: key || "remote-import-preview", + secretName: name, + version: 1, + }, + }); + canonicalExternalRef = prepared.externalRef ?? externalRef; + } catch (error) { + conflicts.push({ + type: "provider_guardrail", + message: error instanceof Error ? error.message : "Provider rejected this external reference", + }); + } + conflicts.push(...remoteImportConflictsFor({ + providerConfigId: providerConfig.id, + externalRef: canonicalExternalRef, + name, + key, + maps, + })); const hasDuplicate = conflicts.some((conflict) => conflict.type === "exact_reference"); const hasConflict = conflicts.length > 0; - return { + candidates.push({ externalRef, remoteName, name, key, providerVersionRef: remote.providerVersionRef ?? null, - providerMetadata: remote.metadata ?? null, + providerMetadata: sanitizeRemoteProviderMetadata(providerId, remote.metadata), status: hasDuplicate ? "duplicate" : hasConflict ? "conflict" : "ready", importable: !hasConflict, conflicts, - }; - }); + }); + } return { providerConfigId: providerConfig.id, provider: providerId, @@ -1066,7 +1130,14 @@ export function secretService(db: Db) { const externalRef = selection.externalRef.trim(); const name = selection.name?.trim() || deriveSecretNameFromExternalRef(externalRef); const key = normalizeSecretKey(selection.key?.trim() || name); - const conflicts = remoteImportConflictsFor({ externalRef, name, key, maps }); + let prepared: PreparedSecretVersion | undefined; + const conflicts = remoteImportConflictsFor({ + providerConfigId: providerConfig.id, + externalRef, + name, + key, + maps, + }); if (!key) { results.push({ externalRef, @@ -1079,6 +1150,42 @@ export function secretService(db: Db) { }); continue; } + if (conflicts.length === 0) { + try { + prepared = await provider.linkExternalSecret({ + externalRef, + providerVersionRef: selection.providerVersionRef ?? null, + providerConfig: runtimeConfig, + context: { + companyId, + secretKey: key, + secretName: name, + version: 1, + }, + }); + const canonicalDuplicate = maps.byProviderConfigExternalRef.get( + remoteImportExternalRefKey(providerConfig.id, prepared.externalRef ?? externalRef), + ); + if (canonicalDuplicate) { + conflicts.push({ + type: "exact_reference", + existingSecretId: canonicalDuplicate.id, + message: "An existing secret already links this exact provider reference.", + }); + } + } catch (error) { + results.push({ + externalRef, + name, + key, + status: "error", + reason: error instanceof Error ? error.message : "Provider rejected this external reference", + secretId: null, + conflicts: [], + }); + continue; + } + } if (conflicts.length > 0) { results.push({ externalRef, @@ -1095,17 +1202,23 @@ export function secretService(db: Db) { } try { - const prepared = await provider.linkExternalSecret({ - externalRef, - providerVersionRef: selection.providerVersionRef ?? null, - providerConfig: runtimeConfig, - context: { - companyId, - secretKey: key, - secretName: name, - version: 1, - }, - }); + if (!prepared) { + prepared = await provider.linkExternalSecret({ + externalRef, + providerVersionRef: selection.providerVersionRef ?? null, + providerConfig: runtimeConfig, + context: { + companyId, + secretKey: key, + secretName: name, + version: 1, + }, + }); + } + if (!prepared) { + throw unprocessable("Provider rejected this external reference"); + } + const preparedSecret = prepared; const secret = await db.transaction(async (tx) => { const inserted = await tx .insert(companySecrets) @@ -1117,8 +1230,8 @@ export function secretService(db: Db) { providerConfigId: providerConfig.id, status: "active", managedMode: "external_reference", - externalRef: prepared.externalRef, - providerMetadata: selection.providerMetadata ?? null, + externalRef: preparedSecret.externalRef, + providerMetadata: null, latestVersion: 1, description: null, lastRotatedAt: new Date(), @@ -1130,17 +1243,20 @@ export function secretService(db: Db) { await tx.insert(companySecretVersions).values({ secretId: inserted.id, version: 1, - material: prepared.material, - valueSha256: prepared.valueSha256, - fingerprintSha256: prepared.fingerprintSha256 ?? prepared.valueSha256, - providerVersionRef: prepared.providerVersionRef ?? null, + material: preparedSecret.material, + valueSha256: preparedSecret.valueSha256, + fingerprintSha256: preparedSecret.fingerprintSha256 ?? preparedSecret.valueSha256, + providerVersionRef: preparedSecret.providerVersionRef ?? null, status: "current", createdByAgentId: actor?.agentId ?? null, createdByUserId: actor?.userId ?? null, }); return inserted; }); - maps.byExternalRef.set(externalRef, secret); + maps.byProviderConfigExternalRef.set( + remoteImportExternalRefKey(providerConfig.id, preparedSecret.externalRef ?? externalRef), + secret, + ); maps.byName.set(name, secret); maps.byKey.set(key, secret); results.push({ @@ -1413,6 +1529,24 @@ export function secretService(db: Db) { if (secret.managedMode !== "external_reference" && patch.externalRef !== undefined) { throw unprocessable("Managed secrets cannot override externalRef"); } + if ( + secret.managedMode === "external_reference" && + patch.externalRef !== undefined && + patch.externalRef !== secret.externalRef + ) { + throw unprocessable( + "External reference secrets cannot be retargeted through generic update", + ); + } + if ( + secret.managedMode === "external_reference" && + patch.providerConfigId !== undefined && + patch.providerConfigId !== secret.providerConfigId + ) { + throw unprocessable( + "External reference secrets cannot change provider vault through generic update", + ); + } if (patch.providerConfigId !== undefined) { await assertProviderConfigForSecret( secret.companyId,