From a17f08dc59da0ddf80b1076e8e4d0250e19e9426 Mon Sep 17 00:00:00 2001 From: Nicolas Borges Date: Thu, 23 Jul 2026 16:33:01 -0400 Subject: [PATCH 1/4] fix: source aware api-key flag + redact api key value from logging --- src/core/identity.tsx | 35 +++++++--- ...ntialProviderCommand.83b094791cea0052.json | 8 +++ ...ntialProviderCommand.dfed014f440c1884.json | 8 +++ ...ntialProviderCommand.b0c07380332f4673.json | 6 +- ...tialProvidersCommand.23f97c9dcdd6350b.json | 8 +-- ...tialProvidersCommand.6e1f31dcdf053d87.json | 4 +- ...tialProvidersCommand.7d2e22c637f6b633.json | 4 +- ...ntialProviderCommand.a779aef2084d562f.json | 14 ++++ .../__fixtures__/create-2.golden.json | 2 +- .../identity/__fixtures__/create.golden.json | 2 +- .../identity/__fixtures__/get.golden.json | 6 +- .../__fixtures__/list-page-1.golden.json | 4 +- .../__fixtures__/list-page-2.golden.json | 4 +- .../identity/__fixtures__/list.golden.json | 8 +-- .../identity/__fixtures__/update.golden.json | 6 +- .../create/index.tsx | 69 ++++++++++++++---- .../api-key-credential-provider/index.tsx | 4 +- .../update/index.tsx | 64 +++++++++++++---- src/handlers/identity/identity.test.tsx | 65 ++++++++++++++++- src/handlers/identity/types.tsx | 22 ++++-- src/handlers/source.test.tsx | 70 +++++++++++++++++++ src/handlers/source.tsx | 46 ++++++++++++ src/testing/TestCoreClient.tsx | 12 ++-- 23 files changed, 392 insertions(+), 79 deletions(-) create mode 100644 src/handlers/identity/__fixtures__/CreateApiKeyCredentialProviderCommand.83b094791cea0052.json create mode 100644 src/handlers/identity/__fixtures__/CreateApiKeyCredentialProviderCommand.dfed014f440c1884.json create mode 100644 src/handlers/identity/__fixtures__/UpdateApiKeyCredentialProviderCommand.a779aef2084d562f.json create mode 100644 src/handlers/source.test.tsx create mode 100644 src/handlers/source.tsx diff --git a/src/core/identity.tsx b/src/core/identity.tsx index 6ef9e3b1e..d6aa9dd3a 100644 --- a/src/core/identity.tsx +++ b/src/core/identity.tsx @@ -10,7 +10,11 @@ import { type ListApiKeyCredentialProvidersResponse, type UpdateApiKeyCredentialProviderResponse, } from "@aws-sdk/client-bedrock-agentcore-control"; -import type { CoreIdentityClient } from "../handlers/identity/types"; +import type { + CoreIdentityClient, + CreateApiKeyCredentialProviderInput, + UpdateApiKeyCredentialProviderInput, +} from "../handlers/identity/types"; import type { AwsClients, CoreOptions } from "./types"; import { toClientConfig } from "./utils"; @@ -18,13 +22,18 @@ export class IdentityClient implements CoreIdentityClient { constructor(private readonly clients: AwsClients) {} async createApiKeyCredentialProvider( - name: string, - apiKey: string, + input: CreateApiKeyCredentialProviderInput, options: CoreOptions, ): Promise { - return this.clients - .control(toClientConfig(options)) - .send(new CreateApiKeyCredentialProviderCommand({ name, apiKey })); + return this.clients.control(toClientConfig(options)).send( + new CreateApiKeyCredentialProviderCommand({ + name: input.name, + apiKey: input.apiKey, + apiKeySecretConfig: input.apiKeySecretConfig, + apiKeySecretSource: input.apiKeySecretSource, + tags: input.tags, + }), + ); } async getApiKeyCredentialProvider( @@ -47,13 +56,17 @@ export class IdentityClient implements CoreIdentityClient { } async updateApiKeyCredentialProvider( - name: string, - apiKey: string, + input: UpdateApiKeyCredentialProviderInput, options: CoreOptions, ): Promise { - return this.clients - .control(toClientConfig(options)) - .send(new UpdateApiKeyCredentialProviderCommand({ name, apiKey })); + return this.clients.control(toClientConfig(options)).send( + new UpdateApiKeyCredentialProviderCommand({ + name: input.name, + apiKey: input.apiKey, + apiKeySecretConfig: input.apiKeySecretConfig, + apiKeySecretSource: input.apiKeySecretSource, + }), + ); } async deleteApiKeyCredentialProvider( diff --git a/src/handlers/identity/__fixtures__/CreateApiKeyCredentialProviderCommand.83b094791cea0052.json b/src/handlers/identity/__fixtures__/CreateApiKeyCredentialProviderCommand.83b094791cea0052.json new file mode 100644 index 000000000..1d407c18f --- /dev/null +++ b/src/handlers/identity/__fixtures__/CreateApiKeyCredentialProviderCommand.83b094791cea0052.json @@ -0,0 +1,8 @@ +{ + "apiKeySecretArn": { + "secretArn": "arn:aws:secretsmanager:us-west-2:685197708687:secret:bedrock-agentcore-identity!default/apikey/agentcore-cli-identity-fixture-ceac1e1d-2BRhkE" + }, + "name": "agentcore-cli-identity-fixture", + "credentialProviderArn": "arn:aws:bedrock-agentcore:us-west-2:685197708687:token-vault/default/apikeycredentialprovider/agentcore-cli-identity-fixture", + "apiKeySecretSource": "MANAGED" +} \ No newline at end of file diff --git a/src/handlers/identity/__fixtures__/CreateApiKeyCredentialProviderCommand.dfed014f440c1884.json b/src/handlers/identity/__fixtures__/CreateApiKeyCredentialProviderCommand.dfed014f440c1884.json new file mode 100644 index 000000000..82c564b13 --- /dev/null +++ b/src/handlers/identity/__fixtures__/CreateApiKeyCredentialProviderCommand.dfed014f440c1884.json @@ -0,0 +1,8 @@ +{ + "apiKeySecretArn": { + "secretArn": "arn:aws:secretsmanager:us-west-2:685197708687:secret:bedrock-agentcore-identity!default/apikey/agentcore-cli-identity-fixture-2-56db9332-Jv9u5N" + }, + "name": "agentcore-cli-identity-fixture-2", + "credentialProviderArn": "arn:aws:bedrock-agentcore:us-west-2:685197708687:token-vault/default/apikeycredentialprovider/agentcore-cli-identity-fixture-2", + "apiKeySecretSource": "MANAGED" +} \ No newline at end of file diff --git a/src/handlers/identity/__fixtures__/GetApiKeyCredentialProviderCommand.b0c07380332f4673.json b/src/handlers/identity/__fixtures__/GetApiKeyCredentialProviderCommand.b0c07380332f4673.json index e24f8571a..dfd54cf0e 100644 --- a/src/handlers/identity/__fixtures__/GetApiKeyCredentialProviderCommand.b0c07380332f4673.json +++ b/src/handlers/identity/__fixtures__/GetApiKeyCredentialProviderCommand.b0c07380332f4673.json @@ -1,14 +1,14 @@ { "apiKeySecretArn": { - "secretArn": "arn:aws:secretsmanager:us-west-2:685197708687:secret:bedrock-agentcore-identity!default/apikey/agentcore-cli-identity-fixture-34b3f8a9-cHh41n" + "secretArn": "arn:aws:secretsmanager:us-west-2:685197708687:secret:bedrock-agentcore-identity!default/apikey/agentcore-cli-identity-fixture-ceac1e1d-2BRhkE" }, "name": "agentcore-cli-identity-fixture", "credentialProviderArn": "arn:aws:bedrock-agentcore:us-west-2:685197708687:token-vault/default/apikeycredentialprovider/agentcore-cli-identity-fixture", "createdTime": { - "$date": "2026-07-23T16:31:54.009Z" + "$date": "2026-07-23T19:31:48.713Z" }, "lastUpdatedTime": { - "$date": "2026-07-23T16:31:54.009Z" + "$date": "2026-07-23T19:31:48.713Z" }, "apiKeySecretSource": "MANAGED" } \ No newline at end of file diff --git a/src/handlers/identity/__fixtures__/ListApiKeyCredentialProvidersCommand.23f97c9dcdd6350b.json b/src/handlers/identity/__fixtures__/ListApiKeyCredentialProvidersCommand.23f97c9dcdd6350b.json index 302dbefb1..cbbd672ee 100644 --- a/src/handlers/identity/__fixtures__/ListApiKeyCredentialProvidersCommand.23f97c9dcdd6350b.json +++ b/src/handlers/identity/__fixtures__/ListApiKeyCredentialProvidersCommand.23f97c9dcdd6350b.json @@ -4,20 +4,20 @@ "name": "agentcore-cli-identity-fixture", "credentialProviderArn": "arn:aws:bedrock-agentcore:us-west-2:685197708687:token-vault/default/apikeycredentialprovider/agentcore-cli-identity-fixture", "createdTime": { - "$date": "2026-07-23T16:31:54.009Z" + "$date": "2026-07-23T19:31:48.713Z" }, "lastUpdatedTime": { - "$date": "2026-07-23T16:31:54.009Z" + "$date": "2026-07-23T19:31:48.713Z" } }, { "name": "agentcore-cli-identity-fixture-2", "credentialProviderArn": "arn:aws:bedrock-agentcore:us-west-2:685197708687:token-vault/default/apikeycredentialprovider/agentcore-cli-identity-fixture-2", "createdTime": { - "$date": "2026-07-23T16:31:54.309Z" + "$date": "2026-07-23T19:31:49.014Z" }, "lastUpdatedTime": { - "$date": "2026-07-23T16:31:54.309Z" + "$date": "2026-07-23T19:31:49.014Z" } } ] diff --git a/src/handlers/identity/__fixtures__/ListApiKeyCredentialProvidersCommand.6e1f31dcdf053d87.json b/src/handlers/identity/__fixtures__/ListApiKeyCredentialProvidersCommand.6e1f31dcdf053d87.json index 4c7d1281d..0bde1e53f 100644 --- a/src/handlers/identity/__fixtures__/ListApiKeyCredentialProvidersCommand.6e1f31dcdf053d87.json +++ b/src/handlers/identity/__fixtures__/ListApiKeyCredentialProvidersCommand.6e1f31dcdf053d87.json @@ -4,10 +4,10 @@ "name": "agentcore-cli-identity-fixture-2", "credentialProviderArn": "arn:aws:bedrock-agentcore:us-west-2:685197708687:token-vault/default/apikeycredentialprovider/agentcore-cli-identity-fixture-2", "createdTime": { - "$date": "2026-07-23T16:31:54.309Z" + "$date": "2026-07-23T19:31:49.014Z" }, "lastUpdatedTime": { - "$date": "2026-07-23T16:31:54.309Z" + "$date": "2026-07-23T19:31:49.014Z" } } ] diff --git a/src/handlers/identity/__fixtures__/ListApiKeyCredentialProvidersCommand.7d2e22c637f6b633.json b/src/handlers/identity/__fixtures__/ListApiKeyCredentialProvidersCommand.7d2e22c637f6b633.json index e0cc1edb9..1aab12e1f 100644 --- a/src/handlers/identity/__fixtures__/ListApiKeyCredentialProvidersCommand.7d2e22c637f6b633.json +++ b/src/handlers/identity/__fixtures__/ListApiKeyCredentialProvidersCommand.7d2e22c637f6b633.json @@ -4,10 +4,10 @@ "name": "agentcore-cli-identity-fixture", "credentialProviderArn": "arn:aws:bedrock-agentcore:us-west-2:685197708687:token-vault/default/apikeycredentialprovider/agentcore-cli-identity-fixture", "createdTime": { - "$date": "2026-07-23T16:31:54.009Z" + "$date": "2026-07-23T19:31:48.713Z" }, "lastUpdatedTime": { - "$date": "2026-07-23T16:31:54.009Z" + "$date": "2026-07-23T19:31:48.713Z" } } ], diff --git a/src/handlers/identity/__fixtures__/UpdateApiKeyCredentialProviderCommand.a779aef2084d562f.json b/src/handlers/identity/__fixtures__/UpdateApiKeyCredentialProviderCommand.a779aef2084d562f.json new file mode 100644 index 000000000..dfd54cf0e --- /dev/null +++ b/src/handlers/identity/__fixtures__/UpdateApiKeyCredentialProviderCommand.a779aef2084d562f.json @@ -0,0 +1,14 @@ +{ + "apiKeySecretArn": { + "secretArn": "arn:aws:secretsmanager:us-west-2:685197708687:secret:bedrock-agentcore-identity!default/apikey/agentcore-cli-identity-fixture-ceac1e1d-2BRhkE" + }, + "name": "agentcore-cli-identity-fixture", + "credentialProviderArn": "arn:aws:bedrock-agentcore:us-west-2:685197708687:token-vault/default/apikeycredentialprovider/agentcore-cli-identity-fixture", + "createdTime": { + "$date": "2026-07-23T19:31:48.713Z" + }, + "lastUpdatedTime": { + "$date": "2026-07-23T19:31:48.713Z" + }, + "apiKeySecretSource": "MANAGED" +} \ No newline at end of file diff --git a/src/handlers/identity/__fixtures__/create-2.golden.json b/src/handlers/identity/__fixtures__/create-2.golden.json index 429b6ec9c..82c564b13 100644 --- a/src/handlers/identity/__fixtures__/create-2.golden.json +++ b/src/handlers/identity/__fixtures__/create-2.golden.json @@ -1,6 +1,6 @@ { "apiKeySecretArn": { - "secretArn": "arn:aws:secretsmanager:us-west-2:685197708687:secret:bedrock-agentcore-identity!default/apikey/agentcore-cli-identity-fixture-2-29a3e5b6-qiRJxb" + "secretArn": "arn:aws:secretsmanager:us-west-2:685197708687:secret:bedrock-agentcore-identity!default/apikey/agentcore-cli-identity-fixture-2-56db9332-Jv9u5N" }, "name": "agentcore-cli-identity-fixture-2", "credentialProviderArn": "arn:aws:bedrock-agentcore:us-west-2:685197708687:token-vault/default/apikeycredentialprovider/agentcore-cli-identity-fixture-2", diff --git a/src/handlers/identity/__fixtures__/create.golden.json b/src/handlers/identity/__fixtures__/create.golden.json index 23d34eaa3..1d407c18f 100644 --- a/src/handlers/identity/__fixtures__/create.golden.json +++ b/src/handlers/identity/__fixtures__/create.golden.json @@ -1,6 +1,6 @@ { "apiKeySecretArn": { - "secretArn": "arn:aws:secretsmanager:us-west-2:685197708687:secret:bedrock-agentcore-identity!default/apikey/agentcore-cli-identity-fixture-34b3f8a9-cHh41n" + "secretArn": "arn:aws:secretsmanager:us-west-2:685197708687:secret:bedrock-agentcore-identity!default/apikey/agentcore-cli-identity-fixture-ceac1e1d-2BRhkE" }, "name": "agentcore-cli-identity-fixture", "credentialProviderArn": "arn:aws:bedrock-agentcore:us-west-2:685197708687:token-vault/default/apikeycredentialprovider/agentcore-cli-identity-fixture", diff --git a/src/handlers/identity/__fixtures__/get.golden.json b/src/handlers/identity/__fixtures__/get.golden.json index 9360f4609..7a27b5b13 100644 --- a/src/handlers/identity/__fixtures__/get.golden.json +++ b/src/handlers/identity/__fixtures__/get.golden.json @@ -1,10 +1,10 @@ { "apiKeySecretArn": { - "secretArn": "arn:aws:secretsmanager:us-west-2:685197708687:secret:bedrock-agentcore-identity!default/apikey/agentcore-cli-identity-fixture-34b3f8a9-cHh41n" + "secretArn": "arn:aws:secretsmanager:us-west-2:685197708687:secret:bedrock-agentcore-identity!default/apikey/agentcore-cli-identity-fixture-ceac1e1d-2BRhkE" }, "name": "agentcore-cli-identity-fixture", "credentialProviderArn": "arn:aws:bedrock-agentcore:us-west-2:685197708687:token-vault/default/apikeycredentialprovider/agentcore-cli-identity-fixture", - "createdTime": "2026-07-23T16:31:54.009Z", - "lastUpdatedTime": "2026-07-23T16:31:54.009Z", + "createdTime": "2026-07-23T19:31:48.713Z", + "lastUpdatedTime": "2026-07-23T19:31:48.713Z", "apiKeySecretSource": "MANAGED" } \ No newline at end of file diff --git a/src/handlers/identity/__fixtures__/list-page-1.golden.json b/src/handlers/identity/__fixtures__/list-page-1.golden.json index 455b164f3..064555294 100644 --- a/src/handlers/identity/__fixtures__/list-page-1.golden.json +++ b/src/handlers/identity/__fixtures__/list-page-1.golden.json @@ -3,8 +3,8 @@ { "name": "agentcore-cli-identity-fixture", "credentialProviderArn": "arn:aws:bedrock-agentcore:us-west-2:685197708687:token-vault/default/apikeycredentialprovider/agentcore-cli-identity-fixture", - "createdTime": "2026-07-23T16:31:54.009Z", - "lastUpdatedTime": "2026-07-23T16:31:54.009Z" + "createdTime": "2026-07-23T19:31:48.713Z", + "lastUpdatedTime": "2026-07-23T19:31:48.713Z" } ], "nextToken": "eyJBY2NvdW50QW5kVHlwZUdzaVBrIjp7InZhbHVlU3RyIjoiNjg1MTk3NzA4Njg3I0FQSV9LRVkiLCJhdHRyaWJ1dGVUeXBlIjoiUyJ9LCJTSyI6eyJ2YWx1ZVN0ciI6ImFnZW50Y29yZS1jbGktaWRlbnRpdHktZml4dHVyZSIsImF0dHJpYnV0ZVR5cGUiOiJTIn0sIlBLIjp7InZhbHVlU3RyIjoiQVBJX0tFWV9DUkVEX1BST1ZJREVSIzY4NTE5NzcwODY4NyNkZWZhdWx0I2FnZW50Y29yZS1jbGktaWRlbnRpdHktZml4dHVyZSIsImF0dHJpYnV0ZVR5cGUiOiJTIn19" diff --git a/src/handlers/identity/__fixtures__/list-page-2.golden.json b/src/handlers/identity/__fixtures__/list-page-2.golden.json index 6f0f60522..e8ba37a34 100644 --- a/src/handlers/identity/__fixtures__/list-page-2.golden.json +++ b/src/handlers/identity/__fixtures__/list-page-2.golden.json @@ -3,8 +3,8 @@ { "name": "agentcore-cli-identity-fixture-2", "credentialProviderArn": "arn:aws:bedrock-agentcore:us-west-2:685197708687:token-vault/default/apikeycredentialprovider/agentcore-cli-identity-fixture-2", - "createdTime": "2026-07-23T16:31:54.309Z", - "lastUpdatedTime": "2026-07-23T16:31:54.309Z" + "createdTime": "2026-07-23T19:31:49.014Z", + "lastUpdatedTime": "2026-07-23T19:31:49.014Z" } ] } \ No newline at end of file diff --git a/src/handlers/identity/__fixtures__/list.golden.json b/src/handlers/identity/__fixtures__/list.golden.json index b3501a60a..7125ee9c9 100644 --- a/src/handlers/identity/__fixtures__/list.golden.json +++ b/src/handlers/identity/__fixtures__/list.golden.json @@ -3,14 +3,14 @@ { "name": "agentcore-cli-identity-fixture", "credentialProviderArn": "arn:aws:bedrock-agentcore:us-west-2:685197708687:token-vault/default/apikeycredentialprovider/agentcore-cli-identity-fixture", - "createdTime": "2026-07-23T16:31:54.009Z", - "lastUpdatedTime": "2026-07-23T16:31:54.009Z" + "createdTime": "2026-07-23T19:31:48.713Z", + "lastUpdatedTime": "2026-07-23T19:31:48.713Z" }, { "name": "agentcore-cli-identity-fixture-2", "credentialProviderArn": "arn:aws:bedrock-agentcore:us-west-2:685197708687:token-vault/default/apikeycredentialprovider/agentcore-cli-identity-fixture-2", - "createdTime": "2026-07-23T16:31:54.309Z", - "lastUpdatedTime": "2026-07-23T16:31:54.309Z" + "createdTime": "2026-07-23T19:31:49.014Z", + "lastUpdatedTime": "2026-07-23T19:31:49.014Z" } ] } \ No newline at end of file diff --git a/src/handlers/identity/__fixtures__/update.golden.json b/src/handlers/identity/__fixtures__/update.golden.json index 9360f4609..7a27b5b13 100644 --- a/src/handlers/identity/__fixtures__/update.golden.json +++ b/src/handlers/identity/__fixtures__/update.golden.json @@ -1,10 +1,10 @@ { "apiKeySecretArn": { - "secretArn": "arn:aws:secretsmanager:us-west-2:685197708687:secret:bedrock-agentcore-identity!default/apikey/agentcore-cli-identity-fixture-34b3f8a9-cHh41n" + "secretArn": "arn:aws:secretsmanager:us-west-2:685197708687:secret:bedrock-agentcore-identity!default/apikey/agentcore-cli-identity-fixture-ceac1e1d-2BRhkE" }, "name": "agentcore-cli-identity-fixture", "credentialProviderArn": "arn:aws:bedrock-agentcore:us-west-2:685197708687:token-vault/default/apikeycredentialprovider/agentcore-cli-identity-fixture", - "createdTime": "2026-07-23T16:31:54.009Z", - "lastUpdatedTime": "2026-07-23T16:31:54.009Z", + "createdTime": "2026-07-23T19:31:48.713Z", + "lastUpdatedTime": "2026-07-23T19:31:48.713Z", "apiKeySecretSource": "MANAGED" } \ No newline at end of file diff --git a/src/handlers/identity/api-key-credential-provider/create/index.tsx b/src/handlers/identity/api-key-credential-provider/create/index.tsx index 72b671a02..237a68ef3 100644 --- a/src/handlers/identity/api-key-credential-provider/create/index.tsx +++ b/src/handlers/identity/api-key-credential-provider/create/index.tsx @@ -1,33 +1,74 @@ import z from "zod"; import { createHandler, flag } from "../../../../router"; import { JsonRendererKey } from "../../../../tui"; -import type { Core } from "../../../types"; +import type { AppIO, Core } from "../../../types"; import { coreOptsFromCtx } from "../../../utils"; +import { readSourceText } from "../../../source"; +import { parseJsonFlag } from "../../../utils"; -export const createCreateApiKeyCredentialProviderHandler = (core: Core) => +export const createCreateApiKeyCredentialProviderHandler = (core: Core, io: AppIO) => createHandler({ name: "create", description: "create an API key credential provider", flags: [ flag("name", "the name of the API key credential provider", z.string().optional()), - flag("api-key", "the API key value", z.string().optional()), + flag("api-key", "the API key value (inline, file://path, or -)", z.string().optional(), { + sensitive: true, + }), + flag("api-key-secret-arn", "existing Secrets Manager secret ARN", z.string().optional()), + flag( + "api-key-secret-json-key", + "JSON key containing the API key in the secret", + z.string().optional(), + ), + flag("tags", "tags as key=value (repeatable) or JSON object", z.string().optional()), ], handle: async (ctx, flags) => { if (!flags.name) { throw new TypeError("required option '--name ' not specified"); } - if (!flags["api-key"]) { - throw new TypeError("required option '--api-key ' not specified"); - } - ctx - .require(JsonRendererKey) - .renderJson( - await core.identity.createApiKeyCredentialProvider( - flags.name, - flags["api-key"], - coreOptsFromCtx(ctx), - ), + const hasApiKey = flags["api-key"] !== undefined; + const hasSecretArn = flags["api-key-secret-arn"] !== undefined; + const hasSecretJsonKey = flags["api-key-secret-json-key"] !== undefined; + + if (hasApiKey && (hasSecretArn || hasSecretJsonKey)) { + throw new TypeError( + "--api-key and --api-key-secret-arn/--api-key-secret-json-key are mutually exclusive", + ); + } + if (!hasApiKey && !hasSecretArn) { + throw new TypeError( + "either --api-key or --api-key-secret-arn and --api-key-secret-json-key are required", + ); + } + if (hasSecretArn !== hasSecretJsonKey) { + throw new TypeError( + "--api-key-secret-arn and --api-key-secret-json-key must be specified together", ); + } + + const apiKey = hasApiKey ? await readSourceText(flags["api-key"]!, io.stdin) : undefined; + + const apiKeySecretConfig = hasSecretArn + ? { secretId: flags["api-key-secret-arn"]!, jsonKey: flags["api-key-secret-json-key"]! } + : undefined; + + const apiKeySecretSource = hasApiKey ? "MANAGED" : "EXTERNAL"; + + const tags = parseJsonFlag>("tags", flags.tags); + + ctx.require(JsonRendererKey).renderJson( + await core.identity.createApiKeyCredentialProvider( + { + name: flags.name, + apiKey, + apiKeySecretConfig, + apiKeySecretSource: apiKeySecretSource as any, + tags, + }, + coreOptsFromCtx(ctx), + ), + ); }, }); diff --git a/src/handlers/identity/api-key-credential-provider/index.tsx b/src/handlers/identity/api-key-credential-provider/index.tsx index ffb308599..9d975d25e 100644 --- a/src/handlers/identity/api-key-credential-provider/index.tsx +++ b/src/handlers/identity/api-key-credential-provider/index.tsx @@ -11,9 +11,9 @@ import { createUpdateApiKeyCredentialProviderHandler } from "./update"; export function createApiKeyCredentialProviderHandler(core: Core, io: AppIO): Router { return new Router("api-key-credential-provider", "manage API key credential providers") .default(createHelpDefault(io)) - .handler(createCreateApiKeyCredentialProviderHandler(core)) + .handler(createCreateApiKeyCredentialProviderHandler(core, io)) .handler(createGetApiKeyCredentialProviderHandler(core)) .handler(createListApiKeyCredentialProvidersHandler(core)) - .handler(createUpdateApiKeyCredentialProviderHandler(core)) + .handler(createUpdateApiKeyCredentialProviderHandler(core, io)) .handler(createDeleteApiKeyCredentialProviderHandler(core)); } diff --git a/src/handlers/identity/api-key-credential-provider/update/index.tsx b/src/handlers/identity/api-key-credential-provider/update/index.tsx index b081d5198..e7d2e8ff2 100644 --- a/src/handlers/identity/api-key-credential-provider/update/index.tsx +++ b/src/handlers/identity/api-key-credential-provider/update/index.tsx @@ -1,33 +1,69 @@ import z from "zod"; import { createHandler, flag } from "../../../../router"; import { JsonRendererKey } from "../../../../tui"; -import type { Core } from "../../../types"; +import type { AppIO, Core } from "../../../types"; import { coreOptsFromCtx } from "../../../utils"; +import { readSourceText } from "../../../source"; -export const createUpdateApiKeyCredentialProviderHandler = (core: Core) => +export const createUpdateApiKeyCredentialProviderHandler = (core: Core, io: AppIO) => createHandler({ name: "update", description: "update an API key credential provider", flags: [ flag("name", "the name of the API key credential provider", z.string().optional()), - flag("api-key", "the new API key value", z.string().optional()), + flag("api-key", "the new API key value (inline, file://path, or -)", z.string().optional(), { + sensitive: true, + }), + flag("api-key-secret-arn", "existing Secrets Manager secret ARN", z.string().optional()), + flag( + "api-key-secret-json-key", + "JSON key containing the API key in the secret", + z.string().optional(), + ), ], handle: async (ctx, flags) => { if (!flags.name) { throw new TypeError("required option '--name ' not specified"); } - if (!flags["api-key"]) { - throw new TypeError("required option '--api-key ' not specified"); - } - ctx - .require(JsonRendererKey) - .renderJson( - await core.identity.updateApiKeyCredentialProvider( - flags.name, - flags["api-key"], - coreOptsFromCtx(ctx), - ), + const hasApiKey = flags["api-key"] !== undefined; + const hasSecretArn = flags["api-key-secret-arn"] !== undefined; + const hasSecretJsonKey = flags["api-key-secret-json-key"] !== undefined; + + if (hasApiKey && (hasSecretArn || hasSecretJsonKey)) { + throw new TypeError( + "--api-key and --api-key-secret-arn/--api-key-secret-json-key are mutually exclusive", + ); + } + if (!hasApiKey && !hasSecretArn) { + throw new TypeError( + "either --api-key or --api-key-secret-arn and --api-key-secret-json-key are required", + ); + } + if (hasSecretArn !== hasSecretJsonKey) { + throw new TypeError( + "--api-key-secret-arn and --api-key-secret-json-key must be specified together", ); + } + + const apiKey = hasApiKey ? await readSourceText(flags["api-key"]!, io.stdin) : undefined; + + const apiKeySecretConfig = hasSecretArn + ? { secretId: flags["api-key-secret-arn"]!, jsonKey: flags["api-key-secret-json-key"]! } + : undefined; + + const apiKeySecretSource = hasApiKey ? "MANAGED" : "EXTERNAL"; + + ctx.require(JsonRendererKey).renderJson( + await core.identity.updateApiKeyCredentialProvider( + { + name: flags.name, + apiKey, + apiKeySecretConfig, + apiKeySecretSource: apiKeySecretSource as any, + }, + coreOptsFromCtx(ctx), + ), + ); }, }); diff --git a/src/handlers/identity/identity.test.tsx b/src/handlers/identity/identity.test.tsx index 02f75a680..9f359e690 100644 --- a/src/handlers/identity/identity.test.tsx +++ b/src/handlers/identity/identity.test.tsx @@ -193,7 +193,7 @@ describe("api-key-credential-provider CRUDL", () => { [ "create --name only", ["identity", "api-key-credential-provider", "create", "--name", "x"], - /--api-key/, + /--api-key.*--api-key-secret-arn/, ], [ "create --api-key only", @@ -206,13 +206,74 @@ describe("api-key-credential-provider CRUDL", () => { [ "update --name only", ["identity", "api-key-credential-provider", "update", "--name", "x"], - /--api-key/, + /--api-key.*--api-key-secret-arn/, ], ["delete bare", ["identity", "api-key-credential-provider", "delete"], /--name/], ] as const)("rejects missing required flags for `%s`", async (_label, args, message) => { expect(run([...args])).rejects.toThrow(message); }); + test.each([ + [ + "create: --api-key with --api-key-secret-arn", + [ + "identity", + "api-key-credential-provider", + "create", + "--name", + "x", + "--api-key", + "k", + "--api-key-secret-arn", + "arn:aws:secretsmanager:us-west-2:123:secret:s", + ], + /mutually exclusive/, + ], + [ + "create: --api-key-secret-arn without --api-key-secret-json-key", + [ + "identity", + "api-key-credential-provider", + "create", + "--name", + "x", + "--api-key-secret-arn", + "arn:aws:secretsmanager:us-west-2:123:secret:s", + ], + /must be specified together/, + ], + [ + "update: --api-key with --api-key-secret-arn", + [ + "identity", + "api-key-credential-provider", + "update", + "--name", + "x", + "--api-key", + "k", + "--api-key-secret-arn", + "arn:aws:secretsmanager:us-west-2:123:secret:s", + ], + /mutually exclusive/, + ], + [ + "update: --api-key-secret-json-key without --api-key-secret-arn", + [ + "identity", + "api-key-credential-provider", + "update", + "--name", + "x", + "--api-key-secret-json-key", + "key", + ], + /--api-key.*--api-key-secret-arn/, + ], + ] as const)("rejects mutually exclusive flags for `%s`", async (_label, args, message) => { + expect(run([...args])).rejects.toThrow(message); + }); + test("propagates ResourceNotFoundException from get", async () => { await expect( run(["identity", "api-key-credential-provider", "get", "--name", MISSING_PROVIDER_NAME]), diff --git a/src/handlers/identity/types.tsx b/src/handlers/identity/types.tsx index 9a6d36cc0..3da60f717 100644 --- a/src/handlers/identity/types.tsx +++ b/src/handlers/identity/types.tsx @@ -3,14 +3,29 @@ import type { DeleteApiKeyCredentialProviderResponse, GetApiKeyCredentialProviderResponse, ListApiKeyCredentialProvidersResponse, + SecretSourceType, UpdateApiKeyCredentialProviderResponse, } from "@aws-sdk/client-bedrock-agentcore-control"; import type { CoreOptions } from "../../core/types"; +export interface CreateApiKeyCredentialProviderInput { + name: string; + apiKey?: string; + apiKeySecretConfig?: { secretId: string; jsonKey: string }; + apiKeySecretSource?: SecretSourceType; + tags?: Record; +} + +export interface UpdateApiKeyCredentialProviderInput { + name: string; + apiKey?: string; + apiKeySecretConfig?: { secretId: string; jsonKey: string }; + apiKeySecretSource?: SecretSourceType; +} + export interface CoreIdentityClient { createApiKeyCredentialProvider( - name: string, - apiKey: string, + input: CreateApiKeyCredentialProviderInput, options: CoreOptions, ): Promise; getApiKeyCredentialProvider( @@ -23,8 +38,7 @@ export interface CoreIdentityClient { options: CoreOptions, ): Promise; updateApiKeyCredentialProvider( - name: string, - apiKey: string, + input: UpdateApiKeyCredentialProviderInput, options: CoreOptions, ): Promise; deleteApiKeyCredentialProvider( diff --git a/src/handlers/source.test.tsx b/src/handlers/source.test.tsx new file mode 100644 index 000000000..7cec30da2 --- /dev/null +++ b/src/handlers/source.test.tsx @@ -0,0 +1,70 @@ +import { describe, expect, test } from "bun:test"; +import { join } from "node:path"; +import { writeFileSync, mkdirSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { Readable } from "node:stream"; +import { readSource, readSourceText } from "./source"; + +const TMP = join(tmpdir(), "source-test-" + Date.now()); + +function fakeStdin(content: string): NodeJS.ReadStream { + const stream = Readable.from([Buffer.from(content)]); + return stream as unknown as NodeJS.ReadStream; +} + +describe("readSource", () => { + test("returns inline value as UTF-8 bytes", async () => { + const result = await readSource("hello"); + expect(new TextDecoder().decode(result)).toBe("hello"); + }); + + test("reads from a file:// path", async () => { + mkdirSync(TMP, { recursive: true }); + const path = join(TMP, "key.txt"); + writeFileSync(path, "file-content"); + try { + const result = await readSource(`file://${path}`); + expect(new TextDecoder().decode(result)).toBe("file-content"); + } finally { + rmSync(TMP, { recursive: true, force: true }); + } + }); + + test("throws on unreadable file:// path", async () => { + await expect(readSource("file:///nonexistent/path.txt")).rejects.toThrow( + "unable to read source file", + ); + }); + + test("reads from stdin when source is -", async () => { + const stdin = fakeStdin("stdin-content"); + const result = await readSource("-", stdin); + expect(new TextDecoder().decode(result)).toBe("stdin-content"); + }); + + test("throws when stdin is - but no stdin available", async () => { + await expect(readSource("-")).rejects.toThrow("stdin is not available"); + }); +}); + +describe("readSourceText", () => { + test("returns inline value as string", async () => { + expect(await readSourceText("my-key")).toBe("my-key"); + }); + + test("reads file and returns string", async () => { + mkdirSync(TMP, { recursive: true }); + const path = join(TMP, "secret.txt"); + writeFileSync(path, "secret-from-file"); + try { + expect(await readSourceText(`file://${path}`)).toBe("secret-from-file"); + } finally { + rmSync(TMP, { recursive: true, force: true }); + } + }); + + test("reads stdin and returns string", async () => { + const stdin = fakeStdin("piped-secret"); + expect(await readSourceText("-", stdin)).toBe("piped-secret"); + }); +}); diff --git a/src/handlers/source.tsx b/src/handlers/source.tsx new file mode 100644 index 000000000..66ed27c5c --- /dev/null +++ b/src/handlers/source.tsx @@ -0,0 +1,46 @@ +import { readFile } from "node:fs/promises"; +import { buffer } from "node:stream/consumers"; +import { addAbortSignal } from "node:stream"; + +// readSource resolves a source-aware flag value to its raw bytes. +// Supports three forms: +// - "file://path" reads the file at path +// - "-" reads stdin to EOF +// - anything else is returned as-is (inline value encoded as UTF-8) +export async function readSource( + source: string, + stdin?: NodeJS.ReadStream, + signal?: AbortSignal, +): Promise { + signal?.throwIfAborted(); + + if (source.startsWith("file://")) { + const path = source.slice("file://".length); + try { + return await readFile(path, { signal }); + } catch (error) { + if ((error as Error)?.name === "AbortError") throw error; + throw new TypeError(`unable to read source file: ${path}`); + } + } + + if (source === "-") { + if (!stdin) { + throw new TypeError("stdin is not available for this source"); + } + const stream = signal ? addAbortSignal(signal, stdin) : stdin; + return buffer(stream); + } + + return new TextEncoder().encode(source); +} + +// readSourceText resolves a source-aware flag value to a string. +export async function readSourceText( + source: string, + stdin?: NodeJS.ReadStream, + signal?: AbortSignal, +): Promise { + const bytes = await readSource(source, stdin, signal); + return new TextDecoder().decode(bytes); +} diff --git a/src/testing/TestCoreClient.tsx b/src/testing/TestCoreClient.tsx index 531c2df07..78c5d5898 100644 --- a/src/testing/TestCoreClient.tsx +++ b/src/testing/TestCoreClient.tsx @@ -36,7 +36,11 @@ import type { } from "@aws-sdk/client-bedrock-agentcore"; import type { Core } from "../handlers/types"; import type { CoreHarnessClient, CreateHarnessInput } from "../handlers/harness/types"; -import type { CoreIdentityClient } from "../handlers/identity/types"; +import type { + CoreIdentityClient, + CreateApiKeyCredentialProviderInput, + UpdateApiKeyCredentialProviderInput, +} from "../handlers/identity/types"; import type { CoreRuntimeClient } from "../handlers/runtime/types"; import type { CoreOptions } from "../core/types"; import type { ProjectManager } from "../handlers/project/types"; @@ -568,8 +572,7 @@ type TestCoreClientOptions = { class TestIdentityClient implements CoreIdentityClient { async createApiKeyCredentialProvider( - _name: string, - _apiKey: string, + _input: CreateApiKeyCredentialProviderInput, _options: CoreOptions, ): Promise { return DEFAULT_CREATE_API_KEY_RESPONSE; @@ -591,8 +594,7 @@ class TestIdentityClient implements CoreIdentityClient { } async updateApiKeyCredentialProvider( - _name: string, - _apiKey: string, + _input: UpdateApiKeyCredentialProviderInput, _options: CoreOptions, ): Promise { return DEFAULT_UPDATE_API_KEY_RESPONSE; From 912131e2a0f5e6087e1d6516a7109a50ef54a552 Mon Sep 17 00:00:00 2001 From: Nicolas Borges Date: Fri, 24 Jul 2026 12:22:04 -0400 Subject: [PATCH 2/4] fix: support key-value pair and JSON input for tags flag --- .../create/index.tsx | 7 ++- src/handlers/source.tsx | 21 ++++---- src/handlers/utils.test.tsx | 54 +++++++++++++++++++ src/handlers/utils.tsx | 35 ++++++++++++ 4 files changed, 104 insertions(+), 13 deletions(-) create mode 100644 src/handlers/utils.test.tsx diff --git a/src/handlers/identity/api-key-credential-provider/create/index.tsx b/src/handlers/identity/api-key-credential-provider/create/index.tsx index 237a68ef3..80b9734ab 100644 --- a/src/handlers/identity/api-key-credential-provider/create/index.tsx +++ b/src/handlers/identity/api-key-credential-provider/create/index.tsx @@ -2,9 +2,8 @@ import z from "zod"; import { createHandler, flag } from "../../../../router"; import { JsonRendererKey } from "../../../../tui"; import type { AppIO, Core } from "../../../types"; -import { coreOptsFromCtx } from "../../../utils"; +import { coreOptsFromCtx, parseTags } from "../../../utils"; import { readSourceText } from "../../../source"; -import { parseJsonFlag } from "../../../utils"; export const createCreateApiKeyCredentialProviderHandler = (core: Core, io: AppIO) => createHandler({ @@ -21,7 +20,7 @@ export const createCreateApiKeyCredentialProviderHandler = (core: Core, io: AppI "JSON key containing the API key in the secret", z.string().optional(), ), - flag("tags", "tags as key=value (repeatable) or JSON object", z.string().optional()), + flag("tags", "tags as key=value (repeatable) or JSON object", z.array(z.string()).optional()), ], handle: async (ctx, flags) => { if (!flags.name) { @@ -56,7 +55,7 @@ export const createCreateApiKeyCredentialProviderHandler = (core: Core, io: AppI const apiKeySecretSource = hasApiKey ? "MANAGED" : "EXTERNAL"; - const tags = parseJsonFlag>("tags", flags.tags); + const tags = parseTags(flags.tags); ctx.require(JsonRendererKey).renderJson( await core.identity.createApiKeyCredentialProvider( diff --git a/src/handlers/source.tsx b/src/handlers/source.tsx index 66ed27c5c..f550b8eb0 100644 --- a/src/handlers/source.tsx +++ b/src/handlers/source.tsx @@ -19,20 +19,19 @@ export async function readSource( try { return await readFile(path, { signal }); } catch (error) { - if ((error as Error)?.name === "AbortError") throw error; + if (error instanceof Error && error.name === "AbortError") throw error; throw new TypeError(`unable to read source file: ${path}`); } } - if (source === "-") { - if (!stdin) { - throw new TypeError("stdin is not available for this source"); - } - const stream = signal ? addAbortSignal(signal, stdin) : stdin; - return buffer(stream); + if (source !== "-") { + return new TextEncoder().encode(source); + } + if (!stdin) { + throw new TypeError("stdin is not available for this source"); } - return new TextEncoder().encode(source); + return buffer(signal ? addAbortSignal(signal, stdin) : stdin); } // readSourceText resolves a source-aware flag value to a string. @@ -42,5 +41,9 @@ export async function readSourceText( signal?: AbortSignal, ): Promise { const bytes = await readSource(source, stdin, signal); - return new TextDecoder().decode(bytes); + try { + return new TextDecoder("utf-8", { fatal: true }).decode(bytes); + } catch { + throw new TypeError("source must contain valid UTF-8"); + } } diff --git a/src/handlers/utils.test.tsx b/src/handlers/utils.test.tsx new file mode 100644 index 000000000..b9d5a384a --- /dev/null +++ b/src/handlers/utils.test.tsx @@ -0,0 +1,54 @@ +import { describe, expect, test } from "bun:test"; +import { parseTags } from "./utils"; + +describe("parseTags", () => { + test("returns undefined for undefined input", () => { + expect(parseTags(undefined)).toBeUndefined(); + }); + + test("returns undefined for empty array", () => { + expect(parseTags([])).toBeUndefined(); + }); + + test("parses a single key=value pair", () => { + expect(parseTags(["env=prod"])).toEqual({ env: "prod" }); + }); + + test("parses multiple key=value pairs", () => { + expect(parseTags(["env=prod", "team=agentcore"])).toEqual({ + env: "prod", + team: "agentcore", + }); + }); + + test("handles values containing equals signs", () => { + expect(parseTags(["config=a=b=c"])).toEqual({ config: "a=b=c" }); + }); + + test("parses a JSON object", () => { + expect(parseTags(['{"env":"prod","team":"agentcore"}'])).toEqual({ + env: "prod", + team: "agentcore", + }); + }); + + test("rejects invalid JSON", () => { + expect(() => parseTags(["{not json}"])).toThrow("Invalid JSON"); + }); + + test("rejects JSON array (not an object)", () => { + expect(() => parseTags(['["a","b"]'])).toThrow("expected key=value"); + }); + + test("rejects JSON with non-string values", () => { + expect(() => parseTags(['{"count":42}'])).toThrow("must be a string, got number"); + }); + + test("rejects key=value without a key", () => { + expect(() => parseTags(["=value"])).toThrow("expected key=value"); + }); + + test("rejects bare value without equals", () => { + expect(() => parseTags(["noequals"])).toThrow("expected key=value"); + }); +}); diff --git a/src/handlers/utils.tsx b/src/handlers/utils.tsx index 89a79d2e7..245af9e63 100644 --- a/src/handlers/utils.tsx +++ b/src/handlers/utils.tsx @@ -28,3 +28,38 @@ export function parseJsonFlag(name: string, raw: string | undefined): T | und ); } } + +// parseTags parses a tags flag that accepts two mutually exclusive forms: +// - Repeated key=value shorthand: ["env=prod", "team=foo"] +// - A single JSON object: ['{"env":"prod","team":"foo"}'] +// The two forms cannot be mixed. Returns undefined when the input is empty. +export function parseTags(values: string[] | undefined): Record | undefined { + if (!values || values.length === 0) return undefined; + + const first = values[0]; + if (values.length === 1 && first?.trimStart().startsWith("{")) { + const parsed = parseJsonFlag>("tags", first); + if (parsed === undefined) return undefined; + if (Array.isArray(parsed)) { + throw new TypeError("--tags JSON must be an object of string key-value pairs"); + } + for (const [key, value] of Object.entries(parsed)) { + if (typeof value !== "string") { + throw new TypeError(`--tags value for key '${key}' must be a string, got ${typeof value}`); + } + } + return parsed as Record; + } + + const result: Record = {}; + for (const entry of values) { + const eqIndex = entry.indexOf("="); + if (eqIndex < 1) { + throw new TypeError( + `Invalid tag '${entry}': expected key=value format or a single JSON object`, + ); + } + result[entry.slice(0, eqIndex)] = entry.slice(eqIndex + 1); + } + return result; +} From 970b581046eb56b302563ef7ba52a2ae6cff08a9 Mon Sep 17 00:00:00 2001 From: Nicolas Borges Date: Fri, 24 Jul 2026 12:56:02 -0400 Subject: [PATCH 3/4] fix: update existing secrets flags to single structured ref flag --- src/core/identity.tsx | 23 ++---- ...ntialProviderCommand.83b094791cea0052.json | 2 +- ...ntialProviderCommand.dfed014f440c1884.json | 2 +- ...ntialProviderCommand.b0c07380332f4673.json | 6 +- ...tialProvidersCommand.23f97c9dcdd6350b.json | 8 +-- ...tialProvidersCommand.6e1f31dcdf053d87.json | 4 +- ...tialProvidersCommand.7d2e22c637f6b633.json | 4 +- ...ntialProviderCommand.a779aef2084d562f.json | 6 +- .../__fixtures__/create-2.golden.json | 2 +- .../identity/__fixtures__/create.golden.json | 2 +- .../identity/__fixtures__/get.golden.json | 6 +- .../__fixtures__/list-page-1.golden.json | 4 +- .../__fixtures__/list-page-2.golden.json | 4 +- .../identity/__fixtures__/list.golden.json | 8 +-- .../identity/__fixtures__/update.golden.json | 6 +- .../create/index.tsx | 33 +++------ .../parser.test.tsx | 72 +++++++++++++++++++ .../api-key-credential-provider/parser.tsx | 35 +++++++++ .../update/index.tsx | 44 ++++++------ src/handlers/identity/identity.test.tsx | 42 +++++------ src/handlers/identity/types.tsx | 19 ++--- 21 files changed, 202 insertions(+), 130 deletions(-) create mode 100644 src/handlers/identity/api-key-credential-provider/parser.test.tsx create mode 100644 src/handlers/identity/api-key-credential-provider/parser.tsx diff --git a/src/core/identity.tsx b/src/core/identity.tsx index d6aa9dd3a..0c50f7602 100644 --- a/src/core/identity.tsx +++ b/src/core/identity.tsx @@ -25,15 +25,9 @@ export class IdentityClient implements CoreIdentityClient { input: CreateApiKeyCredentialProviderInput, options: CoreOptions, ): Promise { - return this.clients.control(toClientConfig(options)).send( - new CreateApiKeyCredentialProviderCommand({ - name: input.name, - apiKey: input.apiKey, - apiKeySecretConfig: input.apiKeySecretConfig, - apiKeySecretSource: input.apiKeySecretSource, - tags: input.tags, - }), - ); + return this.clients + .control(toClientConfig(options)) + .send(new CreateApiKeyCredentialProviderCommand(input)); } async getApiKeyCredentialProvider( @@ -59,14 +53,9 @@ export class IdentityClient implements CoreIdentityClient { input: UpdateApiKeyCredentialProviderInput, options: CoreOptions, ): Promise { - return this.clients.control(toClientConfig(options)).send( - new UpdateApiKeyCredentialProviderCommand({ - name: input.name, - apiKey: input.apiKey, - apiKeySecretConfig: input.apiKeySecretConfig, - apiKeySecretSource: input.apiKeySecretSource, - }), - ); + return this.clients + .control(toClientConfig(options)) + .send(new UpdateApiKeyCredentialProviderCommand(input)); } async deleteApiKeyCredentialProvider( diff --git a/src/handlers/identity/__fixtures__/CreateApiKeyCredentialProviderCommand.83b094791cea0052.json b/src/handlers/identity/__fixtures__/CreateApiKeyCredentialProviderCommand.83b094791cea0052.json index 1d407c18f..47982402a 100644 --- a/src/handlers/identity/__fixtures__/CreateApiKeyCredentialProviderCommand.83b094791cea0052.json +++ b/src/handlers/identity/__fixtures__/CreateApiKeyCredentialProviderCommand.83b094791cea0052.json @@ -1,6 +1,6 @@ { "apiKeySecretArn": { - "secretArn": "arn:aws:secretsmanager:us-west-2:685197708687:secret:bedrock-agentcore-identity!default/apikey/agentcore-cli-identity-fixture-ceac1e1d-2BRhkE" + "secretArn": "arn:aws:secretsmanager:us-west-2:685197708687:secret:bedrock-agentcore-identity!default/apikey/agentcore-cli-identity-fixture-21072164-GDCjeF" }, "name": "agentcore-cli-identity-fixture", "credentialProviderArn": "arn:aws:bedrock-agentcore:us-west-2:685197708687:token-vault/default/apikeycredentialprovider/agentcore-cli-identity-fixture", diff --git a/src/handlers/identity/__fixtures__/CreateApiKeyCredentialProviderCommand.dfed014f440c1884.json b/src/handlers/identity/__fixtures__/CreateApiKeyCredentialProviderCommand.dfed014f440c1884.json index 82c564b13..634f2c708 100644 --- a/src/handlers/identity/__fixtures__/CreateApiKeyCredentialProviderCommand.dfed014f440c1884.json +++ b/src/handlers/identity/__fixtures__/CreateApiKeyCredentialProviderCommand.dfed014f440c1884.json @@ -1,6 +1,6 @@ { "apiKeySecretArn": { - "secretArn": "arn:aws:secretsmanager:us-west-2:685197708687:secret:bedrock-agentcore-identity!default/apikey/agentcore-cli-identity-fixture-2-56db9332-Jv9u5N" + "secretArn": "arn:aws:secretsmanager:us-west-2:685197708687:secret:bedrock-agentcore-identity!default/apikey/agentcore-cli-identity-fixture-2-2a7aabf1-MsSy03" }, "name": "agentcore-cli-identity-fixture-2", "credentialProviderArn": "arn:aws:bedrock-agentcore:us-west-2:685197708687:token-vault/default/apikeycredentialprovider/agentcore-cli-identity-fixture-2", diff --git a/src/handlers/identity/__fixtures__/GetApiKeyCredentialProviderCommand.b0c07380332f4673.json b/src/handlers/identity/__fixtures__/GetApiKeyCredentialProviderCommand.b0c07380332f4673.json index dfd54cf0e..0317d054a 100644 --- a/src/handlers/identity/__fixtures__/GetApiKeyCredentialProviderCommand.b0c07380332f4673.json +++ b/src/handlers/identity/__fixtures__/GetApiKeyCredentialProviderCommand.b0c07380332f4673.json @@ -1,14 +1,14 @@ { "apiKeySecretArn": { - "secretArn": "arn:aws:secretsmanager:us-west-2:685197708687:secret:bedrock-agentcore-identity!default/apikey/agentcore-cli-identity-fixture-ceac1e1d-2BRhkE" + "secretArn": "arn:aws:secretsmanager:us-west-2:685197708687:secret:bedrock-agentcore-identity!default/apikey/agentcore-cli-identity-fixture-21072164-GDCjeF" }, "name": "agentcore-cli-identity-fixture", "credentialProviderArn": "arn:aws:bedrock-agentcore:us-west-2:685197708687:token-vault/default/apikeycredentialprovider/agentcore-cli-identity-fixture", "createdTime": { - "$date": "2026-07-23T19:31:48.713Z" + "$date": "2026-07-24T17:00:22.115Z" }, "lastUpdatedTime": { - "$date": "2026-07-23T19:31:48.713Z" + "$date": "2026-07-24T17:00:22.115Z" }, "apiKeySecretSource": "MANAGED" } \ No newline at end of file diff --git a/src/handlers/identity/__fixtures__/ListApiKeyCredentialProvidersCommand.23f97c9dcdd6350b.json b/src/handlers/identity/__fixtures__/ListApiKeyCredentialProvidersCommand.23f97c9dcdd6350b.json index cbbd672ee..baf210512 100644 --- a/src/handlers/identity/__fixtures__/ListApiKeyCredentialProvidersCommand.23f97c9dcdd6350b.json +++ b/src/handlers/identity/__fixtures__/ListApiKeyCredentialProvidersCommand.23f97c9dcdd6350b.json @@ -4,20 +4,20 @@ "name": "agentcore-cli-identity-fixture", "credentialProviderArn": "arn:aws:bedrock-agentcore:us-west-2:685197708687:token-vault/default/apikeycredentialprovider/agentcore-cli-identity-fixture", "createdTime": { - "$date": "2026-07-23T19:31:48.713Z" + "$date": "2026-07-24T17:00:22.115Z" }, "lastUpdatedTime": { - "$date": "2026-07-23T19:31:48.713Z" + "$date": "2026-07-24T17:00:22.115Z" } }, { "name": "agentcore-cli-identity-fixture-2", "credentialProviderArn": "arn:aws:bedrock-agentcore:us-west-2:685197708687:token-vault/default/apikeycredentialprovider/agentcore-cli-identity-fixture-2", "createdTime": { - "$date": "2026-07-23T19:31:49.014Z" + "$date": "2026-07-24T17:00:22.404Z" }, "lastUpdatedTime": { - "$date": "2026-07-23T19:31:49.014Z" + "$date": "2026-07-24T17:00:22.404Z" } } ] diff --git a/src/handlers/identity/__fixtures__/ListApiKeyCredentialProvidersCommand.6e1f31dcdf053d87.json b/src/handlers/identity/__fixtures__/ListApiKeyCredentialProvidersCommand.6e1f31dcdf053d87.json index 0bde1e53f..551e34e22 100644 --- a/src/handlers/identity/__fixtures__/ListApiKeyCredentialProvidersCommand.6e1f31dcdf053d87.json +++ b/src/handlers/identity/__fixtures__/ListApiKeyCredentialProvidersCommand.6e1f31dcdf053d87.json @@ -4,10 +4,10 @@ "name": "agentcore-cli-identity-fixture-2", "credentialProviderArn": "arn:aws:bedrock-agentcore:us-west-2:685197708687:token-vault/default/apikeycredentialprovider/agentcore-cli-identity-fixture-2", "createdTime": { - "$date": "2026-07-23T19:31:49.014Z" + "$date": "2026-07-24T17:00:22.404Z" }, "lastUpdatedTime": { - "$date": "2026-07-23T19:31:49.014Z" + "$date": "2026-07-24T17:00:22.404Z" } } ] diff --git a/src/handlers/identity/__fixtures__/ListApiKeyCredentialProvidersCommand.7d2e22c637f6b633.json b/src/handlers/identity/__fixtures__/ListApiKeyCredentialProvidersCommand.7d2e22c637f6b633.json index 1aab12e1f..e22a6d263 100644 --- a/src/handlers/identity/__fixtures__/ListApiKeyCredentialProvidersCommand.7d2e22c637f6b633.json +++ b/src/handlers/identity/__fixtures__/ListApiKeyCredentialProvidersCommand.7d2e22c637f6b633.json @@ -4,10 +4,10 @@ "name": "agentcore-cli-identity-fixture", "credentialProviderArn": "arn:aws:bedrock-agentcore:us-west-2:685197708687:token-vault/default/apikeycredentialprovider/agentcore-cli-identity-fixture", "createdTime": { - "$date": "2026-07-23T19:31:48.713Z" + "$date": "2026-07-24T17:00:22.115Z" }, "lastUpdatedTime": { - "$date": "2026-07-23T19:31:48.713Z" + "$date": "2026-07-24T17:00:22.115Z" } } ], diff --git a/src/handlers/identity/__fixtures__/UpdateApiKeyCredentialProviderCommand.a779aef2084d562f.json b/src/handlers/identity/__fixtures__/UpdateApiKeyCredentialProviderCommand.a779aef2084d562f.json index dfd54cf0e..0317d054a 100644 --- a/src/handlers/identity/__fixtures__/UpdateApiKeyCredentialProviderCommand.a779aef2084d562f.json +++ b/src/handlers/identity/__fixtures__/UpdateApiKeyCredentialProviderCommand.a779aef2084d562f.json @@ -1,14 +1,14 @@ { "apiKeySecretArn": { - "secretArn": "arn:aws:secretsmanager:us-west-2:685197708687:secret:bedrock-agentcore-identity!default/apikey/agentcore-cli-identity-fixture-ceac1e1d-2BRhkE" + "secretArn": "arn:aws:secretsmanager:us-west-2:685197708687:secret:bedrock-agentcore-identity!default/apikey/agentcore-cli-identity-fixture-21072164-GDCjeF" }, "name": "agentcore-cli-identity-fixture", "credentialProviderArn": "arn:aws:bedrock-agentcore:us-west-2:685197708687:token-vault/default/apikeycredentialprovider/agentcore-cli-identity-fixture", "createdTime": { - "$date": "2026-07-23T19:31:48.713Z" + "$date": "2026-07-24T17:00:22.115Z" }, "lastUpdatedTime": { - "$date": "2026-07-23T19:31:48.713Z" + "$date": "2026-07-24T17:00:22.115Z" }, "apiKeySecretSource": "MANAGED" } \ No newline at end of file diff --git a/src/handlers/identity/__fixtures__/create-2.golden.json b/src/handlers/identity/__fixtures__/create-2.golden.json index 82c564b13..634f2c708 100644 --- a/src/handlers/identity/__fixtures__/create-2.golden.json +++ b/src/handlers/identity/__fixtures__/create-2.golden.json @@ -1,6 +1,6 @@ { "apiKeySecretArn": { - "secretArn": "arn:aws:secretsmanager:us-west-2:685197708687:secret:bedrock-agentcore-identity!default/apikey/agentcore-cli-identity-fixture-2-56db9332-Jv9u5N" + "secretArn": "arn:aws:secretsmanager:us-west-2:685197708687:secret:bedrock-agentcore-identity!default/apikey/agentcore-cli-identity-fixture-2-2a7aabf1-MsSy03" }, "name": "agentcore-cli-identity-fixture-2", "credentialProviderArn": "arn:aws:bedrock-agentcore:us-west-2:685197708687:token-vault/default/apikeycredentialprovider/agentcore-cli-identity-fixture-2", diff --git a/src/handlers/identity/__fixtures__/create.golden.json b/src/handlers/identity/__fixtures__/create.golden.json index 1d407c18f..47982402a 100644 --- a/src/handlers/identity/__fixtures__/create.golden.json +++ b/src/handlers/identity/__fixtures__/create.golden.json @@ -1,6 +1,6 @@ { "apiKeySecretArn": { - "secretArn": "arn:aws:secretsmanager:us-west-2:685197708687:secret:bedrock-agentcore-identity!default/apikey/agentcore-cli-identity-fixture-ceac1e1d-2BRhkE" + "secretArn": "arn:aws:secretsmanager:us-west-2:685197708687:secret:bedrock-agentcore-identity!default/apikey/agentcore-cli-identity-fixture-21072164-GDCjeF" }, "name": "agentcore-cli-identity-fixture", "credentialProviderArn": "arn:aws:bedrock-agentcore:us-west-2:685197708687:token-vault/default/apikeycredentialprovider/agentcore-cli-identity-fixture", diff --git a/src/handlers/identity/__fixtures__/get.golden.json b/src/handlers/identity/__fixtures__/get.golden.json index 7a27b5b13..d236d9e3f 100644 --- a/src/handlers/identity/__fixtures__/get.golden.json +++ b/src/handlers/identity/__fixtures__/get.golden.json @@ -1,10 +1,10 @@ { "apiKeySecretArn": { - "secretArn": "arn:aws:secretsmanager:us-west-2:685197708687:secret:bedrock-agentcore-identity!default/apikey/agentcore-cli-identity-fixture-ceac1e1d-2BRhkE" + "secretArn": "arn:aws:secretsmanager:us-west-2:685197708687:secret:bedrock-agentcore-identity!default/apikey/agentcore-cli-identity-fixture-21072164-GDCjeF" }, "name": "agentcore-cli-identity-fixture", "credentialProviderArn": "arn:aws:bedrock-agentcore:us-west-2:685197708687:token-vault/default/apikeycredentialprovider/agentcore-cli-identity-fixture", - "createdTime": "2026-07-23T19:31:48.713Z", - "lastUpdatedTime": "2026-07-23T19:31:48.713Z", + "createdTime": "2026-07-24T17:00:22.115Z", + "lastUpdatedTime": "2026-07-24T17:00:22.115Z", "apiKeySecretSource": "MANAGED" } \ No newline at end of file diff --git a/src/handlers/identity/__fixtures__/list-page-1.golden.json b/src/handlers/identity/__fixtures__/list-page-1.golden.json index 064555294..8b95a7631 100644 --- a/src/handlers/identity/__fixtures__/list-page-1.golden.json +++ b/src/handlers/identity/__fixtures__/list-page-1.golden.json @@ -3,8 +3,8 @@ { "name": "agentcore-cli-identity-fixture", "credentialProviderArn": "arn:aws:bedrock-agentcore:us-west-2:685197708687:token-vault/default/apikeycredentialprovider/agentcore-cli-identity-fixture", - "createdTime": "2026-07-23T19:31:48.713Z", - "lastUpdatedTime": "2026-07-23T19:31:48.713Z" + "createdTime": "2026-07-24T17:00:22.115Z", + "lastUpdatedTime": "2026-07-24T17:00:22.115Z" } ], "nextToken": "eyJBY2NvdW50QW5kVHlwZUdzaVBrIjp7InZhbHVlU3RyIjoiNjg1MTk3NzA4Njg3I0FQSV9LRVkiLCJhdHRyaWJ1dGVUeXBlIjoiUyJ9LCJTSyI6eyJ2YWx1ZVN0ciI6ImFnZW50Y29yZS1jbGktaWRlbnRpdHktZml4dHVyZSIsImF0dHJpYnV0ZVR5cGUiOiJTIn0sIlBLIjp7InZhbHVlU3RyIjoiQVBJX0tFWV9DUkVEX1BST1ZJREVSIzY4NTE5NzcwODY4NyNkZWZhdWx0I2FnZW50Y29yZS1jbGktaWRlbnRpdHktZml4dHVyZSIsImF0dHJpYnV0ZVR5cGUiOiJTIn19" diff --git a/src/handlers/identity/__fixtures__/list-page-2.golden.json b/src/handlers/identity/__fixtures__/list-page-2.golden.json index e8ba37a34..49b746a39 100644 --- a/src/handlers/identity/__fixtures__/list-page-2.golden.json +++ b/src/handlers/identity/__fixtures__/list-page-2.golden.json @@ -3,8 +3,8 @@ { "name": "agentcore-cli-identity-fixture-2", "credentialProviderArn": "arn:aws:bedrock-agentcore:us-west-2:685197708687:token-vault/default/apikeycredentialprovider/agentcore-cli-identity-fixture-2", - "createdTime": "2026-07-23T19:31:49.014Z", - "lastUpdatedTime": "2026-07-23T19:31:49.014Z" + "createdTime": "2026-07-24T17:00:22.404Z", + "lastUpdatedTime": "2026-07-24T17:00:22.404Z" } ] } \ No newline at end of file diff --git a/src/handlers/identity/__fixtures__/list.golden.json b/src/handlers/identity/__fixtures__/list.golden.json index 7125ee9c9..6ae3d954c 100644 --- a/src/handlers/identity/__fixtures__/list.golden.json +++ b/src/handlers/identity/__fixtures__/list.golden.json @@ -3,14 +3,14 @@ { "name": "agentcore-cli-identity-fixture", "credentialProviderArn": "arn:aws:bedrock-agentcore:us-west-2:685197708687:token-vault/default/apikeycredentialprovider/agentcore-cli-identity-fixture", - "createdTime": "2026-07-23T19:31:48.713Z", - "lastUpdatedTime": "2026-07-23T19:31:48.713Z" + "createdTime": "2026-07-24T17:00:22.115Z", + "lastUpdatedTime": "2026-07-24T17:00:22.115Z" }, { "name": "agentcore-cli-identity-fixture-2", "credentialProviderArn": "arn:aws:bedrock-agentcore:us-west-2:685197708687:token-vault/default/apikeycredentialprovider/agentcore-cli-identity-fixture-2", - "createdTime": "2026-07-23T19:31:49.014Z", - "lastUpdatedTime": "2026-07-23T19:31:49.014Z" + "createdTime": "2026-07-24T17:00:22.404Z", + "lastUpdatedTime": "2026-07-24T17:00:22.404Z" } ] } \ No newline at end of file diff --git a/src/handlers/identity/__fixtures__/update.golden.json b/src/handlers/identity/__fixtures__/update.golden.json index 7a27b5b13..d236d9e3f 100644 --- a/src/handlers/identity/__fixtures__/update.golden.json +++ b/src/handlers/identity/__fixtures__/update.golden.json @@ -1,10 +1,10 @@ { "apiKeySecretArn": { - "secretArn": "arn:aws:secretsmanager:us-west-2:685197708687:secret:bedrock-agentcore-identity!default/apikey/agentcore-cli-identity-fixture-ceac1e1d-2BRhkE" + "secretArn": "arn:aws:secretsmanager:us-west-2:685197708687:secret:bedrock-agentcore-identity!default/apikey/agentcore-cli-identity-fixture-21072164-GDCjeF" }, "name": "agentcore-cli-identity-fixture", "credentialProviderArn": "arn:aws:bedrock-agentcore:us-west-2:685197708687:token-vault/default/apikeycredentialprovider/agentcore-cli-identity-fixture", - "createdTime": "2026-07-23T19:31:48.713Z", - "lastUpdatedTime": "2026-07-23T19:31:48.713Z", + "createdTime": "2026-07-24T17:00:22.115Z", + "lastUpdatedTime": "2026-07-24T17:00:22.115Z", "apiKeySecretSource": "MANAGED" } \ No newline at end of file diff --git a/src/handlers/identity/api-key-credential-provider/create/index.tsx b/src/handlers/identity/api-key-credential-provider/create/index.tsx index 80b9734ab..10d1b1475 100644 --- a/src/handlers/identity/api-key-credential-provider/create/index.tsx +++ b/src/handlers/identity/api-key-credential-provider/create/index.tsx @@ -4,6 +4,7 @@ import { JsonRendererKey } from "../../../../tui"; import type { AppIO, Core } from "../../../types"; import { coreOptsFromCtx, parseTags } from "../../../utils"; import { readSourceText } from "../../../source"; +import { parseSecretReference } from "../parser"; export const createCreateApiKeyCredentialProviderHandler = (core: Core, io: AppIO) => createHandler({ @@ -14,10 +15,9 @@ export const createCreateApiKeyCredentialProviderHandler = (core: Core, io: AppI flag("api-key", "the API key value (inline, file://path, or -)", z.string().optional(), { sensitive: true, }), - flag("api-key-secret-arn", "existing Secrets Manager secret ARN", z.string().optional()), flag( - "api-key-secret-json-key", - "JSON key containing the API key in the secret", + "api-key-secret-reference", + 'external secret reference JSON: {"secretId":"","jsonKey":""}', z.string().optional(), ), flag("tags", "tags as key=value (repeatable) or JSON object", z.array(z.string()).optional()), @@ -28,33 +28,20 @@ export const createCreateApiKeyCredentialProviderHandler = (core: Core, io: AppI } const hasApiKey = flags["api-key"] !== undefined; - const hasSecretArn = flags["api-key-secret-arn"] !== undefined; - const hasSecretJsonKey = flags["api-key-secret-json-key"] !== undefined; + const hasSecretRef = flags["api-key-secret-reference"] !== undefined; - if (hasApiKey && (hasSecretArn || hasSecretJsonKey)) { - throw new TypeError( - "--api-key and --api-key-secret-arn/--api-key-secret-json-key are mutually exclusive", - ); + if (hasApiKey && hasSecretRef) { + throw new TypeError("--api-key and --api-key-secret-reference are mutually exclusive"); } - if (!hasApiKey && !hasSecretArn) { - throw new TypeError( - "either --api-key or --api-key-secret-arn and --api-key-secret-json-key are required", - ); - } - if (hasSecretArn !== hasSecretJsonKey) { - throw new TypeError( - "--api-key-secret-arn and --api-key-secret-json-key must be specified together", - ); + if (!hasApiKey && !hasSecretRef) { + throw new TypeError("either --api-key or --api-key-secret-reference is required"); } const apiKey = hasApiKey ? await readSourceText(flags["api-key"]!, io.stdin) : undefined; - - const apiKeySecretConfig = hasSecretArn - ? { secretId: flags["api-key-secret-arn"]!, jsonKey: flags["api-key-secret-json-key"]! } + const apiKeySecretConfig = hasSecretRef + ? parseSecretReference(flags["api-key-secret-reference"]!) : undefined; - const apiKeySecretSource = hasApiKey ? "MANAGED" : "EXTERNAL"; - const tags = parseTags(flags.tags); ctx.require(JsonRendererKey).renderJson( diff --git a/src/handlers/identity/api-key-credential-provider/parser.test.tsx b/src/handlers/identity/api-key-credential-provider/parser.test.tsx new file mode 100644 index 000000000..29d7b204b --- /dev/null +++ b/src/handlers/identity/api-key-credential-provider/parser.test.tsx @@ -0,0 +1,72 @@ +import { describe, expect, test } from "bun:test"; +import { parseSecretReference } from "./parser"; + +describe("parseSecretReference", () => { + test("parses a valid secret reference", () => { + const result = parseSecretReference( + '{"secretId":"arn:aws:secretsmanager:us-west-2:123:secret:my-secret","jsonKey":"apiKey"}', + ); + expect(result).toEqual({ + secretId: "arn:aws:secretsmanager:us-west-2:123:secret:my-secret", + jsonKey: "apiKey", + }); + }); + + test("rejects invalid JSON", () => { + expect(() => parseSecretReference("{not json}")).toThrow("Invalid JSON"); + }); + + test("rejects non-object input", () => { + expect(() => parseSecretReference('"just a string"')).toThrow("must be a JSON object"); + }); + + test("rejects array input", () => { + expect(() => parseSecretReference("[]")).toThrow("must be a JSON object"); + }); + + test("rejects missing secretId", () => { + expect(() => parseSecretReference('{"jsonKey":"apiKey"}')).toThrow('non-empty "secretId"'); + }); + + test("rejects missing jsonKey", () => { + expect(() => + parseSecretReference('{"secretId":"arn:aws:secretsmanager:us-west-2:123:secret:s"}'), + ).toThrow('non-empty "jsonKey"'); + }); + + test("rejects empty secretId", () => { + expect(() => parseSecretReference('{"secretId":"","jsonKey":"apiKey"}')).toThrow( + 'non-empty "secretId"', + ); + }); + + test("rejects empty jsonKey", () => { + expect(() => + parseSecretReference( + '{"secretId":"arn:aws:secretsmanager:us-west-2:123:secret:s","jsonKey":""}', + ), + ).toThrow('non-empty "jsonKey"'); + }); + + test("rejects unexpected fields", () => { + expect(() => + parseSecretReference( + '{"secretId":"arn:aws:secretsmanager:us-west-2:123:secret:s","jsonKey":"apiKey","extra":"bad"}', + ), + ).toThrow("unexpected fields: extra"); + }); + + test("rejects non-string secretId", () => { + expect(() => parseSecretReference('{"secretId":123,"jsonKey":"apiKey"}')).toThrow( + 'non-empty "secretId"', + ); + }); + + test("rejects non-string jsonKey", () => { + expect(() => + parseSecretReference( + '{"secretId":"arn:aws:secretsmanager:us-west-2:123:secret:s","jsonKey":true}', + ), + ).toThrow('non-empty "jsonKey"'); + }); +}); diff --git a/src/handlers/identity/api-key-credential-provider/parser.tsx b/src/handlers/identity/api-key-credential-provider/parser.tsx new file mode 100644 index 000000000..eb38dddf2 --- /dev/null +++ b/src/handlers/identity/api-key-credential-provider/parser.tsx @@ -0,0 +1,35 @@ +// Requires exactly { secretId, jsonKey } with non-empty string values. +export function parseSecretReference(raw: string): { secretId: string; jsonKey: string } { + let parsed: unknown; + try { + parsed = JSON.parse(raw); + } catch (error) { + throw new TypeError( + `Invalid JSON for --api-key-secret-reference: ${error instanceof Error ? error.message : String(error)}`, + ); + } + + if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) { + throw new TypeError( + '--api-key-secret-reference must be a JSON object with "secretId" and "jsonKey"', + ); + } + + const obj = parsed as Record; + const allowedKeys = new Set(["secretId", "jsonKey"]); + const unexpected = Object.keys(obj).filter((k) => !allowedKeys.has(k)); + if (unexpected.length > 0) { + throw new TypeError( + `--api-key-secret-reference contains unexpected fields: ${unexpected.join(", ")}`, + ); + } + + if (typeof obj.secretId !== "string" || obj.secretId.length === 0) { + throw new TypeError('--api-key-secret-reference requires a non-empty "secretId" string'); + } + if (typeof obj.jsonKey !== "string" || obj.jsonKey.length === 0) { + throw new TypeError('--api-key-secret-reference requires a non-empty "jsonKey" string'); + } + + return { secretId: obj.secretId, jsonKey: obj.jsonKey }; +} diff --git a/src/handlers/identity/api-key-credential-provider/update/index.tsx b/src/handlers/identity/api-key-credential-provider/update/index.tsx index e7d2e8ff2..fed6b543d 100644 --- a/src/handlers/identity/api-key-credential-provider/update/index.tsx +++ b/src/handlers/identity/api-key-credential-provider/update/index.tsx @@ -4,6 +4,7 @@ import { JsonRendererKey } from "../../../../tui"; import type { AppIO, Core } from "../../../types"; import { coreOptsFromCtx } from "../../../utils"; import { readSourceText } from "../../../source"; +import { parseSecretReference } from "../parser"; export const createUpdateApiKeyCredentialProviderHandler = (core: Core, io: AppIO) => createHandler({ @@ -14,10 +15,9 @@ export const createUpdateApiKeyCredentialProviderHandler = (core: Core, io: AppI flag("api-key", "the new API key value (inline, file://path, or -)", z.string().optional(), { sensitive: true, }), - flag("api-key-secret-arn", "existing Secrets Manager secret ARN", z.string().optional()), flag( - "api-key-secret-json-key", - "JSON key containing the API key in the secret", + "api-key-secret-reference", + 'external secret reference JSON: {"secretId":"","jsonKey":""}', z.string().optional(), ), ], @@ -27,42 +27,42 @@ export const createUpdateApiKeyCredentialProviderHandler = (core: Core, io: AppI } const hasApiKey = flags["api-key"] !== undefined; - const hasSecretArn = flags["api-key-secret-arn"] !== undefined; - const hasSecretJsonKey = flags["api-key-secret-json-key"] !== undefined; + const hasSecretRef = flags["api-key-secret-reference"] !== undefined; - if (hasApiKey && (hasSecretArn || hasSecretJsonKey)) { - throw new TypeError( - "--api-key and --api-key-secret-arn/--api-key-secret-json-key are mutually exclusive", - ); + if (hasApiKey && hasSecretRef) { + throw new TypeError("--api-key and --api-key-secret-reference are mutually exclusive"); } - if (!hasApiKey && !hasSecretArn) { - throw new TypeError( - "either --api-key or --api-key-secret-arn and --api-key-secret-json-key are required", - ); + if (!hasApiKey && !hasSecretRef) { + throw new TypeError("either --api-key or --api-key-secret-reference is required"); } - if (hasSecretArn !== hasSecretJsonKey) { + + const opts = coreOptsFromCtx(ctx); + const existing = await core.identity.getApiKeyCredentialProvider(flags.name, opts); + const existingSource = existing.apiKeySecretSource; + + if (hasApiKey && existingSource === "EXTERNAL") { throw new TypeError( - "--api-key-secret-arn and --api-key-secret-json-key must be specified together", + "this provider uses an external secret; use --api-key-secret-reference to update it", ); } + if (hasSecretRef && existingSource === "MANAGED") { + throw new TypeError("this provider uses a managed secret; use --api-key to update it"); + } const apiKey = hasApiKey ? await readSourceText(flags["api-key"]!, io.stdin) : undefined; - - const apiKeySecretConfig = hasSecretArn - ? { secretId: flags["api-key-secret-arn"]!, jsonKey: flags["api-key-secret-json-key"]! } + const apiKeySecretConfig = hasSecretRef + ? parseSecretReference(flags["api-key-secret-reference"]!) : undefined; - const apiKeySecretSource = hasApiKey ? "MANAGED" : "EXTERNAL"; - ctx.require(JsonRendererKey).renderJson( await core.identity.updateApiKeyCredentialProvider( { name: flags.name, apiKey, apiKeySecretConfig, - apiKeySecretSource: apiKeySecretSource as any, + apiKeySecretSource: existingSource, }, - coreOptsFromCtx(ctx), + opts, ), ); }, diff --git a/src/handlers/identity/identity.test.tsx b/src/handlers/identity/identity.test.tsx index 9f359e690..7c04997af 100644 --- a/src/handlers/identity/identity.test.tsx +++ b/src/handlers/identity/identity.test.tsx @@ -193,7 +193,7 @@ describe("api-key-credential-provider CRUDL", () => { [ "create --name only", ["identity", "api-key-credential-provider", "create", "--name", "x"], - /--api-key.*--api-key-secret-arn/, + /--api-key.*--api-key-secret-reference/, ], [ "create --api-key only", @@ -206,7 +206,7 @@ describe("api-key-credential-provider CRUDL", () => { [ "update --name only", ["identity", "api-key-credential-provider", "update", "--name", "x"], - /--api-key.*--api-key-secret-arn/, + /--api-key.*--api-key-secret-reference/, ], ["delete bare", ["identity", "api-key-credential-provider", "delete"], /--name/], ] as const)("rejects missing required flags for `%s`", async (_label, args, message) => { @@ -215,7 +215,7 @@ describe("api-key-credential-provider CRUDL", () => { test.each([ [ - "create: --api-key with --api-key-secret-arn", + "create: --api-key with --api-key-secret-reference", [ "identity", "api-key-credential-provider", @@ -224,53 +224,53 @@ describe("api-key-credential-provider CRUDL", () => { "x", "--api-key", "k", - "--api-key-secret-arn", - "arn:aws:secretsmanager:us-west-2:123:secret:s", + "--api-key-secret-reference", + '{"secretId":"arn:aws:secretsmanager:us-west-2:123:secret:s","jsonKey":"apiKey"}', ], /mutually exclusive/, ], [ - "create: --api-key-secret-arn without --api-key-secret-json-key", + "create: --api-key-secret-reference missing secretId", [ "identity", "api-key-credential-provider", "create", "--name", "x", - "--api-key-secret-arn", - "arn:aws:secretsmanager:us-west-2:123:secret:s", + "--api-key-secret-reference", + '{"jsonKey":"apiKey"}', ], - /must be specified together/, + /non-empty "secretId"/, ], [ - "update: --api-key with --api-key-secret-arn", + "create: --api-key-secret-reference with unexpected field", [ "identity", "api-key-credential-provider", - "update", + "create", "--name", "x", - "--api-key", - "k", - "--api-key-secret-arn", - "arn:aws:secretsmanager:us-west-2:123:secret:s", + "--api-key-secret-reference", + '{"secretId":"arn:aws:secretsmanager:us-west-2:123:secret:s","jsonKey":"apiKey","extra":"bad"}', ], - /mutually exclusive/, + /unexpected fields/, ], [ - "update: --api-key-secret-json-key without --api-key-secret-arn", + "update: --api-key with --api-key-secret-reference", [ "identity", "api-key-credential-provider", "update", "--name", "x", - "--api-key-secret-json-key", - "key", + "--api-key", + "k", + "--api-key-secret-reference", + '{"secretId":"arn:aws:secretsmanager:us-west-2:123:secret:s","jsonKey":"apiKey"}', ], - /--api-key.*--api-key-secret-arn/, + /mutually exclusive/, ], - ] as const)("rejects mutually exclusive flags for `%s`", async (_label, args, message) => { + ] as const)("rejects invalid secret input for `%s`", async (_label, args, message) => { expect(run([...args])).rejects.toThrow(message); }); diff --git a/src/handlers/identity/types.tsx b/src/handlers/identity/types.tsx index 3da60f717..7c11c75a1 100644 --- a/src/handlers/identity/types.tsx +++ b/src/handlers/identity/types.tsx @@ -1,27 +1,16 @@ import type { + CreateApiKeyCredentialProviderRequest, CreateApiKeyCredentialProviderResponse, DeleteApiKeyCredentialProviderResponse, GetApiKeyCredentialProviderResponse, ListApiKeyCredentialProvidersResponse, - SecretSourceType, + UpdateApiKeyCredentialProviderRequest, UpdateApiKeyCredentialProviderResponse, } from "@aws-sdk/client-bedrock-agentcore-control"; import type { CoreOptions } from "../../core/types"; -export interface CreateApiKeyCredentialProviderInput { - name: string; - apiKey?: string; - apiKeySecretConfig?: { secretId: string; jsonKey: string }; - apiKeySecretSource?: SecretSourceType; - tags?: Record; -} - -export interface UpdateApiKeyCredentialProviderInput { - name: string; - apiKey?: string; - apiKeySecretConfig?: { secretId: string; jsonKey: string }; - apiKeySecretSource?: SecretSourceType; -} +export type CreateApiKeyCredentialProviderInput = CreateApiKeyCredentialProviderRequest; +export type UpdateApiKeyCredentialProviderInput = UpdateApiKeyCredentialProviderRequest; export interface CoreIdentityClient { createApiKeyCredentialProvider( From 4fff3c75ada2ce1d3b6ae9b61a5f8c94a31667ea Mon Sep 17 00:00:00 2001 From: Nicolas Borges Date: Mon, 27 Jul 2026 12:09:27 -0400 Subject: [PATCH 4/4] chore: update identity handlers to use shared SourceResolver class --- .../create/index.tsx | 8 ++- .../update/index.tsx | 8 ++- src/handlers/source.test.tsx | 70 ------------------- src/handlers/source.tsx | 49 ------------- 4 files changed, 10 insertions(+), 125 deletions(-) delete mode 100644 src/handlers/source.test.tsx delete mode 100644 src/handlers/source.tsx diff --git a/src/handlers/identity/api-key-credential-provider/create/index.tsx b/src/handlers/identity/api-key-credential-provider/create/index.tsx index 10d1b1475..73389b42e 100644 --- a/src/handlers/identity/api-key-credential-provider/create/index.tsx +++ b/src/handlers/identity/api-key-credential-provider/create/index.tsx @@ -1,9 +1,10 @@ import z from "zod"; import { createHandler, flag } from "../../../../router"; import { JsonRendererKey } from "../../../../tui"; -import type { AppIO, Core } from "../../../types"; +import type { Core } from "../../../types"; +import type { AppIO } from "../../../../io"; import { coreOptsFromCtx, parseTags } from "../../../utils"; -import { readSourceText } from "../../../source"; +import { SourceResolver } from "../../../../io"; import { parseSecretReference } from "../parser"; export const createCreateApiKeyCredentialProviderHandler = (core: Core, io: AppIO) => @@ -37,7 +38,8 @@ export const createCreateApiKeyCredentialProviderHandler = (core: Core, io: AppI throw new TypeError("either --api-key or --api-key-secret-reference is required"); } - const apiKey = hasApiKey ? await readSourceText(flags["api-key"]!, io.stdin) : undefined; + const resolver = new SourceResolver({ stdin: io.stdin }); + const apiKey = await resolver.resolveText("api-key", flags["api-key"]); const apiKeySecretConfig = hasSecretRef ? parseSecretReference(flags["api-key-secret-reference"]!) : undefined; diff --git a/src/handlers/identity/api-key-credential-provider/update/index.tsx b/src/handlers/identity/api-key-credential-provider/update/index.tsx index fed6b543d..6c289f1cb 100644 --- a/src/handlers/identity/api-key-credential-provider/update/index.tsx +++ b/src/handlers/identity/api-key-credential-provider/update/index.tsx @@ -1,9 +1,10 @@ import z from "zod"; import { createHandler, flag } from "../../../../router"; import { JsonRendererKey } from "../../../../tui"; -import type { AppIO, Core } from "../../../types"; +import type { Core } from "../../../types"; +import type { AppIO } from "../../../../io"; import { coreOptsFromCtx } from "../../../utils"; -import { readSourceText } from "../../../source"; +import { SourceResolver } from "../../../../io"; import { parseSecretReference } from "../parser"; export const createUpdateApiKeyCredentialProviderHandler = (core: Core, io: AppIO) => @@ -49,7 +50,8 @@ export const createUpdateApiKeyCredentialProviderHandler = (core: Core, io: AppI throw new TypeError("this provider uses a managed secret; use --api-key to update it"); } - const apiKey = hasApiKey ? await readSourceText(flags["api-key"]!, io.stdin) : undefined; + const resolver = new SourceResolver({ stdin: io.stdin }); + const apiKey = await resolver.resolveText("api-key", flags["api-key"]); const apiKeySecretConfig = hasSecretRef ? parseSecretReference(flags["api-key-secret-reference"]!) : undefined; diff --git a/src/handlers/source.test.tsx b/src/handlers/source.test.tsx deleted file mode 100644 index 7cec30da2..000000000 --- a/src/handlers/source.test.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import { describe, expect, test } from "bun:test"; -import { join } from "node:path"; -import { writeFileSync, mkdirSync, rmSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { Readable } from "node:stream"; -import { readSource, readSourceText } from "./source"; - -const TMP = join(tmpdir(), "source-test-" + Date.now()); - -function fakeStdin(content: string): NodeJS.ReadStream { - const stream = Readable.from([Buffer.from(content)]); - return stream as unknown as NodeJS.ReadStream; -} - -describe("readSource", () => { - test("returns inline value as UTF-8 bytes", async () => { - const result = await readSource("hello"); - expect(new TextDecoder().decode(result)).toBe("hello"); - }); - - test("reads from a file:// path", async () => { - mkdirSync(TMP, { recursive: true }); - const path = join(TMP, "key.txt"); - writeFileSync(path, "file-content"); - try { - const result = await readSource(`file://${path}`); - expect(new TextDecoder().decode(result)).toBe("file-content"); - } finally { - rmSync(TMP, { recursive: true, force: true }); - } - }); - - test("throws on unreadable file:// path", async () => { - await expect(readSource("file:///nonexistent/path.txt")).rejects.toThrow( - "unable to read source file", - ); - }); - - test("reads from stdin when source is -", async () => { - const stdin = fakeStdin("stdin-content"); - const result = await readSource("-", stdin); - expect(new TextDecoder().decode(result)).toBe("stdin-content"); - }); - - test("throws when stdin is - but no stdin available", async () => { - await expect(readSource("-")).rejects.toThrow("stdin is not available"); - }); -}); - -describe("readSourceText", () => { - test("returns inline value as string", async () => { - expect(await readSourceText("my-key")).toBe("my-key"); - }); - - test("reads file and returns string", async () => { - mkdirSync(TMP, { recursive: true }); - const path = join(TMP, "secret.txt"); - writeFileSync(path, "secret-from-file"); - try { - expect(await readSourceText(`file://${path}`)).toBe("secret-from-file"); - } finally { - rmSync(TMP, { recursive: true, force: true }); - } - }); - - test("reads stdin and returns string", async () => { - const stdin = fakeStdin("piped-secret"); - expect(await readSourceText("-", stdin)).toBe("piped-secret"); - }); -}); diff --git a/src/handlers/source.tsx b/src/handlers/source.tsx deleted file mode 100644 index f550b8eb0..000000000 --- a/src/handlers/source.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { readFile } from "node:fs/promises"; -import { buffer } from "node:stream/consumers"; -import { addAbortSignal } from "node:stream"; - -// readSource resolves a source-aware flag value to its raw bytes. -// Supports three forms: -// - "file://path" reads the file at path -// - "-" reads stdin to EOF -// - anything else is returned as-is (inline value encoded as UTF-8) -export async function readSource( - source: string, - stdin?: NodeJS.ReadStream, - signal?: AbortSignal, -): Promise { - signal?.throwIfAborted(); - - if (source.startsWith("file://")) { - const path = source.slice("file://".length); - try { - return await readFile(path, { signal }); - } catch (error) { - if (error instanceof Error && error.name === "AbortError") throw error; - throw new TypeError(`unable to read source file: ${path}`); - } - } - - if (source !== "-") { - return new TextEncoder().encode(source); - } - if (!stdin) { - throw new TypeError("stdin is not available for this source"); - } - - return buffer(signal ? addAbortSignal(signal, stdin) : stdin); -} - -// readSourceText resolves a source-aware flag value to a string. -export async function readSourceText( - source: string, - stdin?: NodeJS.ReadStream, - signal?: AbortSignal, -): Promise { - const bytes = await readSource(source, stdin, signal); - try { - return new TextDecoder("utf-8", { fatal: true }).decode(bytes); - } catch { - throw new TypeError("source must contain valid UTF-8"); - } -}