From 7538666f8059ce465ec9813e38cf499860c12379 Mon Sep 17 00:00:00 2001 From: Nicolas Borges Date: Thu, 23 Jul 2026 16:33:01 -0400 Subject: [PATCH] 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 685acf94e..c34d06601 100644 --- a/src/handlers/identity/api-key-credential-provider/index.tsx +++ b/src/handlers/identity/api-key-credential-provider/index.tsx @@ -10,9 +10,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 2251ce286..175c8c42d 100644 --- a/src/handlers/identity/identity.test.tsx +++ b/src/handlers/identity/identity.test.tsx @@ -185,7 +185,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", @@ -198,13 +198,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;