/** * agentCoreIdentity — AWS Bedrock AgentCore Identity adapter (peer-dep * `@aws-sdk/client-bedrock-agentcore`). * * import { agentCoreIdentity } from 'agentfootprint/security'; * const credentials = agentCoreIdentity({ region: 'us-east-1' }); * * Maps the {@link CredentialProvider} port onto AgentCore Identity's * `GetResourceOauth2Token` (the SDK's `@requires_access_token` underneath): * - request.mode 'machine' → `M2M`; 'user' → `USER_FEDERATION` * - request.service → the configured OAuth2 credential-provider name * - request.identity → (per-request workload identity scoping; see below) * - request.userToken → the user's own JWT, exchanged for a user-scoped * workload token before vending (9.12.0; see below) * - a returned access token → `{ status: 'issued', credential: bearer(token) }` * - a returned auth URL → `{ status: 'authorization-required' }` (3LO consent) * * The token vault + refresh-token handling live in AgentCore, so repeat calls * usually return a token directly (no consent round-trip). * * **Per-request identity forwarding (workload identity scoping).** * `GetResourceOauth2Token` carries NO user/tenant field — in AgentCore the * user identity is bound EARLIER, at workload-token acquisition. There are two * ways to bind it, and this adapter dispatches both: * * `GetWorkloadAccessTokenForUserId(workloadName, userId)` — the ASSERTION. * The agent says who the user is; AWS takes its word for it and returns a * workload token keyed to that (workload, user). * `GetWorkloadAccessTokenForJWT(workloadName, userToken)` — the PROOF (9.12.0). * The agent hands over the token the user's own identity provider signed, * and gets back "an opaque token representing the identity of both the * workload and the user" (the service's own words for the response). * * Either way the answer is a `workloadAccessToken`, and it flows into * `GetResourceOauth2Token`'s `workloadIdentityToken` unchanged — which is why * the second one needed no new credential path, no new result branch and no * change to `toHeaders()`. AgentCore keys its token vault + 3LO grants per * (workload, user), so the whole point of resolving either is that the vault * entry belongs to the person rather than to the agent. * * **The JWT rides the REQUEST.** `req.userToken` is per call, because the * person calling is per call; a JWT in this provider's construction options * would be one user's session serving everybody. Presence is what selects the * exchange — a proof that arrived is never downgraded to an assertion — and * `requireUserToken` turns "no JWT on a `mode: 'user'` request" from a quiet * fallback into a refusal, for a deployment where every user IS authenticated. * * The ladder, in order, for one `getCredential(req)`: * 1. `req.userToken` present → exchange it (needs `workloadName`; `mode: 'user'`). * 2. else a userId derives from `req.identity` (default `identity.principal`; * override via `userIdFor`) on a `mode: 'user'` request, and * `options.workloadName` is configured → the by-userId exchange. * 3. else the static `options.workloadIdentityToken` flows exactly as before. * * `tenant` has no native AgentCore field and is NOT forwarded by default — * tenant isolation derives from the workload identity itself (per-tenant * workloads), or encode it via `userIdFor` (e.g. `${tenant}:${principal}`). * * **Secrecy.** The inbound JWT and the exchanged workload token are secrets of * equal weight, and neither appears in anything this file throws. A failed * exchange is described by the SHAPE of the response — how many fields it * carried and what they are called — never by its content, because every field * of a token-exchange response is a token. A thrown message reaches the LLM as * a tool result AND rides `agentfootprint.credential.failed`, so an error that * quotes the response has published the user's session to both at once. * * ── How this talks to the SDK (9.4.0 — read this before editing) ──────────── * Through **`client.send(new SomeCommand(input))`**, never through a method on * the client. A bare `@aws-sdk/client-*` **Client** is command-based: its * prototype carries `send` and `destroy` and NOTHING ELSE. The per-operation * shortcuts (`getResourceOauth2Token(...)`) live on the AGGREGATED client * (`BedrockAgentCore`), which is a different class. * * Until 9.4.0 this adapter built a `BedrockAgentCoreClient` and then duck-typed * `client.getResourceOauth2Token` — a method that is never there — so the * documented path failed 100% of the time on the very first call, with a * message blaming the SDK version. Every test injected `_client` and so never * touched the shim. The sibling memory adapter in this same package had the * command form right all along; this one now matches it, and * `test/adapters/aws/aws-command-pin.test.ts` pins the command names for both. * * Pattern: Adapter (GoF) + lazy peer-dep load — the AWS SDK is required only when * `getCredential` first runs (or never, if you inject `_client` / `_sdk`). */ import type { CredentialProvider } from '../../identity/types.js'; /** Raw result shape we consume from the AgentCore identity client. */ export interface AgentCoreOauthResponse { readonly accessToken?: string; readonly authorizationUrl?: string; /** Correlates a 3LO consent round-trip. The real service reports this as * `sessionUri`; the SDK shim renames it here. */ readonly sessionId?: string; /** Unix seconds. **AgentCore does not report one** — * `GetResourceOauth2TokenResponse` has no expiry field, so this is only ever * populated by an injected `_client` that knows one from elsewhere. */ readonly expiresAt?: number; } /** The minimal, operation-semantic surface the adapter calls — `GetResourceOauth2Token` * and (for per-user workload scoping) `GetWorkloadAccessTokenForUserId`. * `createIdentityClient` maps the real SDK's `send(new Command(...))` onto this; * tests and custom integrations inject a fake via `_client`. */ export interface AgentCoreIdentityClientLike { getResourceOauth2Token(input: { readonly resourceCredentialProviderName: string; readonly scopes: readonly string[]; readonly oauth2Flow: 'M2M' | 'USER_FEDERATION' | 'ON_BEHALF_OF_TOKEN_EXCHANGE'; readonly forceAuthentication: boolean; readonly workloadIdentityToken?: string; }): Promise; /** Optional — required only when `workloadName` is configured. Exchanges * (workloadName, userId) for a USER-SCOPED workload access token; AgentCore * keys its token vault + 3LO grants per (workload, user). */ getWorkloadAccessTokenForUserId?(input: { readonly workloadName: string; readonly userId: string; }): Promise<{ readonly workloadAccessToken?: string; }>; /** Optional — required only when a request carries `userToken`. Exchanges the * user's OWN IdP-issued JWT for a workload access token representing both the * workload and the user. Same answer shape as the by-userId exchange, which * is why both feed the same `workloadIdentityToken`. */ getWorkloadAccessTokenForJWT?(input: { readonly workloadName: string; readonly userToken: string; }): Promise<{ readonly workloadAccessToken?: string; }>; /** Optional — required only for services named in `apiKeyServices`. AgentCore * keeps API keys in the same vault as OAuth tokens, behind a different * operation, so this is a sibling of `getResourceOauth2Token` rather than a * mode of it. */ getResourceApiKey?(input: { readonly resourceCredentialProviderName: string; /** REQUIRED on the wire — `GetResourceApiKeyRequest` declares it non-optional. */ readonly workloadIdentityToken: string; }): Promise<{ readonly apiKey?: string; }>; /** Optional — required only by {@link completeAgentCoreAuthorization}. Tells * AgentCore that the person behind `sessionId` finished consenting, which is * what releases the token into the vault for the next vend. */ completeResourceTokenAuth?(input: { readonly sessionId: string; readonly userToken?: string; readonly userId?: string; }): Promise; } export interface AgentCoreIdentityOptions { readonly region?: string; /** The agent's workload access token (AgentCore Runtime injects one in-container; * supply it explicitly when running elsewhere). Used as-is unless a per-user * workload token is resolved (see `workloadName`). */ readonly workloadIdentityToken?: string; /** The AgentCore workload identity name — the OPT-IN for per-request identity * scoping, and the one option BOTH exchanges need. When set, a `mode: 'user'` * request resolves a per-user workload access token before vending — from * `req.userToken` via `GetWorkloadAccessTokenForJWT` when the user's own JWT * arrived, otherwise from `req.identity` via `GetWorkloadAccessTokenForUserId` * — so AgentCore's token vault + 3LO grants are keyed per (workload, user) * instead of per workload. Omit → today's static-token behavior. */ readonly workloadName?: string; /** * Refuse a `mode: 'user'` request that carries no `req.userToken` (9.12.0), * instead of falling back to the by-userId exchange or the static token. * * The opt-in for a deployment where the front door really does authenticate * every person — an AgentCore Runtime behind JWT inbound auth, an API gateway * that validates a bearer token. There, a delegated request with no proof * attached is a wiring bug, and the failure it causes without this flag is the * quiet kind: a token gets vended, the call succeeds, and it was scoped to the * agent (or to a user id the agent asserted) rather than to the person. * * `mode: 'machine'` is never affected — M2M is the workload's own identity and * has no user to prove. */ readonly requireUserToken?: boolean; /** Map `req.identity` → the AgentCore `userId`. Default: `identity.principal`. * `tenant` has no native AgentCore field — encode it here if you need * tenant-scoped vault entries (e.g. ``({ tenant, principal }) => * tenant && principal ? `${tenant}:${principal}` : principal``). Return * `undefined` to skip per-user scoping for that request. */ readonly userIdFor?: (identity: { readonly principal?: string; readonly tenant?: string; }) => string | undefined; /** * How a `mode: 'user'` request gets its token (9.66.0). Default `'consent'`. * * `'consent'` — AgentCore's `USER_FEDERATION`. If the vault has no grant * for this (workload, user), the result is an * `authorization-required` with a URL to send the person to. * They approve once; later calls return a token directly. * `'exchange'` — AgentCore's `ON_BEHALF_OF_TOKEN_EXCHANGE`. The person's * own login is TRADED for a scoped downstream token, with no * consent screen at any point. * * `'exchange'` is not simply the nicer one. It works only where the * downstream provider was configured for it (an on-behalf-of exchange grant * on the credential provider) and where trading the user's session for * downstream access is a decision your organisation has already made — the * consent screen is what asks the person, and this flow is the deployment * saying it does not need to. Choose it deliberately. * * `mode: 'machine'` is unaffected: M2M has no user to act for. */ readonly userFlow?: 'consent' | 'exchange'; /** * Services whose credential is an API KEY, not an OAuth token (9.66.0). * * AgentCore's vault holds both kinds behind two different operations, and the * provider NAME alone does not say which — so a request for a service named * here is vended with `GetResourceApiKey` and comes back as an * {@link apiKey} credential. Everything else takes the OAuth path. * * There is no auto-detection on purpose: guessing would mean calling one * operation, catching a failure, and retrying with the other, which turns a * configuration mistake into two round-trips and an ambiguous error. */ readonly apiKeyServices?: readonly string[]; /** Header an API-key credential is sent in. Default `'x-api-key'`. */ readonly apiKeyHeader?: string; /** Stable provider id (default 'agentcore-identity'). */ readonly id?: string; /** Test seam — inject a client implementing {@link AgentCoreIdentityClientLike}. * Bypasses the SDK entirely; the field mapping below is then yours. */ readonly _client?: AgentCoreIdentityClientLike; /** @internal Test injection — the AWS SDK module, to exercise the real shim * (`send(new Command(...))`) with a fake SDK. */ readonly _sdk?: BedrockAgentCoreIdentitySdkModule; } /** The slice of `@aws-sdk/client-bedrock-agentcore` this shim touches. */ export interface BedrockAgentCoreIdentitySdkModule { readonly BedrockAgentCoreClient?: new (config: { region?: string; }) => { send(cmd: unknown): Promise; }; readonly GetResourceOauth2TokenCommand?: new (input: unknown) => unknown; readonly GetWorkloadAccessTokenForUserIdCommand?: new (input: unknown) => unknown; readonly GetWorkloadAccessTokenForJWTCommand?: new (input: unknown) => unknown; readonly GetResourceApiKeyCommand?: new (input: unknown) => unknown; readonly CompleteResourceTokenAuthCommand?: new (input: unknown) => unknown; } /** Build a {@link CredentialProvider} backed by AWS Bedrock AgentCore Identity. */ export declare function agentCoreIdentity(options?: AgentCoreIdentityOptions): CredentialProvider; /** Connection options for {@link completeAgentCoreAuthorization}. */ export interface CompleteAgentCoreAuthorizationOptions { /** The 3LO round-trip this completes — the `sessionId` from the * `authorization-required` result the agent returned. */ readonly sessionId: string; /** The person's own IdP-issued JWT, if your callback has it. Preferred: it is * the artifact their provider signed. */ readonly userToken?: string; /** The user id you asserted for them, when no JWT is available. Exactly one * of `userToken` / `userId` is required. */ readonly userId?: string; readonly region?: string; /** Test seam — same client surface the provider uses. */ readonly _client?: AgentCoreIdentityClientLike; /** @internal Test injection — the AWS SDK module. */ readonly _sdk?: BedrockAgentCoreIdentitySdkModule; } /** * Tell AgentCore the person finished consenting, so the token lands in the vault. * * ── Where this runs, and why it is not a provider method ───────────────────── * A 3LO consent has three actors and two processes. The agent asks for a * credential and gets back `authorization-required` with a URL and a session * id; the PERSON opens that URL in their browser and approves; AgentCore then * redirects their browser to a callback route **your web app** owns. That route * is where this belongs — a different process from the agent run, often a * different service — so it takes its own connection options rather than * pretending to be a method on a provider that route has never seen. * * Your route's job before calling this is the part nobody else can do: confirm * the browser session really belongs to the user you are about to name. This * function is the handshake, not the authentication. * * ── After it returns ───────────────────────────────────────────────────────── * Nothing is handed back — success is an empty acknowledgement. The token now * exists in the vault for that (workload, user), and the way to obtain it is to * run the agent's request again: the next `getCredential` for that service * returns `issued` where the last one returned `authorization-required`. In * agentfootprint terms the consent is a PAUSE, and this is what makes the * resume succeed. * * The authorization URL and its session are short-lived (AWS documents ten * minutes) — a person who wanders off has to start the consent again, and the * refusal you get is AgentCore's, not this adapter's. * * @example An Express-style callback route * app.get('/oauth/callback', async (req, res) => { * const user = await requireSignedInUser(req); // yours, and load-bearing * await completeAgentCoreAuthorization({ * sessionId: String(req.query.session), * userId: user.id, * region: 'us-east-1', * }); * res.send('Approved — you can return to the assistant.'); * }); */ export declare function completeAgentCoreAuthorization(options: CompleteAgentCoreAuthorizationOptions): Promise;