import type { NullableHeaders } from './internal/headers'; import { buildHeaders } from './internal/headers'; import * as Errors from './core/error'; import { readEnv, loggerFor, rejectUnsupportedCredentials, type PlatformCredentials } from './internal/utils'; import { Anthropic, APIRequest, ClientOptions } from '@anthropic-ai/sdk/client'; export { BaseAnthropic } from '@anthropic-ai/sdk/client'; import * as Resources from '@anthropic-ai/sdk/resources/index'; import { VERSION } from '@anthropic-ai/sdk/version'; import { VERSION as PACKAGE_VERSION } from './version'; import { AwsCredentialIdentityProvider } from '@smithy/types'; import { loadConfig } from '@smithy/node-config-provider'; import { NODE_REGION_CONFIG_OPTIONS, NODE_REGION_CONFIG_FILE_OPTIONS } from '@smithy/config-resolver'; import { getAuthHeaders } from './core/auth'; import type { Middleware } from './core/middleware'; import { FinalRequestOptions } from './internal/request-options'; const DEFAULT_SERVICE_NAME = 'aws-external-anthropic'; const CREDENTIALS: PlatformCredentials = { accepts: ['apiKey', 'authToken'], unsupportedOptionMessage: (option) => `The \`${option}\` option is not supported by AnthropicAws; authentication uses an API key (\`apiKey\`) or AWS credentials (SigV4).`, }; export interface AwsClientOptions extends ClientOptions { /** * AWS region for the API gateway. * * Resolved by precedence: `awsRegion` arg > `AWS_REGION` env var > * `AWS_DEFAULT_REGION` env var > region from the AWS shared config file * (`~/.aws/config`) for the given `awsProfile` (or `[default]`). * * When resolution falls through to the config file, the region is loaded * asynchronously. It will be available after `await client.ready` or the * first request; until then `awsRegion` and `baseURL` are `undefined`. */ awsRegion?: string | undefined; /** * API key, sent as `Authorization: Bearer `. * * Takes precedence over AWS credential options. If neither `apiKey` nor * AWS credentials are provided, falls back to the `ANTHROPIC_AWS_API_KEY` * environment variable, then to `AWS_ACCESS_KEY_ID`/`AWS_SECRET_ACCESS_KEY` * from the environment, then to the default AWS credential chain. `null` * skips `ANTHROPIC_AWS_API_KEY`. * * Mutually exclusive with `authToken`. When `authToken` is passed, * `ANTHROPIC_AWS_API_KEY` is not read. */ apiKey?: string | null | undefined; /** * AWS access key ID for SigV4 authentication. * * Must be provided together with `awsSecretAccessKey`. */ awsAccessKey?: string | null | undefined; /** * AWS secret access key for SigV4 authentication. * * Must be provided together with `awsAccessKey`. */ awsSecretAccessKey?: string | null | undefined; /** * AWS session token for temporary credentials. */ awsSessionToken?: string | null | undefined; /** * AWS named profile for credential and region resolution. * * When set, credentials are loaded from the AWS credential chain using this * profile (even if `AWS_ACCESS_KEY_ID`/`AWS_SECRET_ACCESS_KEY` are set in the * environment), and the profile's `region` from `~/.aws/config` is used as a * fallback when no region is provided via arg or environment variable. */ awsProfile?: string | undefined; /** * Custom provider chain resolver for AWS credentials. * Useful for non-Node environments, like edge workers, where the default * credential provider chain may not work. */ providerChainResolver?: (() => Promise) | null; /** * Workspace ID sent on every request as the `anthropic-workspace-id` header. * * Resolved by precedence: `workspaceId` arg > `ANTHROPIC_AWS_WORKSPACE_ID` env var. */ workspaceId?: string | undefined; /** * Skip authentication for requests. This is useful when you have a gateway * or proxy that handles authentication on your behalf. * * @default false */ skipAuth?: boolean; } const noRegionError = () => new Errors.AnthropicError( 'No AWS region or base URL found. Set `awsRegion` in the constructor, the `AWS_REGION` / `AWS_DEFAULT_REGION` ' + 'environment variable, configure a `region` for your profile in `~/.aws/config`, or provide a `baseURL` / ' + '`ANTHROPIC_AWS_BASE_URL` environment variable.', ); /** API Client for interfacing with the Anthropic AWS API. */ export class AnthropicAws extends Anthropic { /** @internal */ override _instrumentationScope() { return { platform: 'aws', version: PACKAGE_VERSION }; } override userProfiles: WithoutActionURLs = withoutActionURLs( new Resources.UserProfiles(this), ); override beta: BetaResource = makeBetaResource(this); awsRegion: string | undefined; awsAccessKey: string | null; awsSecretAccessKey: string | null; awsSessionToken: string | null; awsProfile: string | null; providerChainResolver: (() => Promise) | null; workspaceId: string | undefined; skipAuth: boolean = false; /** * Resolves once the client is fully configured (region and base URL * resolved). Rejects if region resolution fails. Await this to fail fast on * misconfiguration instead of waiting for the first request. */ readonly ready: Promise; private _useSigV4: boolean; /** * API Client for interfacing with the Anthropic AWS API. * * Auth is resolved by precedence: `apiKey` constructor arg > explicit AWS * credentials > `awsProfile` > `ANTHROPIC_AWS_API_KEY` env var > * `AWS_ACCESS_KEY_ID`/`AWS_SECRET_ACCESS_KEY` env vars > default AWS * credential chain. The `credentials`, `config`, and `profile` options * throw an `AnthropicError`. * * @param {string | undefined} [opts.apiKey] - API key for Bearer token authentication. * @param {string | null | undefined} [opts.awsAccessKey] - AWS access key ID for SigV4 authentication. * @param {string | null | undefined} [opts.awsSecretAccessKey] - AWS secret access key for SigV4 authentication. * @param {string | null | undefined} [opts.awsSessionToken] - AWS session token for temporary credentials. * @param {string | undefined} [opts.awsProfile] - AWS named profile for credential and region resolution. * @param {string | undefined} [opts.awsRegion] - AWS region. Resolved by precedence: arg > `AWS_REGION` env > `AWS_DEFAULT_REGION` env > `~/.aws/config`. * @param {(() => Promise) | null} [opts.providerChainResolver] - Custom provider chain resolver for AWS credentials. * @param {string | undefined} [opts.workspaceId] - Workspace ID sent as `anthropic-workspace-id` header. Resolved by precedence: arg > `ANTHROPIC_AWS_WORKSPACE_ID` env var. * @param {string} [opts.baseURL=process.env['ANTHROPIC_AWS_BASE_URL'] ?? https://aws-external-anthropic.{awsRegion}.api.aws] - Override the default base URL for the API. * @param {number} [opts.timeout=10 minutes] - The maximum amount of time (in milliseconds) the client will wait for a response before timing out. * @param {MergedRequestInit} [opts.fetchOptions] - Additional `RequestInit` options to be passed to `fetch` calls. * @param {Fetch} [opts.fetch] - Specify a custom `fetch` function implementation. * @param {number} [opts.maxRetries=2] - The maximum number of times the client will retry a request. * @param {HeadersLike} opts.defaultHeaders - Default headers to include with every request to the API. * @param {Record} opts.defaultQuery - Default query parameters to include with every request to the API. * @param {boolean} [opts.dangerouslyAllowBrowser=false] - By default, client-side use of this library is not allowed, as it risks exposing your secret API credentials to attackers. * @param {boolean} [opts.skipAuth=false] - Skip authentication for requests. This is useful when you have a gateway or proxy that handles authentication on your behalf. */ constructor({ awsRegion, baseURL, apiKey, awsAccessKey = null, awsSecretAccessKey = null, awsSessionToken = null, awsProfile, providerChainResolver = null, workspaceId, skipAuth = false, ...opts }: AwsClientOptions = {}) { rejectUnsupportedCredentials(opts, CREDENTIALS); // Region resolution: arg > AWS_REGION env > AWS_DEFAULT_REGION env > ~/.aws/config (async). // The first three are resolved here; config-file fallback is kicked off below and // awaited on `ready` / first request. const syncRegion = awsRegion ?? readEnv('AWS_REGION') ?? readEnv('AWS_DEFAULT_REGION'); const explicitBaseURL = baseURL ?? readEnv('ANTHROPIC_AWS_BASE_URL'); let resolvedBaseURL: string | undefined; if (explicitBaseURL) { resolvedBaseURL = explicitBaseURL; } else if (syncRegion) { resolvedBaseURL = `https://aws-external-anthropic.${syncRegion}.api.aws`; } else { // No region known yet (or skipAuth) — will be resolved async from ~/.aws/config, // or is not needed at all. resolvedBaseURL = undefined; } // Precedence-based auth resolution: // 1. apiKey constructor arg // 2. awsAccessKey/awsSecretAccessKey constructor args (SigV4) // 3. awsProfile constructor arg (SigV4) // 4. ANTHROPIC_AWS_API_KEY env var, unless `apiKey` is `null`, which clears // the key as in `withOptions({ apiKey: null })`, or an `authToken` was // passed: the base client rejects a key together with a token // 5. Default AWS credential chain (SigV4) const hasExplicitApiKey = apiKey != null; const hasPartialAwsCreds = (awsAccessKey != null) !== (awsSecretAccessKey != null); if (hasPartialAwsCreds) { throw new Errors.AnthropicError( '`awsAccessKey` and `awsSecretAccessKey` must be provided together. You provided only one.', ); } const hasExplicitAwsCreds = awsAccessKey != null && awsSecretAccessKey != null; const hasAwsProfile = awsProfile != null; let resolvedApiKey: string | undefined; if (hasExplicitApiKey) { resolvedApiKey = apiKey; } else if (apiKey === undefined && !hasExplicitAwsCreds && !hasAwsProfile && opts.authToken == null) { resolvedApiKey = readEnv('ANTHROPIC_AWS_API_KEY') ?? undefined; } const resolvedWorkspaceId = workspaceId ?? readEnv('ANTHROPIC_AWS_WORKSPACE_ID'); if (!resolvedWorkspaceId && !skipAuth) { throw new Errors.AnthropicError( 'No workspace ID found. Set `workspaceId` in the constructor or the `ANTHROPIC_AWS_WORKSPACE_ID` environment variable.', ); } super({ // `null`, not `undefined`: `undefined` makes the base client fall back // to the ANTHROPIC_API_KEY / ANTHROPIC_AUTH_TOKEN env vars, which are // not auth sources for the AWS gateway — the key would flip // `withOptions()` clones out of SigV4 mode. An `authToken` passed in // `opts` (as the environment helpers' clones do) still wins here. apiKey: resolvedApiKey ?? null, authToken: null, baseURL: resolvedBaseURL, ...opts, // The workspace header is merged last (buildHeaders is last-wins): the // resolved `workspaceId` is authoritative, and on clones the inherited // `defaultHeaders` already contain the parent's merged-in workspace // header, which a `workspaceId` override must beat. defaultHeaders: buildHeaders([opts.defaultHeaders, { 'anthropic-workspace-id': resolvedWorkspaceId }]), }); // Only a caller-supplied base URL (arg or env) is an explicit choice; the // region-derived one must stay re-derivable so clones honour an // `awsRegion` override or an async-resolved region. this._baseURLIsExplicit = !!explicitBaseURL; this.awsRegion = syncRegion; this.awsAccessKey = awsAccessKey; this.awsSecretAccessKey = awsSecretAccessKey; this.awsSessionToken = awsSessionToken; this.awsProfile = awsProfile ?? null; this.providerChainResolver = providerChainResolver; this.workspaceId = resolvedWorkspaceId; this.skipAuth = skipAuth; this._useSigV4 = resolvedApiKey == null; // `withOptions()` rebuilds clones from `this._options`, but the base // constructor only recorded what `super()` received — the AWS-specific // options were destructured out. Re-record them so clones round-trip the // full AWS configuration. `baseURL` is recorded as the explicit value only // (`undefined` when region-derived), so clones re-derive their endpoint. const awsOptions: AwsClientOptions = { ...this._options, apiKey: resolvedApiKey ?? null, baseURL: explicitBaseURL, awsRegion, awsAccessKey, awsSecretAccessKey, awsSessionToken, awsProfile, providerChainResolver, workspaceId: resolvedWorkspaceId, skipAuth, }; this._options = awsOptions; if (syncRegion || explicitBaseURL || skipAuth) { this.ready = Promise.resolve(); } else { this.ready = this._resolveRegionFromConfig(awsProfile).then((region: string) => { this.awsRegion = region; this.baseURL = `https://aws-external-anthropic.${region}.api.aws`; }); // Suppress unhandledRejection; the error surfaces via `await ready` or the first request. this.ready.catch(() => {}); } } // Auth is SigV4 or an API key only; never resolve unrelated local Anthropic // credentials (which could also supply a base URL). protected override _shouldResolveDefaultCredentials(): boolean { return false; } /** * Create a new client instance re-using the same options given to the * current client with optional overriding, including the AWS-specific * options — e.g. `withOptions({ awsRegion: 'eu-west-1' })` re-derives the * endpoint for the new region. */ override withOptions(options: Partial): this { return super.withOptions({ // Pin the resolved region so clones don't re-resolve it (async config // lookup or env read); an `awsRegion` override still wins. awsRegion: this.awsRegion, ...options, }); } private _resolveRegionFromConfig(profile: string | undefined): Promise { return loadConfig( { ...NODE_REGION_CONFIG_OPTIONS, default: () => { throw noRegionError(); }, }, { ...NODE_REGION_CONFIG_FILE_OPTIONS, ...(profile && { profile }), }, )(); } protected override async prepareOptions(options: FinalRequestOptions): Promise { await super.prepareOptions(options); await this.ready; } protected override async authHeaders(opts: FinalRequestOptions): Promise { if (this.skipAuth) { return undefined; } if (!this._useSigV4) { // API key mode — use inherited API key auth return super.authHeaders(opts); } // SigV4 mode — auth is handled in prepareRequest since it needs the full // request. This intentionally supersedes any inherited or clone-supplied // `authToken` (e.g. the per-helper bearer that `copyClientForHelper` // sets): the gateway authenticates the SigV4 identity, so a bearer header // would only ride alongside the signature unused. return undefined; } protected override validateHeaders(): void { // Auth validation is handled in the constructor and the backend middleware } protected override getUserAgent(): string { return `AnthropicAws/JS ${VERSION}`; } protected override backendMiddleware(): ReadonlyArray { return [async (request, next) => next(await this.#signRequest(request))]; } /** * SigV4-signs the request. Runs inside the user middleware chain, so the * signature always covers the final, middleware-mutated request. */ async #signRequest(request: APIRequest): Promise { if (this.skipAuth || !this._useSigV4) { return request; } if (!this.awsRegion) { // Only reachable when an explicit baseURL was provided without a region. throw noRegionError(); } const headers = await getAuthHeaders(request, { url: request.url, regionName: this.awsRegion, serviceName: DEFAULT_SERVICE_NAME, awsAccessKey: this.awsAccessKey, awsSecretAccessKey: this.awsSecretAccessKey, awsSessionToken: this.awsSessionToken, awsProfile: this.awsProfile, providerChainResolver: this.providerChainResolver, logger: loggerFor(this), }); // Signed headers take precedence: the signature must match what goes // over the wire, so it can't be overridden by other header sources. return { ...request, headers: buildHeaders([request.headers, headers]).values }; } } /** * The AWS gateway serves user profiles but not their action URLs. */ type WithoutActionURLs = Resource & { actionURLs: never }; type BetaResource = Resources.Beta & { userProfiles: WithoutActionURLs }; function makeBetaResource(client: AnthropicAws): BetaResource { const resource = new Resources.Beta(client); withoutActionURLs(resource.userProfiles); return resource as BetaResource; } function withoutActionURLs(resource: Resource): WithoutActionURLs { Object.defineProperty(resource, 'actionURLs', { enumerable: false, get() { throw new Errors.AnthropicError('User profile action URLs are not available on `AnthropicAws`.'); }, }); return resource as WithoutActionURLs; }