Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
3c09c23
feat: create credential providers before synthesizing a deploy
Aug 27, 2026
a5cd14b
test+fix: cover identity client, clear dropped credentials, honest en…
Aug 27, 2026
2c47266
test: cover the identity factory's found + secret-ARN mapping branches
Aug 27, 2026
0e76e9f
fix: use a valid Oauth2ProviderConfigInput in the no-secret-ARN test
Aug 27, 2026
10672be
fix: address review — env-key collisions, prereq order, no partial pr…
Aug 27, 2026
903abbc
test+fix: keep collision message wording, cover cross-type collision,…
Aug 27, 2026
d47f298
Merge refactor into feat/deploy-credential-providers
Aug 28, 2026
30fea4b
fix: fall back to legacy _CLIENT_ID env var for OAuth client id
Aug 28, 2026
c78add5
Merge refactor into feat/deploy-credential-providers
Aug 28, 2026
96a5887
refactor: provision credentials through the core Identity client
Aug 31, 2026
6ccd84e
fix(project): refuse credential names that shadow a credential field
Aug 31, 2026
1d49629
feat(project): push the current secret to an existing credential prov…
Aug 31, 2026
792e9f7
feat(project): read credential secrets from the deploy environment
Aug 31, 2026
b4bf5f1
feat(project): provision payment credential providers
Aug 31, 2026
0ce21b5
fix(project): undo credential providers a failed deploy created
Aug 31, 2026
35a3a20
Merge refactor into feat/deploy-credential-providers
Aug 31, 2026
b6454d7
refactor(core): key the client cache by credential identity, not a se…
Sep 1, 2026
138161e
refactor(core): require an Identity client on FsProjectManager
Sep 1, 2026
8ea52d3
test(project): assert credential provisioning against the account, no…
Sep 1, 2026
86b87fb
docs(project): say what the credential field-suffix guard protects
Sep 1, 2026
24d297b
fix(project): close three gaps in credential provisioning found in re…
Sep 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 49 additions & 1 deletion src/core/identity.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import {
CreateApiKeyCredentialProviderCommand,
CreateOauth2CredentialProviderCommand,
CreatePaymentCredentialProviderCommand,
DeleteApiKeyCredentialProviderCommand,
DeleteOauth2CredentialProviderCommand,
DeletePaymentCredentialProviderCommand,
GetApiKeyCredentialProviderCommand,
GetOauth2CredentialProviderCommand,
GetPaymentCredentialProviderCommand,
ListApiKeyCredentialProvidersCommand,
ListOauth2CredentialProvidersCommand,
UpdateApiKeyCredentialProviderCommand,
UpdateOauth2CredentialProviderCommand,
UpdatePaymentCredentialProviderCommand,
type CreateApiKeyCredentialProviderResponse,
type CreateOauth2CredentialProviderResponse,
type DeleteApiKeyCredentialProviderResponse,
Expand All @@ -19,19 +23,27 @@ import {
type ListOauth2CredentialProvidersResponse,
type UpdateApiKeyCredentialProviderResponse,
type UpdateOauth2CredentialProviderResponse,
type CreatePaymentCredentialProviderResponse,
type DeletePaymentCredentialProviderResponse,
type GetPaymentCredentialProviderResponse,
type UpdatePaymentCredentialProviderResponse,
} from "@aws-sdk/client-bedrock-agentcore-control";
import type {
CoreIdentityClient,
CreateApiKeyCredentialProviderInput,
CreateOauth2CredentialProviderInput,
CreatePaymentCredentialProviderInput,
UpdateApiKeyCredentialProviderInput,
UpdateOauth2CredentialProviderInput,
UpdatePaymentCredentialProviderInput,
} from "../handlers/identity/types";
import type { AwsClients, CoreOptions } from "./types";
import { toClientConfig } from "./utils";

export class IdentityClient implements CoreIdentityClient {
constructor(private readonly clients: AwsClients) {}
// Narrowed to `control` so any holder of a cached control client satisfies it;
// CoreClient passes itself.
constructor(private readonly clients: Pick<AwsClients, "control">) {}

async createApiKeyCredentialProvider(
input: CreateApiKeyCredentialProviderInput,
Expand Down Expand Up @@ -124,4 +136,40 @@ export class IdentityClient implements CoreIdentityClient {
.control(toClientConfig(options))
.send(new DeleteOauth2CredentialProviderCommand({ name }));
}

async createPaymentCredentialProvider(
input: CreatePaymentCredentialProviderInput,
options: CoreOptions,
): Promise<CreatePaymentCredentialProviderResponse> {
return this.clients
.control(toClientConfig(options))
.send(new CreatePaymentCredentialProviderCommand(input));
}

async getPaymentCredentialProvider(
name: string,
options: CoreOptions,
): Promise<GetPaymentCredentialProviderResponse> {
return this.clients
.control(toClientConfig(options))
.send(new GetPaymentCredentialProviderCommand({ name }));
}

async updatePaymentCredentialProvider(
input: UpdatePaymentCredentialProviderInput,
options: CoreOptions,
): Promise<UpdatePaymentCredentialProviderResponse> {
return this.clients
.control(toClientConfig(options))
.send(new UpdatePaymentCredentialProviderCommand(input));
}

async deletePaymentCredentialProvider(
name: string,
options: CoreOptions,
): Promise<DeletePaymentCredentialProviderResponse> {
return this.clients
.control(toClientConfig(options))
.send(new DeletePaymentCredentialProviderCommand({ name }));
}
}
79 changes: 46 additions & 33 deletions src/core/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { RuntimeClient } from "./runtime";
import { FsReadWriteJson } from "../io";
import type {
AwsClients,
AwsCredentials,
ClientConfig,
CoreFetch,
CreateCloudFormationClient,
Expand Down Expand Up @@ -54,10 +55,10 @@ type CoreClientConfig = {
// factories) and exposes feature-scoped sub-clients such as `harness`, keeping the
// surface modular as more features are added.
export class CoreClient implements AwsClients {
private controlClients = new Map<string, BedrockAgentCoreControlClient>();
private dataClients = new Map<string, BedrockAgentCoreClient>();
private iamClients = new Map<string, IAMClient>();
private logsClients = new Map<string, CloudWatchLogsClient>();
private controlClients = new ClientCache<BedrockAgentCoreControlClient>();
private dataClients = new ClientCache<BedrockAgentCoreClient>();
private iamClients = new ClientCache<IAMClient>();
private logsClients = new ClientCache<CloudWatchLogsClient>();

private readonly createControlClient: CreateControlClient;
private readonly createDataClient: CreateDataClient;
Expand Down Expand Up @@ -109,61 +110,73 @@ export class CoreClient implements AwsClients {
this.projectManager = new FsProjectManager({
logger: this.logger.child({ module: "projectManager" }),
createCloudFormationClient: config.createCloudFormationClient,
identity: this.identity,
});
this.describeBedrockAgent = config.describeBedrockAgent ?? describeBedrockAgent;
}

// control returns the control-plane client for `config`, creating and caching it
// on first use.
control(config: ClientConfig): BedrockAgentCoreControlClient {
const key = cacheKey(config);
let client = this.controlClients.get(key);
if (!client) {
client = this.createControlClient(config);
this.controlClients.set(key, client);
}
return client;
return this.controlClients.get(config, this.createControlClient);
}

// data returns the data-plane client for `config`, creating and caching it on
// first use.
data(config: ClientConfig): BedrockAgentCoreClient {
const key = cacheKey(config);
let client = this.dataClients.get(key);
if (!client) {
client = this.createDataClient(config);
this.dataClients.set(key, client);
}
return client;
return this.dataClients.get(config, this.createDataClient);
}

// iam returns the IAM client for `config`, creating and caching it on first
// use (used to provision default execution roles).
iam(config: ClientConfig): IAMClient {
const key = cacheKey(config);
let client = this.iamClients.get(key);
if (!client) {
client = this.createIamClient(config);
this.iamClients.set(key, client);
}
return client;
return this.iamClients.get(config, this.createIamClient);
}

// logs returns the CloudWatch Logs client for `config`, creating and caching it
// on first use (used to read batch-evaluation result log streams).
logs(config: ClientConfig): CloudWatchLogsClient {
const key = cacheKey(config);
let client = this.logsClients.get(key);
return this.logsClients.get(config, this.createLogsClient);
}
}

// ClientCache holds one SDK client per distinct configuration, so callers asking for
// the same region (and endpoint, and credentials) share a connection.
//
// Credentials cannot be part of a serialized key: they are either a provider function
// or an object of resolved credentials, and JSON.stringify drops a function silently.
// That would map two targets in the same region onto one client, and the second would
// then run with the first one's credentials. They are keyed by object identity in an
// outer WeakMap instead, with the serializable fields keyed inside it.
class ClientCache<T> {
private readonly withDefaultChain = new Map<string, T>();
private readonly byCredentials = new WeakMap<object, Map<string, T>>();

get(config: ClientConfig, create: (config: ClientConfig) => T): T {
const clients = this.forCredentials(config.credentials);
const key = configKey(config);
let client = clients.get(key);
if (!client) {
client = this.createLogsClient(config);
this.logsClients.set(key, client);
client = create(config);
clients.set(key, client);
}
return client;
}

private forCredentials(credentials: AwsCredentials | undefined): Map<string, T> {
if (!credentials) return this.withDefaultChain;
let clients = this.byCredentials.get(credentials);
if (!clients) {
clients = new Map();
this.byCredentials.set(credentials, clients);
}
return clients;
}
}

// cacheKey derives a stable cache key from a ClientConfig so that distinct
// configurations (region, endpoint, ...) map to distinct cached clients.
function cacheKey(config: ClientConfig): string {
return JSON.stringify(config);
// configKey names the fields that change how a client is constructed. It is built
// field by field rather than by serializing the config, so two callers that list the
// same fields in a different order still map to the same client.
function configKey({ region, endpoint }: ClientConfig): string {
return JSON.stringify([region, endpoint ?? null]);
}
Loading
Loading