diff --git a/README.md b/README.md index 2a810396c..5a2844324 100644 --- a/README.md +++ b/README.md @@ -69,8 +69,11 @@ agentcore # interactive TUI │ │ ├── update # update an OAuth2 credential provider │ │ └── delete # delete an OAuth2 credential provider │ └── payment-credential-provider +│ ├── create # store Coinbase CDP or Stripe/Privy credentials for payment connectors │ ├── get # get a payment credential provider -│ └── list # list payment credential providers +│ ├── list # list payment credential providers +│ ├── update # replace a payment credential provider's credentials +│ └── delete # delete a payment credential provider ├── runtime # inspect deployed AgentCore Runtimes │ ├── get # fetch a Runtime by id │ ├── list # list Runtimes (server-side paginated) @@ -207,8 +210,9 @@ one resource of the requested type, `--name` may be omitted. The `payment` commands call the Payments control and data planes directly, with no project involved. This command family currently provides read-only inspection -of existing managers, connectors, sessions, instruments, and payment credential -providers. It does not create IAM roles or change provider credentials. +of existing managers, connectors, sessions, and instruments. It does not create IAM +roles. The separate `identity payment-credential-provider` commands can create, +inspect, replace, or delete stored Coinbase CDP and Stripe/Privy credentials. ```bash # Inspect managers and their connectors. @@ -221,6 +225,10 @@ agentcore payment connector get --manager-id --connector-id +# Store provider credentials from files, not inline command arguments. +agentcore identity payment-credential-provider create --name cdp-creds --vendor CoinbaseCDP \ + --api-key-id --api-key-secret file://api-key-secret.txt --wallet-secret file://wallet-secret.txt + # Session and instrument commands take the parent manager ID and a user id. agentcore payment session list --manager-id --user-id alice agentcore payment instrument list --manager-id --user-id alice diff --git a/src/handlers/identity/payment-credential-provider/__fixtures__/CreatePaymentCredentialProviderCommand.6a6a6d692f0c855a.json b/src/handlers/identity/payment-credential-provider/__fixtures__/CreatePaymentCredentialProviderCommand.6a6a6d692f0c855a.json new file mode 100644 index 000000000..84e12fb4e --- /dev/null +++ b/src/handlers/identity/payment-credential-provider/__fixtures__/CreatePaymentCredentialProviderCommand.6a6a6d692f0c855a.json @@ -0,0 +1,19 @@ +{ + "name": "agentcore-cli-payment-fixture-stripe", + "credentialProviderVendor": "StripePrivy", + "credentialProviderArn": "arn:aws:bedrock-agentcore:us-west-2:603141041947:token-vault/default/paymentcredentialprovider/agentcore-cli-payment-fixture-stripe", + "providerConfigurationOutput": { + "stripePrivyConfiguration": { + "appId": "agentcore-cli-fixture-app", + "appSecretArn": { + "secretArn": "arn:aws:secretsmanager:us-west-2:603141041947:secret:bedrock-agentcore-identity!default/payment/stripeprivy/agentcore-cli-payment-fixture-stripe-28e8771e/appsecret-Ns6m08" + }, + "authorizationPrivateKeyArn": { + "secretArn": "arn:aws:secretsmanager:us-west-2:603141041947:secret:bedrock-agentcore-identity!default/payment/stripeprivy/agentcore-cli-payment-fixture-stripe-28e8771e/authprivkey-7sMU9C" + }, + "authorizationId": "agentcore-cli-fixture-auth", + "appSecretSource": "MANAGED", + "authorizationPrivateKeySource": "MANAGED" + } + } +} \ No newline at end of file diff --git a/src/handlers/identity/payment-credential-provider/__fixtures__/CreatePaymentCredentialProviderCommand.7301309043cb46c3.json b/src/handlers/identity/payment-credential-provider/__fixtures__/CreatePaymentCredentialProviderCommand.7301309043cb46c3.json new file mode 100644 index 000000000..a5443ffff --- /dev/null +++ b/src/handlers/identity/payment-credential-provider/__fixtures__/CreatePaymentCredentialProviderCommand.7301309043cb46c3.json @@ -0,0 +1,18 @@ +{ + "name": "agentcore-cli-payment-fixture", + "credentialProviderVendor": "CoinbaseCDP", + "credentialProviderArn": "arn:aws:bedrock-agentcore:us-west-2:603141041947:token-vault/default/paymentcredentialprovider/agentcore-cli-payment-fixture", + "providerConfigurationOutput": { + "coinbaseCdpConfiguration": { + "apiKeyId": "agentcore-cli-fixture-key", + "apiKeySecretArn": { + "secretArn": "arn:aws:secretsmanager:us-west-2:603141041947:secret:bedrock-agentcore-identity!default/payment/coinbasecdp/agentcore-cli-payment-fixture-541ff54c/apikey-eNjrnd" + }, + "walletSecretArn": { + "secretArn": "arn:aws:secretsmanager:us-west-2:603141041947:secret:bedrock-agentcore-identity!default/payment/coinbasecdp/agentcore-cli-payment-fixture-541ff54c/wallet-DFZFS6" + }, + "apiKeySecretSource": "MANAGED", + "walletSecretSource": "MANAGED" + } + } +} \ No newline at end of file diff --git a/src/handlers/identity/payment-credential-provider/__fixtures__/DeletePaymentCredentialProviderCommand.9ebdd66cb38819e.json b/src/handlers/identity/payment-credential-provider/__fixtures__/DeletePaymentCredentialProviderCommand.9ebdd66cb38819e.json new file mode 100644 index 000000000..9e26dfeeb --- /dev/null +++ b/src/handlers/identity/payment-credential-provider/__fixtures__/DeletePaymentCredentialProviderCommand.9ebdd66cb38819e.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/handlers/identity/payment-credential-provider/__fixtures__/DeletePaymentCredentialProviderCommand.d4020a1cc9b08a46.json b/src/handlers/identity/payment-credential-provider/__fixtures__/DeletePaymentCredentialProviderCommand.d4020a1cc9b08a46.json new file mode 100644 index 000000000..9e26dfeeb --- /dev/null +++ b/src/handlers/identity/payment-credential-provider/__fixtures__/DeletePaymentCredentialProviderCommand.d4020a1cc9b08a46.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/handlers/identity/payment-credential-provider/__fixtures__/UpdatePaymentCredentialProviderCommand.417ee64c8755742b.json b/src/handlers/identity/payment-credential-provider/__fixtures__/UpdatePaymentCredentialProviderCommand.417ee64c8755742b.json new file mode 100644 index 000000000..373e326f0 --- /dev/null +++ b/src/handlers/identity/payment-credential-provider/__fixtures__/UpdatePaymentCredentialProviderCommand.417ee64c8755742b.json @@ -0,0 +1,24 @@ +{ + "name": "agentcore-cli-payment-fixture", + "credentialProviderVendor": "CoinbaseCDP", + "credentialProviderArn": "arn:aws:bedrock-agentcore:us-west-2:603141041947:token-vault/default/paymentcredentialprovider/agentcore-cli-payment-fixture", + "providerConfigurationOutput": { + "coinbaseCdpConfiguration": { + "apiKeyId": "agentcore-cli-fixture-key-rotated", + "apiKeySecretArn": { + "secretArn": "arn:aws:secretsmanager:us-west-2:603141041947:secret:bedrock-agentcore-identity!default/payment/coinbasecdp/agentcore-cli-payment-fixture-541ff54c/apikey-eNjrnd" + }, + "walletSecretArn": { + "secretArn": "arn:aws:secretsmanager:us-west-2:603141041947:secret:bedrock-agentcore-identity!default/payment/coinbasecdp/agentcore-cli-payment-fixture-541ff54c/wallet-DFZFS6" + }, + "apiKeySecretSource": "MANAGED", + "walletSecretSource": "MANAGED" + } + }, + "createdTime": { + "$date": "2026-09-08T20:16:25.544Z" + }, + "lastUpdatedTime": { + "$date": "2026-09-08T20:16:27.626Z" + } +} \ No newline at end of file diff --git a/src/handlers/identity/payment-credential-provider/__fixtures__/create-stripe.golden.json b/src/handlers/identity/payment-credential-provider/__fixtures__/create-stripe.golden.json new file mode 100644 index 000000000..84e12fb4e --- /dev/null +++ b/src/handlers/identity/payment-credential-provider/__fixtures__/create-stripe.golden.json @@ -0,0 +1,19 @@ +{ + "name": "agentcore-cli-payment-fixture-stripe", + "credentialProviderVendor": "StripePrivy", + "credentialProviderArn": "arn:aws:bedrock-agentcore:us-west-2:603141041947:token-vault/default/paymentcredentialprovider/agentcore-cli-payment-fixture-stripe", + "providerConfigurationOutput": { + "stripePrivyConfiguration": { + "appId": "agentcore-cli-fixture-app", + "appSecretArn": { + "secretArn": "arn:aws:secretsmanager:us-west-2:603141041947:secret:bedrock-agentcore-identity!default/payment/stripeprivy/agentcore-cli-payment-fixture-stripe-28e8771e/appsecret-Ns6m08" + }, + "authorizationPrivateKeyArn": { + "secretArn": "arn:aws:secretsmanager:us-west-2:603141041947:secret:bedrock-agentcore-identity!default/payment/stripeprivy/agentcore-cli-payment-fixture-stripe-28e8771e/authprivkey-7sMU9C" + }, + "authorizationId": "agentcore-cli-fixture-auth", + "appSecretSource": "MANAGED", + "authorizationPrivateKeySource": "MANAGED" + } + } +} \ No newline at end of file diff --git a/src/handlers/identity/payment-credential-provider/__fixtures__/create.golden.json b/src/handlers/identity/payment-credential-provider/__fixtures__/create.golden.json new file mode 100644 index 000000000..a5443ffff --- /dev/null +++ b/src/handlers/identity/payment-credential-provider/__fixtures__/create.golden.json @@ -0,0 +1,18 @@ +{ + "name": "agentcore-cli-payment-fixture", + "credentialProviderVendor": "CoinbaseCDP", + "credentialProviderArn": "arn:aws:bedrock-agentcore:us-west-2:603141041947:token-vault/default/paymentcredentialprovider/agentcore-cli-payment-fixture", + "providerConfigurationOutput": { + "coinbaseCdpConfiguration": { + "apiKeyId": "agentcore-cli-fixture-key", + "apiKeySecretArn": { + "secretArn": "arn:aws:secretsmanager:us-west-2:603141041947:secret:bedrock-agentcore-identity!default/payment/coinbasecdp/agentcore-cli-payment-fixture-541ff54c/apikey-eNjrnd" + }, + "walletSecretArn": { + "secretArn": "arn:aws:secretsmanager:us-west-2:603141041947:secret:bedrock-agentcore-identity!default/payment/coinbasecdp/agentcore-cli-payment-fixture-541ff54c/wallet-DFZFS6" + }, + "apiKeySecretSource": "MANAGED", + "walletSecretSource": "MANAGED" + } + } +} \ No newline at end of file diff --git a/src/handlers/identity/payment-credential-provider/__fixtures__/delete-stripe.golden.json b/src/handlers/identity/payment-credential-provider/__fixtures__/delete-stripe.golden.json new file mode 100644 index 000000000..9e26dfeeb --- /dev/null +++ b/src/handlers/identity/payment-credential-provider/__fixtures__/delete-stripe.golden.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/handlers/identity/payment-credential-provider/__fixtures__/delete.golden.json b/src/handlers/identity/payment-credential-provider/__fixtures__/delete.golden.json new file mode 100644 index 000000000..9e26dfeeb --- /dev/null +++ b/src/handlers/identity/payment-credential-provider/__fixtures__/delete.golden.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/handlers/identity/payment-credential-provider/__fixtures__/update.golden.json b/src/handlers/identity/payment-credential-provider/__fixtures__/update.golden.json new file mode 100644 index 000000000..954b15006 --- /dev/null +++ b/src/handlers/identity/payment-credential-provider/__fixtures__/update.golden.json @@ -0,0 +1,20 @@ +{ + "name": "agentcore-cli-payment-fixture", + "credentialProviderVendor": "CoinbaseCDP", + "credentialProviderArn": "arn:aws:bedrock-agentcore:us-west-2:603141041947:token-vault/default/paymentcredentialprovider/agentcore-cli-payment-fixture", + "providerConfigurationOutput": { + "coinbaseCdpConfiguration": { + "apiKeyId": "agentcore-cli-fixture-key-rotated", + "apiKeySecretArn": { + "secretArn": "arn:aws:secretsmanager:us-west-2:603141041947:secret:bedrock-agentcore-identity!default/payment/coinbasecdp/agentcore-cli-payment-fixture-541ff54c/apikey-eNjrnd" + }, + "walletSecretArn": { + "secretArn": "arn:aws:secretsmanager:us-west-2:603141041947:secret:bedrock-agentcore-identity!default/payment/coinbasecdp/agentcore-cli-payment-fixture-541ff54c/wallet-DFZFS6" + }, + "apiKeySecretSource": "MANAGED", + "walletSecretSource": "MANAGED" + } + }, + "createdTime": "2026-09-08T20:16:25.544Z", + "lastUpdatedTime": "2026-09-08T20:16:27.626Z" +} \ No newline at end of file diff --git a/src/handlers/identity/payment-credential-provider/create/index.tsx b/src/handlers/identity/payment-credential-provider/create/index.tsx new file mode 100644 index 000000000..a52e9b80d --- /dev/null +++ b/src/handlers/identity/payment-credential-provider/create/index.tsx @@ -0,0 +1,38 @@ +import z from "zod"; +import { InputValidationError } from "../../../../errors"; +import type { AppIO } from "../../../../io"; +import { createHandler, flag } from "../../../../router"; +import { JsonRendererKey } from "../../../../tui"; +import type { Core } from "../../../types"; +import { coreOptsFromCtx, parseTags } from "../../../utils"; +import { + PaymentProviderConfigurationResolver, + paymentCredentialProviderConfigFlags, +} from "../flags"; + +export const createCreatePaymentCredentialProviderHandler = (core: Core, io: AppIO) => + createHandler({ + name: "create", + description: "create a payment credential provider", + flags: [ + flag("name", "the name of the payment credential provider", z.string().optional()), + ...paymentCredentialProviderConfigFlags, + flag("tags", "tags as key=value (repeatable) or JSON object", z.array(z.string()).optional()), + ], + handle: async (ctx, flags) => { + if (!flags.name) { + throw new InputValidationError("required option '--name ' not specified"); + } + + const configuration = await new PaymentProviderConfigurationResolver(flags, io).resolve(); + + ctx + .require(JsonRendererKey) + .renderJson( + await core.identity.createPaymentCredentialProvider( + { name: flags.name, ...configuration, tags: parseTags(flags.tags) }, + coreOptsFromCtx(ctx), + ), + ); + }, + }); diff --git a/src/handlers/identity/payment-credential-provider/delete/index.tsx b/src/handlers/identity/payment-credential-provider/delete/index.tsx new file mode 100644 index 000000000..f7c12f6e0 --- /dev/null +++ b/src/handlers/identity/payment-credential-provider/delete/index.tsx @@ -0,0 +1,24 @@ +import z from "zod"; +import { InputValidationError } from "../../../../errors"; +import { createHandler, flag } from "../../../../router"; +import { JsonRendererKey } from "../../../../tui"; +import type { Core } from "../../../types"; +import { coreOptsFromCtx } from "../../../utils"; + +export const createDeletePaymentCredentialProviderHandler = (core: Core) => + createHandler({ + name: "delete", + description: "delete a payment credential provider", + flags: [flag("name", "the name of the payment credential provider", z.string().optional())], + handle: async (ctx, flags) => { + if (!flags.name) { + throw new InputValidationError("required option '--name ' not specified"); + } + + ctx + .require(JsonRendererKey) + .renderJson( + await core.identity.deletePaymentCredentialProvider(flags.name, coreOptsFromCtx(ctx)), + ); + }, + }); diff --git a/src/handlers/identity/payment-credential-provider/flags.ts b/src/handlers/identity/payment-credential-provider/flags.ts new file mode 100644 index 000000000..9f6d9f589 --- /dev/null +++ b/src/handlers/identity/payment-credential-provider/flags.ts @@ -0,0 +1,292 @@ +import z from "zod"; +import type { + PaymentCredentialProviderVendorType, + PaymentProviderConfigurationInput, + SecretReference, + SecretSourceType, +} from "@aws-sdk/client-bedrock-agentcore-control"; +import { InputValidationError } from "../../../errors"; +import { type AppIO, SourceResolver } from "../../../io"; +import { flag } from "../../../router"; +import { assertMutuallyExclusiveFlags } from "../../utils"; +import { parseSecretReference } from "../parser"; +import { + stripWalletAuthPrefix, + validateApiKeySecret, + validateAppSecret, + validateAuthorizationPrivateKey, + validatePaymentIdentifier, + validateWalletSecret, +} from "./validation"; + +export const PAYMENT_CREDENTIAL_PROVIDER_VENDORS = [ + "CoinbaseCDP", + "StripePrivy", +] as const satisfies readonly PaymentCredentialProviderVendorType[]; + +const SECRET_SOURCE_HELP = "file://path or - for stdin; inline values are rejected"; +const SECRET_REFERENCE_HELP = + 'as an external secret reference JSON: {"secretId":"","jsonKey":""}'; + +export const paymentCredentialProviderConfigFlags = [ + flag("vendor", "the payment vendor: CoinbaseCDP or StripePrivy", z.string().optional()), + flag("api-key-id", "Coinbase CDP API key ID", z.string().optional()), + flag( + "api-key-secret", + `Coinbase CDP API key secret (${SECRET_SOURCE_HELP})`, + z.string().optional(), + { sensitive: true }, + ), + flag( + "api-key-secret-reference", + `Coinbase CDP API key secret ${SECRET_REFERENCE_HELP}`, + z.string().optional(), + ), + flag( + "wallet-secret", + `Coinbase CDP wallet secret (${SECRET_SOURCE_HELP})`, + z.string().optional(), + { + sensitive: true, + }, + ), + flag( + "wallet-secret-reference", + `Coinbase CDP wallet secret ${SECRET_REFERENCE_HELP}`, + z.string().optional(), + ), + flag("app-id", "Privy application ID", z.string().optional()), + flag("app-secret", `Privy application secret (${SECRET_SOURCE_HELP})`, z.string().optional(), { + sensitive: true, + }), + flag( + "app-secret-reference", + `Privy application secret ${SECRET_REFERENCE_HELP}`, + z.string().optional(), + ), + flag("authorization-id", "Stripe/Privy authorization identifier", z.string().optional()), + flag( + "authorization-private-key", + `Stripe/Privy authorization private key (${SECRET_SOURCE_HELP})`, + z.string().optional(), + { sensitive: true }, + ), + flag( + "authorization-private-key-reference", + `Stripe/Privy authorization private key ${SECRET_REFERENCE_HELP}`, + z.string().optional(), + ), +] as const; + +export interface PaymentCredentialProviderConfigFlags { + vendor?: string; + "api-key-id"?: string; + "api-key-secret"?: string; + "api-key-secret-reference"?: string; + "wallet-secret"?: string; + "wallet-secret-reference"?: string; + "app-id"?: string; + "app-secret"?: string; + "app-secret-reference"?: string; + "authorization-id"?: string; + "authorization-private-key"?: string; + "authorization-private-key-reference"?: string; +} + +type VendorFlagName = Exclude; +type IdentifierFlagName = "api-key-id" | "app-id" | "authorization-id"; +type SecretFlagName = + "api-key-secret" | "wallet-secret" | "app-secret" | "authorization-private-key"; + +const COINBASE_FLAGS = [ + "api-key-id", + "api-key-secret", + "api-key-secret-reference", + "wallet-secret", + "wallet-secret-reference", +] as const satisfies readonly VendorFlagName[]; +const STRIPE_PRIVY_FLAGS = [ + "app-id", + "app-secret", + "app-secret-reference", + "authorization-id", + "authorization-private-key", + "authorization-private-key-reference", +] as const satisfies readonly VendorFlagName[]; + +export interface PaymentProviderConfiguration { + credentialProviderVendor: PaymentCredentialProviderVendorType; + providerConfigurationInput: PaymentProviderConfigurationInput; +} + +type SecretInput = + | { flagName: SecretFlagName; kind: "inline"; source: string } + | { flagName: SecretFlagName; kind: "reference"; config: SecretReference }; + +interface ResolvedSecret { + value?: string; + source: SecretSourceType; + config?: SecretReference; +} + +type SecretValidator = (value: string) => true | string; + +function isPaymentCredentialProviderVendor( + value: string, +): value is PaymentCredentialProviderVendorType { + return (PAYMENT_CREDENTIAL_PROVIDER_VENDORS as readonly string[]).includes(value); +} + +// PaymentProviderConfigurationResolver turns the shared vendor flags of +// `identity payment-credential-provider create|update` into the SDK's +// providerConfigurationInput union. Shape checks (vendor, cross-vendor flags, +// identifiers, inline-versus-reference) all run before any secret is read so a +// rejected command never consumes stdin. +export class PaymentProviderConfigurationResolver { + private readonly resolver: SourceResolver; + + constructor( + private readonly flags: PaymentCredentialProviderConfigFlags, + io: AppIO, + ) { + this.resolver = new SourceResolver({ stdin: io.stdin }); + } + + async resolve(): Promise { + const vendor = this.vendor(); + this.rejectOtherVendorFlags(vendor); + return vendor === "CoinbaseCDP" ? this.resolveCoinbaseCdp() : this.resolveStripePrivy(); + } + + private async resolveCoinbaseCdp(): Promise { + const apiKeyId = this.identifier("api-key-id"); + const apiKeySecretInput = this.secretInput("api-key-secret"); + const walletSecretInput = this.secretInput("wallet-secret"); + if (this.flags["api-key-secret"] === "-" && this.flags["wallet-secret"] === "-") { + throw new InputValidationError( + "--api-key-secret and --wallet-secret cannot both read from stdin", + ); + } + const apiKeySecret = await this.secret(apiKeySecretInput, validateApiKeySecret); + const walletSecret = await this.secret(walletSecretInput, validateWalletSecret); + + return { + credentialProviderVendor: "CoinbaseCDP", + providerConfigurationInput: { + coinbaseCdpConfiguration: { + apiKeyId, + apiKeySecret: apiKeySecret.value, + apiKeySecretSource: apiKeySecret.source, + apiKeySecretConfig: apiKeySecret.config, + walletSecret: walletSecret.value, + walletSecretSource: walletSecret.source, + walletSecretConfig: walletSecret.config, + }, + }, + }; + } + + private async resolveStripePrivy(): Promise { + const appId = this.identifier("app-id"); + const authorizationId = this.identifier("authorization-id"); + const appSecretInput = this.secretInput("app-secret"); + const authorizationPrivateKeyInput = this.secretInput("authorization-private-key"); + if (this.flags["app-secret"] === "-" && this.flags["authorization-private-key"] === "-") { + throw new InputValidationError( + "--app-secret and --authorization-private-key cannot both read from stdin", + ); + } + const appSecret = await this.secret(appSecretInput, validateAppSecret); + const authorizationPrivateKey = await this.secret( + authorizationPrivateKeyInput, + validateAuthorizationPrivateKey, + stripWalletAuthPrefix, + ); + + return { + credentialProviderVendor: "StripePrivy", + providerConfigurationInput: { + stripePrivyConfiguration: { + appId, + appSecret: appSecret.value, + appSecretSource: appSecret.source, + appSecretConfig: appSecret.config, + authorizationPrivateKey: authorizationPrivateKey.value, + authorizationPrivateKeySource: authorizationPrivateKey.source, + authorizationPrivateKeyConfig: authorizationPrivateKey.config, + authorizationId, + }, + }, + }; + } + + private vendor(): PaymentCredentialProviderVendorType { + const vendor = this.flags.vendor; + if (vendor === undefined) { + throw new InputValidationError("required option '--vendor ' not specified"); + } + if (!isPaymentCredentialProviderVendor(vendor)) { + throw new InputValidationError( + `--vendor must be one of ${PAYMENT_CREDENTIAL_PROVIDER_VENDORS.join(", ")}`, + ); + } + return vendor; + } + + private rejectOtherVendorFlags(vendor: PaymentCredentialProviderVendorType): void { + const otherVendorFlags = vendor === "CoinbaseCDP" ? STRIPE_PRIVY_FLAGS : COINBASE_FLAGS; + const passed = otherVendorFlags.filter((flagName) => this.flags[flagName] !== undefined); + if (passed.length === 0) return; + throw new InputValidationError( + `${passed.map((flagName) => `--${flagName}`).join(", ")} ${ + passed.length === 1 ? "is" : "are" + } not valid with --vendor ${vendor}`, + ); + } + + private identifier(flagName: IdentifierFlagName): string { + const raw = this.flags[flagName]; + if (raw === undefined) { + throw new InputValidationError(`required option '--${flagName} <${flagName}>' not specified`); + } + const value = raw.trim(); + const validation = validatePaymentIdentifier(`--${flagName}`, value); + if (validation !== true) throw new InputValidationError(validation); + return value; + } + + private secretInput(flagName: SecretFlagName): SecretInput { + const referenceFlagName = `${flagName}-reference` as const; + const source = this.flags[flagName]; + const reference = this.flags[referenceFlagName]; + assertMutuallyExclusiveFlags( + { [flagName]: source, [referenceFlagName]: reference }, + [flagName, referenceFlagName], + { exactlyOne: true }, + ); + if (source !== undefined) return { flagName, kind: "inline", source }; + return { + flagName, + kind: "reference", + config: parseSecretReference(referenceFlagName, reference!), + }; + } + + private async secret( + input: SecretInput, + validate: SecretValidator, + normalize: (value: string) => string = (value) => value.trim(), + ): Promise { + if (input.kind === "reference") return { source: "EXTERNAL", config: input.config }; + + const value = normalize( + (await this.resolver.resolveSecret(input.flagName, input.source)) ?? "", + ); + if (value.length === 0) { + throw new InputValidationError(`--${input.flagName} must not be empty`); + } + const validation = validate(value); + if (validation !== true) throw new InputValidationError(validation); + return { source: "MANAGED", value }; + } +} diff --git a/src/handlers/identity/payment-credential-provider/index.tsx b/src/handlers/identity/payment-credential-provider/index.tsx index ed8323500..efe695139 100644 --- a/src/handlers/identity/payment-credential-provider/index.tsx +++ b/src/handlers/identity/payment-credential-provider/index.tsx @@ -2,13 +2,19 @@ import { Router } from "../../../router"; import { renderTui } from "../../../tui"; import type { AppIO } from "../../../io"; import type { Core } from "../../types"; +import { createCreatePaymentCredentialProviderHandler } from "./create"; +import { createDeletePaymentCredentialProviderHandler } from "./delete"; import { createGetPaymentCredentialProviderHandler } from "./get"; import { createListPaymentCredentialProvidersHandler } from "./list"; +import { createUpdatePaymentCredentialProviderHandler } from "./update"; export function createPaymentCredentialProviderHandler(core: Core, io: AppIO): Router { return new Router("payment-credential-provider", "manage payment credential providers") .default(renderTui(core, io)) .supportedTuiCommands() + .handler(createCreatePaymentCredentialProviderHandler(core, io)) .handler(createGetPaymentCredentialProviderHandler(core)) - .handler(createListPaymentCredentialProvidersHandler(core)); + .handler(createListPaymentCredentialProvidersHandler(core)) + .handler(createUpdatePaymentCredentialProviderHandler(core, io)) + .handler(createDeletePaymentCredentialProviderHandler(core)); } diff --git a/src/handlers/identity/payment-credential-provider/paymentCredentialProvider.read.test.tsx b/src/handlers/identity/payment-credential-provider/paymentCredentialProvider.read.test.tsx index 65b7003dc..993d6d732 100644 --- a/src/handlers/identity/payment-credential-provider/paymentCredentialProvider.read.test.tsx +++ b/src/handlers/identity/payment-credential-provider/paymentCredentialProvider.read.test.tsx @@ -40,8 +40,8 @@ async function run(args: string[]): Promise { return io.stdout(); } -describe("payment-credential-provider read-only command hierarchy", () => { - test("registers get and list only, with no create, update, or delete commands", () => { +describe("payment-credential-provider command hierarchy", () => { + test("retains get and list alongside the mutation commands", () => { const root = createRootHandler(createFixtureCore(), { io: testIO().io, logger: createSilentLogger(), @@ -52,7 +52,13 @@ describe("payment-credential-provider read-only command hierarchy", () => { ?.children() .find((child) => child.name() === "payment-credential-provider"); - expect(payment?.children().map((child) => child.name())).toEqual(["get", "list"]); + expect(payment?.children().map((child) => child.name())).toEqual([ + "create", + "get", + "list", + "update", + "delete", + ]); }); test("prints command help with --json", async () => { diff --git a/src/handlers/identity/payment-credential-provider/paymentCredentialProvider.test.tsx b/src/handlers/identity/payment-credential-provider/paymentCredentialProvider.test.tsx new file mode 100644 index 000000000..1b9495691 --- /dev/null +++ b/src/handlers/identity/payment-credential-provider/paymentCredentialProvider.test.tsx @@ -0,0 +1,560 @@ +import { afterAll, describe, expect, mock, spyOn, test } from "bun:test"; +import { createECDH, createPrivateKey, createPublicKey } from "node:crypto"; +import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { Readable } from "node:stream"; +import { CoreClient } from "../../../core"; +import { + createSilentLogger, + fixtureFactories, + matchGolden, + TestCoreClient, + TestGlobalConfigAccessor, + testIO, +} from "../../../testing"; +import { createRootHandler } from "../../index"; +import type { Core } from "../../types"; + +const REGION = "us-west-2"; +const FIXTURES = join(import.meta.dir, "__fixtures__"); +const BASE = ["identity", "payment-credential-provider"]; + +const FIXTURE_PROVIDER_NAME = "agentcore-cli-payment-fixture"; +const FIXTURE_STRIPE_PROVIDER_NAME = "agentcore-cli-payment-fixture-stripe"; +const COINBASE_FLAGS = ["--name", "cdp", "--vendor", "CoinbaseCDP", "--api-key-id", "cdp-key-1"]; +const STRIPE_FLAGS = [ + "--name", + "privy", + "--vendor", + "StripePrivy", + "--app-id", + "privy-app", + "--authorization-id", + "privy-auth", +]; +const SECRET_REFERENCE = { + secretId: "arn:aws:secretsmanager:us-west-2:123:secret:payment-fixture", + jsonKey: "secret", +}; +const SECRET_REFERENCE_JSON = JSON.stringify(SECRET_REFERENCE); + +// Fixtures are keyed by a hash of the request, so the throwaway secrets must be identical on +// every record and replay. They are derived from fixed bytes rather than committed as key +// material. Coinbase CDP uses a 32-byte Ed25519 seed followed by the 32-byte public key; +// the P-256 wallet keys use SEC1 DER. +const ED25519_PKCS8_PREFIX = Buffer.from("302e020100300506032b657004220420", "hex"); + +function ed25519PrivateKey(fill: number): string { + const seed = Buffer.alloc(32, fill); + const key = createPrivateKey({ + key: Buffer.concat([ED25519_PKCS8_PREFIX, seed]), + format: "der", + type: "pkcs8", + }); + const spki = createPublicKey(key).export({ format: "der", type: "spki" }) as Buffer; + return Buffer.concat([seed, spki.subarray(spki.length - 32)]).toString("base64"); +} + +function p256PrivateKey(fill: number): string { + const scalar = Buffer.alloc(32, fill); + const ecdh = createECDH("prime256v1"); + ecdh.setPrivateKey(scalar); + const point = ecdh.getPublicKey(); + return createPrivateKey({ + format: "jwk", + key: { + kty: "EC", + crv: "P-256", + d: scalar.toString("base64url"), + x: point.subarray(1, 33).toString("base64url"), + y: point.subarray(33).toString("base64url"), + }, + }) + .export({ format: "der", type: "sec1" }) + .toString("base64"); +} + +const API_KEY_SECRET = ed25519PrivateKey(0x11); +const WALLET_SECRET = p256PrivateKey(0x22); +const UPDATED_API_KEY_SECRET = ed25519PrivateKey(0x55); +const UPDATED_WALLET_SECRET = p256PrivateKey(0x66); +const APP_SECRET = p256PrivateKey(0x77); +const AUTHORIZATION_PRIVATE_KEY = p256PrivateKey(0x88); + +const SECRETS_DIR = mkdtempSync(join(tmpdir(), "agentcore-payment-fixture-")); +afterAll(() => rmSync(SECRETS_DIR, { recursive: true, force: true })); + +function secretFile(name: string, content: string): string { + const path = join(SECRETS_DIR, name); + writeFileSync(path, `${content}\n`); + return `file://${path}`; +} + +const WALLET_SECRET_FILE = secretFile("wallet-secret", WALLET_SECRET); +const UPDATED_WALLET_SECRET_FILE = secretFile("wallet-secret-updated", UPDATED_WALLET_SECRET); +const AUTHORIZATION_PRIVATE_KEY_FILE = secretFile( + "authorization-private-key", + `wallet-auth:${AUTHORIZATION_PRIVATE_KEY}`, +); + +function createFixtureCore(): CoreClient { + const { createControlClient, createDataClient, createIamClient, createLogsClient } = + fixtureFactories(FIXTURES); + return new CoreClient({ + createControlClient, + createDataClient, + createIamClient, + createLogsClient, + logger: createSilentLogger(), + }); +} + +async function run( + args: string[], + stdin?: string, + core: Core = createFixtureCore(), +): Promise { + const io = testIO({ stdin }); + const root = createRootHandler(core, { + io: io.io, + logger: createSilentLogger(), + globalConfigAccessor: new TestGlobalConfigAccessor(), + }); + + await root.route(["node", "agentcore", ...BASE, ...args, "--region", REGION]); + return io.stdout(); +} + +describe("payment-credential-provider flag validation", () => { + test.each([ + ["create", "CoinbaseCDP", COINBASE_FLAGS, ["api-key-secret", "wallet-secret"]], + ["update", "CoinbaseCDP", COINBASE_FLAGS, ["api-key-secret", "wallet-secret"]], + ["create", "StripePrivy", STRIPE_FLAGS, ["app-secret", "authorization-private-key"]], + ["update", "StripePrivy", STRIPE_FLAGS, ["app-secret", "authorization-private-key"]], + ] as const)( + "`%s` rejects competing %s stdin secrets before Core or IO", + async (command, _vendor, flags, secretFlags) => { + const factories = fixtureFactories(FIXTURES); + const sdk = mock(() => { + throw new Error("unexpected SDK client creation"); + }); + for (const name of Object.keys(factories) as (keyof typeof factories)[]) { + spyOn(factories, name).mockImplementation(sdk); + } + const core = new CoreClient({ ...factories, logger: createSilentLogger() }); + const call = spyOn( + core.identity, + command === "create" + ? "createPaymentCredentialProvider" + : "updatePaymentCredentialProvider", + ); + const read = mock(() => { + throw new Error("unexpected stdin read"); + }); + const stdin = new Readable({ read }); + const io = testIO(); + io.io.stdin = stdin as NodeJS.ReadStream; + const root = createRootHandler(core, { + io: io.io, + logger: createSilentLogger(), + globalConfigAccessor: new TestGlobalConfigAccessor(), + }); + + try { + await expect( + root.route([ + "node", + "agentcore", + ...BASE, + command, + ...flags, + ...secretFlags.flatMap((flagName) => [`--${flagName}`, "-"]), + "--region", + REGION, + ]), + ).rejects.toThrow( + `--${secretFlags[0]} and --${secretFlags[1]} cannot both read from stdin`, + ); + expect(call).not.toHaveBeenCalled(); + expect(sdk).not.toHaveBeenCalled(); + expect(read).not.toHaveBeenCalled(); + expect(io.stdout()).toBe(""); + expect(io.stderr()).toBe(""); + } finally { + call.mockRestore(); + stdin.destroy(); + } + }, + ); + + test.each(["create", "update", "delete"])("requires a name for %s", async (command) => { + await expect(run([command])).rejects.toThrow("required option '--name ' not specified"); + }); + + test.each([ + ["create without a vendor", ["create", "--name", "x"], /--vendor /], + ["update without a vendor", ["update", "--name", "x"], /--vendor /], + [ + "create with an unknown vendor", + ["create", "--name", "x", "--vendor", "Square"], + "--vendor must be one of CoinbaseCDP, StripePrivy", + ], + ["update with tags", ["update", ...COINBASE_FLAGS, "--tags", "a=b"], /--tags/], + [ + "CoinbaseCDP without --api-key-id", + ["create", "--name", "x", "--vendor", "CoinbaseCDP"], + "required option '--api-key-id ' not specified", + ], + [ + "StripePrivy without --app-id", + ["create", "--name", "x", "--vendor", "StripePrivy", "--authorization-id", "a"], + "required option '--app-id ' not specified", + ], + [ + "StripePrivy without --authorization-id", + ["create", "--name", "x", "--vendor", "StripePrivy", "--app-id", "a"], + "required option '--authorization-id ' not specified", + ], + [ + "--app-id with CoinbaseCDP", + ["create", ...COINBASE_FLAGS, "--app-id", "a"], + "--app-id is not valid with --vendor CoinbaseCDP", + ], + [ + "Coinbase flags with StripePrivy", + [ + "update", + ...STRIPE_FLAGS, + "--api-key-id", + "k", + "--wallet-secret-reference", + SECRET_REFERENCE_JSON, + ], + "--api-key-id, --wallet-secret-reference are not valid with --vendor StripePrivy", + ], + [ + "CoinbaseCDP without an api key secret", + ["create", ...COINBASE_FLAGS], + "specify exactly one of --api-key-secret, --api-key-secret-reference", + ], + [ + "CoinbaseCDP without a wallet secret", + ["create", ...COINBASE_FLAGS, "--api-key-secret", "-"], + "specify exactly one of --wallet-secret, --wallet-secret-reference", + ], + [ + "CoinbaseCDP with both api key secret forms", + [ + "create", + ...COINBASE_FLAGS, + "--api-key-secret", + "-", + "--api-key-secret-reference", + SECRET_REFERENCE_JSON, + "--wallet-secret-reference", + SECRET_REFERENCE_JSON, + ], + "specify exactly one of --api-key-secret, --api-key-secret-reference", + ], + [ + "StripePrivy without an app secret", + ["create", ...STRIPE_FLAGS, "--authorization-private-key-reference", SECRET_REFERENCE_JSON], + "specify exactly one of --app-secret, --app-secret-reference", + ], + [ + "StripePrivy with both authorization private key forms", + [ + "update", + ...STRIPE_FLAGS, + "--app-secret-reference", + SECRET_REFERENCE_JSON, + "--authorization-private-key", + "-", + "--authorization-private-key-reference", + SECRET_REFERENCE_JSON, + ], + "specify exactly one of --authorization-private-key, --authorization-private-key-reference", + ], + [ + "inline api key secret value", + [ + "create", + ...COINBASE_FLAGS, + "--api-key-secret", + API_KEY_SECRET, + "--wallet-secret-reference", + SECRET_REFERENCE_JSON, + ], + /--api-key-secret must come from stdin \('-'\) or a file \('file:\/\/'\)/, + ], + ] as const)("rejects %s before Core", async (_label, args, message) => { + const core = createFixtureCore(); + const create = spyOn(core.identity, "createPaymentCredentialProvider"); + const update = spyOn(core.identity, "updatePaymentCredentialProvider"); + + try { + await expect(run([...args], undefined, core)).rejects.toThrow(message); + expect(create).not.toHaveBeenCalled(); + expect(update).not.toHaveBeenCalled(); + } finally { + create.mockRestore(); + update.mockRestore(); + } + }); + + test.each([ + [ + "api key secret that is not an Ed25519 key", + [ + "create", + ...COINBASE_FLAGS, + "--api-key-secret", + "-", + "--wallet-secret-reference", + SECRET_REFERENCE_JSON, + ], + "not-base64!", + /Ed25519/, + ], + [ + "empty app secret", + [ + "create", + ...STRIPE_FLAGS, + "--app-secret", + "-", + "--authorization-private-key-reference", + SECRET_REFERENCE_JSON, + ], + "", + "--app-secret must not be empty", + ], + ] as const)("rejects a malformed %s", async (_label, args, stdin, message) => { + const core = createFixtureCore(); + const create = spyOn(core.identity, "createPaymentCredentialProvider"); + + try { + await expect(run([...args], stdin, core)).rejects.toThrow(message); + expect(create).not.toHaveBeenCalled(); + } finally { + create.mockRestore(); + } + }); +}); + +// External-reference fixtures are unavailable; verify the consumer-owned Core contract here. +describe("payment-credential-provider request mapping", () => { + test("creates a CoinbaseCDP provider with external secret references", async () => { + const core = new TestCoreClient(); + await run( + [ + "create", + ...COINBASE_FLAGS, + "--api-key-secret-reference", + SECRET_REFERENCE_JSON, + "--wallet-secret-reference", + JSON.stringify({ ...SECRET_REFERENCE, jsonKey: "wallet" }), + ], + undefined, + core, + ); + + expect(core.identity.calls).toEqual([ + { + method: "createPaymentCredentialProvider", + args: [ + { + name: "cdp", + credentialProviderVendor: "CoinbaseCDP", + providerConfigurationInput: { + coinbaseCdpConfiguration: { + apiKeyId: "cdp-key-1", + apiKeySecretSource: "EXTERNAL", + apiKeySecretConfig: SECRET_REFERENCE, + walletSecretSource: "EXTERNAL", + walletSecretConfig: { ...SECRET_REFERENCE, jsonKey: "wallet" }, + }, + }, + }, + { region: REGION }, + ], + }, + ]); + }); + + test("mixes a managed api key secret with an external wallet secret", async () => { + const core = new TestCoreClient(); + await run( + [ + "create", + ...COINBASE_FLAGS, + "--api-key-secret", + "-", + "--wallet-secret-reference", + SECRET_REFERENCE_JSON, + ], + API_KEY_SECRET, + core, + ); + + expect(core.identity.calls).toEqual([ + { + method: "createPaymentCredentialProvider", + args: [ + { + name: "cdp", + credentialProviderVendor: "CoinbaseCDP", + providerConfigurationInput: { + coinbaseCdpConfiguration: { + apiKeyId: "cdp-key-1", + apiKeySecret: API_KEY_SECRET, + apiKeySecretSource: "MANAGED", + walletSecretSource: "EXTERNAL", + walletSecretConfig: SECRET_REFERENCE, + }, + }, + }, + { region: REGION }, + ], + }, + ]); + }); + + test.each([ + ["create", "createPaymentCredentialProvider"], + ["update", "updatePaymentCredentialProvider"], + ] as const)("%s uses StripePrivy external references", async (command, method) => { + const core = new TestCoreClient(); + await run( + [ + command, + ...STRIPE_FLAGS, + "--app-secret-reference", + SECRET_REFERENCE_JSON, + "--authorization-private-key-reference", + JSON.stringify({ ...SECRET_REFERENCE, jsonKey: "authorization" }), + ], + undefined, + core, + ); + + expect(core.identity.calls).toEqual([ + { + method, + args: [ + { + name: "privy", + credentialProviderVendor: "StripePrivy", + providerConfigurationInput: { + stripePrivyConfiguration: { + appId: "privy-app", + appSecretSource: "EXTERNAL", + appSecretConfig: SECRET_REFERENCE, + authorizationPrivateKeySource: "EXTERNAL", + authorizationPrivateKeyConfig: { ...SECRET_REFERENCE, jsonKey: "authorization" }, + authorizationId: "privy-auth", + }, + }, + }, + { region: REGION }, + ], + }, + ]); + }); +}); + +// Each command uses the real root and Core; fixture hashes verify the full SDK request, +// including resolved managed secrets. Providers must not exist before a fresh recording. +describe("payment-credential-provider write flow", () => { + test("creates a CoinbaseCDP provider with managed stdin/file secrets and tags", async () => { + const stdout = await run( + [ + "create", + "--name", + FIXTURE_PROVIDER_NAME, + "--vendor", + "CoinbaseCDP", + "--api-key-id", + " agentcore-cli-fixture-key ", + "--api-key-secret", + "-", + "--wallet-secret", + WALLET_SECRET_FILE, + "--tags", + "owner=agentcore-cli-tests", + ], + API_KEY_SECRET, + ); + + matchGolden(FIXTURES, "create.golden.json", stdout); + expect(JSON.parse(stdout)).toMatchObject({ + name: FIXTURE_PROVIDER_NAME, + credentialProviderVendor: "CoinbaseCDP", + }); + }); + + test("updates a payment credential provider with fresh keys", async () => { + const stdout = await run( + [ + "update", + "--name", + FIXTURE_PROVIDER_NAME, + "--vendor", + "CoinbaseCDP", + "--api-key-id", + "agentcore-cli-fixture-key-rotated", + "--api-key-secret", + "-", + "--wallet-secret", + UPDATED_WALLET_SECRET_FILE, + ], + UPDATED_API_KEY_SECRET, + ); + + matchGolden(FIXTURES, "update.golden.json", stdout); + expect(JSON.parse(stdout).name).toBe(FIXTURE_PROVIDER_NAME); + }); + + test("creates a StripePrivy provider with managed secrets and strips wallet-auth", async () => { + const stdout = await run( + [ + "create", + "--name", + FIXTURE_STRIPE_PROVIDER_NAME, + "--vendor", + "StripePrivy", + "--app-id", + "agentcore-cli-fixture-app", + "--app-secret", + "-", + "--authorization-id", + "agentcore-cli-fixture-auth", + "--authorization-private-key", + AUTHORIZATION_PRIVATE_KEY_FILE, + ], + APP_SECRET, + ); + + matchGolden(FIXTURES, "create-stripe.golden.json", stdout); + expect(JSON.parse(stdout)).toMatchObject({ + name: FIXTURE_STRIPE_PROVIDER_NAME, + credentialProviderVendor: "StripePrivy", + }); + }); + + test.each([ + [FIXTURE_STRIPE_PROVIDER_NAME, "delete-stripe.golden.json"], + [FIXTURE_PROVIDER_NAME, "delete.golden.json"], + ])("deletes %s", async (name, golden) => { + const core = createFixtureCore(); + const call = spyOn(core.identity, "deletePaymentCredentialProvider"); + + try { + matchGolden(FIXTURES, golden, await run(["delete", "--name", name], undefined, core)); + expect(call.mock.calls).toEqual([[name, { region: REGION }]]); + } finally { + call.mockRestore(); + } + }); +}); diff --git a/src/handlers/identity/payment-credential-provider/update/index.tsx b/src/handlers/identity/payment-credential-provider/update/index.tsx new file mode 100644 index 000000000..2ed38da84 --- /dev/null +++ b/src/handlers/identity/payment-credential-provider/update/index.tsx @@ -0,0 +1,39 @@ +import z from "zod"; +import { InputValidationError } from "../../../../errors"; +import type { AppIO } from "../../../../io"; +import { createHandler, flag } from "../../../../router"; +import { JsonRendererKey } from "../../../../tui"; +import type { Core } from "../../../types"; +import { coreOptsFromCtx } from "../../../utils"; +import { + PaymentProviderConfigurationResolver, + paymentCredentialProviderConfigFlags, +} from "../flags"; + +// The service replaces the whole vendor configuration on update, so the flags +// and validation are the same as create minus tags. +export const createUpdatePaymentCredentialProviderHandler = (core: Core, io: AppIO) => + createHandler({ + name: "update", + description: "update a payment credential provider", + flags: [ + flag("name", "the name of the payment credential provider", z.string().optional()), + ...paymentCredentialProviderConfigFlags, + ], + handle: async (ctx, flags) => { + if (!flags.name) { + throw new InputValidationError("required option '--name ' not specified"); + } + + const configuration = await new PaymentProviderConfigurationResolver(flags, io).resolve(); + + ctx + .require(JsonRendererKey) + .renderJson( + await core.identity.updatePaymentCredentialProvider( + { name: flags.name, ...configuration }, + coreOptsFromCtx(ctx), + ), + ); + }, + }); diff --git a/src/handlers/project/add/credentials/payment/validation.test.ts b/src/handlers/identity/payment-credential-provider/validation.test.ts similarity index 88% rename from src/handlers/project/add/credentials/payment/validation.test.ts rename to src/handlers/identity/payment-credential-provider/validation.test.ts index ac72406b4..2515a5c90 100644 --- a/src/handlers/project/add/credentials/payment/validation.test.ts +++ b/src/handlers/identity/payment-credential-provider/validation.test.ts @@ -20,6 +20,9 @@ describe("payment credential key validation", () => { test("rejects invalid Coinbase key formats", () => { expect(validateApiKeySecret("not-base64")).toContain("Ed25519"); expect(validateApiKeySecret(Buffer.alloc(48, 0x41).toString("base64"))).toContain("length"); + // A bare 32-byte seed is a valid Ed25519 key in general, but the service + // rejects it, so fail fast on the same shape it enforces. + expect(validateApiKeySecret(Buffer.alloc(32, 0x41).toString("base64"))).toContain("length"); expect(validateWalletSecret(ed25519Key)).toContain("P-256"); }); diff --git a/src/handlers/project/add/credentials/payment/validation.ts b/src/handlers/identity/payment-credential-provider/validation.ts similarity index 90% rename from src/handlers/project/add/credentials/payment/validation.ts rename to src/handlers/identity/payment-credential-provider/validation.ts index 9160258eb..d2f06b9a9 100644 --- a/src/handlers/project/add/credentials/payment/validation.ts +++ b/src/handlers/identity/payment-credential-provider/validation.ts @@ -1,5 +1,8 @@ const BASE64_PATTERN = /^[A-Za-z0-9+/]+=*$/; -const ED25519_KEY_LENGTHS = new Set([32, 64]); +// Coinbase CDP hands out the 64-byte seed‖public-key form, and the service rejects a +// bare 32-byte seed ("Expected base64-encoded Ed25519 private key"), so only the +// 64-byte form is accepted here. +const ED25519_KEY_LENGTHS = new Set([64]); const P256_MIN_BYTES = 100; const P256_MAX_BYTES = 200; const WALLET_AUTH_PREFIX = "wallet-auth:"; diff --git a/src/handlers/identity/types.tsx b/src/handlers/identity/types.tsx index 6f8363f90..feff95666 100644 --- a/src/handlers/identity/types.tsx +++ b/src/handlers/identity/types.tsx @@ -77,8 +77,8 @@ export interface CoreIdentityClient { // Payment credential providers hold a payment vendor's own credentials (a Coinbase // CDP API key and wallet secret, or Privy app and authorization secrets). They back - // payment connectors and can be inspected with `identity payment-credential-provider`. - // Project deployment uses the write operations below. + // payment connectors and are managed by `agentcore identity + // payment-credential-provider` as well as provisioned by `project deploy`. createPaymentCredentialProvider( input: CreatePaymentCredentialProviderInput, options: CoreOptions, diff --git a/src/handlers/project/add/credentials/payment/input.ts b/src/handlers/project/add/credentials/payment/input.ts index 038e3b41d..1080fbd24 100644 --- a/src/handlers/project/add/credentials/payment/input.ts +++ b/src/handlers/project/add/credentials/payment/input.ts @@ -12,7 +12,7 @@ import { validateAuthorizationPrivateKey, validatePaymentIdentifier, validateWalletSecret, -} from "./validation"; +} from "../../../../identity/payment-credential-provider/validation"; export const paymentCredentialInputFlags = [ flag("api-key-id", "Coinbase CDP API key ID", z.string().optional()),