From fe4635cf4dd5e62bf232b11b7475235a4143d5e5 Mon Sep 17 00:00:00 2001 From: Nicolas Borges Date: Fri, 6 Nov 2026 09:22:16 -0500 Subject: [PATCH] fix(payments): unblock migrated targets with legacy payment providers --- .../project/backends/cdk/credentials.test.ts | 21 +++++++++++++++++++ src/core/project/backends/cdk/credentials.ts | 10 ++++++--- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/src/core/project/backends/cdk/credentials.test.ts b/src/core/project/backends/cdk/credentials.test.ts index 1f3b56dfcc..5bf24ac3a5 100644 --- a/src/core/project/backends/cdk/credentials.test.ts +++ b/src/core/project/backends/cdk/credentials.test.ts @@ -1155,6 +1155,27 @@ describe("projects migrated from v0", () => { expect(result["openai-key"]?.credentialProviderArn).toBe("arn:apikey:openai-key"); }); + test("a new target deploys when another target uses the bare payment provider", async () => { + const oldProvider = { credentialProviderArn: paymentArn("wallet") }; + const subject = account({ wallet: oldProvider }); + const input = await migratedProject( + [COINBASE], + "AGENTCORE_CREDENTIAL_WALLET_API_KEY_ID='key-1'\n" + + "AGENTCORE_CREDENTIAL_WALLET_API_KEY_SECRET='key-secret'\n" + + "AGENTCORE_CREDENTIAL_WALLET_WALLET_SECRET='wallet-secret'\n", + ); + + const { result } = await run(subject.provision, input, { + ...INPUT, + targetName: "prod", + }); + + expect(subject.contents().wallet).toEqual(oldProvider); + expect(result.wallet?.credentialProviderArn).toMatch( + /^arn:payment:example-prod-wallet-[a-f0-9]{16}$/, + ); + }); + test("never delete API key or OAuth providers, which v0 projects may share", async () => { const subject = account({ "openai-key": { credentialProviderArn: "arn:apikey:openai-key" } }); diff --git a/src/core/project/backends/cdk/credentials.ts b/src/core/project/backends/cdk/credentials.ts index 7f8d1cb0dc..356e91e2cc 100644 --- a/src/core/project/backends/cdk/credentials.ts +++ b/src/core/project/backends/cdk/credentials.ts @@ -334,9 +334,13 @@ export function createCredentialProvisioner( // first provider is written, not partway through the list. const plans: (NamedCredential & { provision: Provision })[] = []; for (const { credential, provider } of named) { - // Lost target state must not turn an existing payment provider into an - // untracked orphan while a newly named provider takes its place. - if (credential.authorizerType === "PaymentCredentialProvider" && !recorded[credential.name]) { + // A bare v0 provider may belong to another target, so its existence does + // not prove this target lost state. + if ( + credential.authorizerType === "PaymentCredentialProvider" && + !recorded[credential.name] && + !keepsV0CredentialNames(project) + ) { const previousName = previousProviderName(project, targetName, credential.name); if ( previousName !== provider &&