-
Notifications
You must be signed in to change notification settings - Fork 5k
AWS default credential chain, SigV4-signed fetch, Bun.aws / Bun.gcp #39210
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Jarred-Sumner
wants to merge
16
commits into
main
Choose a base branch
from
claude/aws-credential-chain
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 8 commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
1a5e2e8
Add AWS default credential chain, SigV4 fetch signing and Bun.aws
Jarred-Sumner 1d4bc5b
Add Bun.gcp, fetch({gcp}), shared cloud credential cache, docs
Jarred-Sumner c270e59
Address review: profile precedence, per-thread caches, waiter fan-out…
Jarred-Sumner f345a6c
Address second review round: dedicated resolver thread, refresh throt…
Jarred-Sumner 3601115
Address third review round: teardown cancellation, S3 presign payload…
Jarred-Sumner 581a1d8
Bun.aws.fetch / Bun.gcp.fetch instead of fetch init options; relative…
Jarred-Sumner f5e26f8
Non-blocking credential resolution; AWSClient/GCPClient classes
Jarred-Sumner b968023
credential_process: drop dead branch; document the INI inline-comment…
Jarred-Sumner fdf3c15
AWSClient.eventStream(): decode application/vnd.amazon.eventstream
Jarred-Sumner 22476dd
eventstream: read response.body once (oxlint)
Jarred-Sumner 4a89065
Typed signing errors, shared form_encode; make shared-mock test asser…
Jarred-Sumner 0a14a14
Address review: host inference edge cases, atomic SSO cache write-bac…
Jarred-Sumner c327de3
Review follow-ups: scoped flight entry access, stricter metadata chec…
Jarred-Sumner bf7aa4e
Review follow-ups: signingDate bounds, signal precedence before defer…
Jarred-Sumner dc236bd
Review follow-ups: auth options from `{ url, ...init }`, stricter sig…
Jarred-Sumner 421968f
Deadline that fires while a raw request is still queued is a timeout;…
Jarred-Sumner File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,169 @@ | ||
| --- | ||
| title: AWS & Google Cloud auth | ||
| description: Call AWS and Google Cloud APIs from Bun with the machine's own credentials — no SDK required | ||
| --- | ||
|
|
||
| Bun can authenticate requests to AWS and Google Cloud the same way their CLIs and SDKs do: from environment variables, config files, SSO sessions, or the instance / container / pod the code is running on. Bun fetches credentials without blocking the JavaScript thread on the network, caches them, and refreshes ones that are in use in the background shortly before they expire. | ||
|
|
||
| ```ts | ||
| // SigV4-signed request with whatever credentials this machine has | ||
| const queues = await Bun.aws.fetch("https://sqs.us-east-1.amazonaws.com/?Action=ListQueues"); | ||
|
|
||
| // Bearer token from Application Default Credentials | ||
| const buckets = await Bun.gcp.fetch("https://storage.googleapis.com/storage/v1/b?project=my-project"); | ||
| ``` | ||
|
|
||
| `Bun.aws` and `Bun.gcp` are ready-made clients using ambient credentials. For several accounts, regions or key files at once, construct more: | ||
|
|
||
| ```ts | ||
| const prod = new Bun.AWSClient({ profile: "prod", region: "eu-west-1" }); | ||
| const deployer = new Bun.GCPClient({ keyFile: "/secrets/deployer.json" }); | ||
| ``` | ||
|
|
||
| `Bun.s3`, `new S3Client()` and `fetch("s3://…")` use the AWS chain automatically when no `accessKeyId`/`secretAccessKey` is configured — see [S3 › Credentials](/runtime/s3#credentials). | ||
|
|
||
| ## AWS | ||
|
|
||
| ### Where credentials come from | ||
|
|
||
| When a request needs AWS credentials and none were passed explicitly, Bun tries, in order: | ||
|
|
||
| 1. `AWS_ACCESS_KEY_ID` + `AWS_SECRET_ACCESS_KEY` (+ `AWS_SESSION_TOKEN`) | ||
| 2. The profile named by the `profile` option, `AWS_PROFILE`, or `default`, from `~/.aws/credentials` and `~/.aws/config` (or `AWS_SHARED_CREDENTIALS_FILE` / `AWS_CONFIG_FILE`). A profile may hold static keys, `role_arn` + `source_profile` / `credential_source` (STS `AssumeRole`), `web_identity_token_file`, `credential_process`, or IAM Identity Center settings (`sso_session` / `sso_*`, using the token that `aws sso login` cached). | ||
| 3. `AWS_WEB_IDENTITY_TOKEN_FILE` + `AWS_ROLE_ARN` — EKS service-account roles (IRSA) | ||
| 4. The container credentials endpoint — `AWS_CONTAINER_CREDENTIALS_RELATIVE_URI` or `AWS_CONTAINER_CREDENTIALS_FULL_URI` (+ `AWS_CONTAINER_AUTHORIZATION_TOKEN[_FILE]`), set by ECS, EKS Pod Identity, App Runner, … | ||
| 5. EC2 instance metadata (IMDSv2). Honours `AWS_EC2_METADATA_DISABLED`, `AWS_EC2_METADATA_SERVICE_ENDPOINT[_MODE]`, `AWS_EC2_METADATA_V1_DISABLED`, `AWS_METADATA_SERVICE_TIMEOUT` and `AWS_METADATA_SERVICE_NUM_ATTEMPTS`. | ||
|
|
||
| A source that isn't configured is skipped. A source that _is_ configured but fails (an expired SSO session, an STS `AccessDenied`, a `credential_process` that exits non-zero) is an error — Bun does not silently fall through to the next one. | ||
|
|
||
| As in the AWS SDKs, selecting a profile (`AWS_PROFILE` or the `profile` option) takes precedence over exported `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY`. Environment variables are read from `process.env` when credentials are resolved, so setting `process.env.AWS_PROFILE` at startup works. | ||
|
|
||
| ### `new Bun.AWSClient(options?)` / `Bun.aws` | ||
|
|
||
| An `AWSClient` bundles signing defaults: `region`, `service`, `profile` or `accessKeyId`/`secretAccessKey`/`sessionToken`, and `endpoint` (a base URL for path-only requests, e.g. LocalStack). Every method accepts the same options to override them per call. `Bun.aws` is the instance with no overrides. | ||
|
|
||
| ```ts | ||
| const prod = new Bun.AWSClient({ profile: "prod", region: "eu-west-1" }); | ||
| prod.region; // "eu-west-1" | ||
| prod.profile; // "prod" | ||
| ``` | ||
|
|
||
| ### `client.credentials()` | ||
|
|
||
| Resolve (or return the cached) credentials. Useful for handing them to another SDK, or just to see what Bun found. | ||
|
|
||
| ```ts | ||
| const creds = await Bun.aws.credentials(); | ||
| // { | ||
| // accessKeyId: "ASIA…", | ||
| // secretAccessKey: "…", | ||
| // sessionToken: "…", // temporary credentials only | ||
| // expiration: 2026-01-01T…, // temporary credentials only | ||
| // region: "us-east-1", // if AWS_REGION / the profile set one | ||
| // accountId: "123456789012", // if the source reported it | ||
| // source: "sso", // "env" | "profile" | "assume-role" | "web-identity" | "process" | "sso" | "container" | "imds" | ||
| // } | ||
|
|
||
| await Bun.aws.credentials({ refresh: true }); // ignore the cache | ||
| await new Bun.AWSClient({ profile: "prod" }).credentials(); // a specific profile | ||
| ``` | ||
|
|
||
| When nothing is found, the promise rejects with `code: "ERR_AWS_MISSING_CREDENTIALS"` and a message listing each source and why it was skipped. A configured source that failed rejects with `code: "ERR_AWS_CREDENTIALS"`. | ||
|
|
||
| ### `client.fetch(input, init?)` | ||
|
|
||
| `fetch()`, with the request signed using [Signature Version 4](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_sigv.html). `init` accepts everything `fetch` does plus the signing options below; with none, the service and region are inferred from `*.amazonaws.com` / `*.on.aws` hostnames and ambient credentials are used. | ||
|
|
||
| ```ts | ||
| const res = await Bun.aws.fetch("https://dynamodb.us-east-1.amazonaws.com/", { | ||
| method: "POST", | ||
| headers: { | ||
| "content-type": "application/x-amz-json-1.0", | ||
| "x-amz-target": "DynamoDB_20120810.GetItem", | ||
| }, | ||
| body: JSON.stringify({ TableName: "users", Key: { id: { S: "42" } } }), | ||
| }); | ||
|
|
||
| // A path-only URL goes to the service's standard endpoint in the resolved | ||
| // region: https://sqs.<region>.amazonaws.com/?Action=ListQueues | ||
| await Bun.aws.fetch("/?Action=ListQueues", { service: "sqs" }); | ||
|
|
||
| // API Gateway / Lambda function URL behind IAM auth on a custom domain | ||
| await Bun.aws.fetch("https://api.example.com/orders", { service: "execute-api", region: "eu-west-1", profile: "prod" }); | ||
| ``` | ||
|
|
||
| | Option | Default | | ||
| | ------------------------------------------------ | ---------------------------------------------------------------------------------------- | | ||
| | `service`, `region` | inferred from the hostname; region falls back to `AWS_REGION`, then the profile's region | | ||
| | `accessKeyId`, `secretAccessKey`, `sessionToken` | ambient credentials | | ||
| | `profile` | `AWS_PROFILE` / `default` | | ||
| | `signQuery`, `expiresIn` | `false`, `900` — put the signature in the query string | | ||
| | `unsignedPayload` | `false` — send `UNSIGNED-PAYLOAD` (S3-style services only) | | ||
|
|
||
| Bun adds `Authorization`, `x-amz-date`, `x-amz-security-token` (temporary credentials) and, for S3, `x-amz-content-sha256`; setting those yourself is an error. `ReadableStream` bodies can only be signed for S3-style services (as `UNSIGNED-PAYLOAD`), since other services need the body's SHA-256 up front. | ||
|
|
||
| A signature is only valid for the exact host and path it was computed for, so `redirect` defaults to `"manual"` — a `307` from S3 pointing at another region comes back as a `307` rather than being followed unsigned. Pass `redirect: "follow"` explicitly if you really want that. | ||
|
|
||
| For `s3://` URLs keep using `fetch("s3://…", { s3: {...} })` or `Bun.s3`. | ||
|
|
||
| ### `client.presign(url, options?)` | ||
|
|
||
| Returns a query-string-signed URL — typically an S3 object URL to hand to a browser. Takes the same signing options as `fetch`, plus `method` (default `"GET"`) and `expiresIn` (seconds, default `900`, max `604800`). | ||
|
|
||
| ```ts | ||
| const download = await Bun.aws.presign("https://my-bucket.s3.eu-west-1.amazonaws.com/report.pdf", { expiresIn: 3600 }); | ||
| const upload = await Bun.aws.presign("https://my-bucket.s3.eu-west-1.amazonaws.com/upload.bin", { method: "PUT" }); | ||
| const queue = await Bun.aws.presign("/123456789012/jobs?Action=ReceiveMessage", { service: "sqs" }); | ||
| ``` | ||
|
|
||
| It returns a promise because the credentials may still have to be fetched; the signing itself is local. For `s3://`-style paths and ACLs, [`S3File.presign()`](/runtime/s3#presigning-urls) is usually more convenient. | ||
|
|
||
| ## Google Cloud | ||
|
|
||
| ### Where tokens come from | ||
|
|
||
| [Application Default Credentials](https://cloud.google.com/docs/authentication/application-default-credentials), in order: | ||
|
|
||
| 1. `GOOGLE_APPLICATION_CREDENTIALS` — a service-account key file, or an `authorized_user` file | ||
| 2. The file `gcloud auth application-default login` writes: `~/.config/gcloud/application_default_credentials.json` (`%APPDATA%\gcloud\…` on Windows, or under `CLOUDSDK_CONFIG`) | ||
| 3. The metadata server on Compute Engine, GKE, Cloud Run, Cloud Functions, App Engine, Cloud Build … (honours `GCE_METADATA_HOST` and `NO_GCE_CHECK`) | ||
|
|
||
| Service-account keys are exchanged for tokens with a self-signed RS256 JWT; user credentials use their refresh token. `external_account` (workload identity federation) files are not supported yet. | ||
|
|
||
| ### `new Bun.GCPClient(options?)` / `Bun.gcp` | ||
|
|
||
| `Bun.gcp` uses Application Default Credentials. A `GCPClient` can instead be given a `keyFile` path or the key's `credentials` (object or JSON string), plus default `scopes` or an `audience` (making `fetch()` send ID tokens by default). | ||
|
|
||
| ```ts | ||
| const deployer = new Bun.GCPClient({ keyFile: "/secrets/deployer.json", scopes: ["cloud-platform"] }); | ||
| const invoker = new Bun.GCPClient({ | ||
| credentials: JSON.parse(process.env.SA_KEY!), | ||
| audience: "https://my-service-abc123.a.run.app", | ||
| }); | ||
| ``` | ||
|
|
||
| ### `client.accessToken()` / `client.idToken()` | ||
|
|
||
| ```ts | ||
| const { token, expiration, source } = await Bun.gcp.accessToken(); | ||
| // source: "service-account" | "authorized-user" | "metadata" | ||
| // also: email, projectId, quotaProjectId when known | ||
|
|
||
| await Bun.gcp.accessToken({ scopes: ["bigquery", "https://www.googleapis.com/auth/pubsub"] }); | ||
| // bare scope names expand to https://www.googleapis.com/auth/<name>; the default is cloud-platform | ||
|
|
||
| // OIDC identity token for calling Cloud Run / Cloud Functions / IAP | ||
| const { token: idToken } = await Bun.gcp.idToken("https://my-service-abc123.a.run.app"); | ||
| ``` | ||
|
|
||
| Tokens are cached per scope set / audience; pass `refresh: true` to force a new one. Failures reject with `code: "ERR_GCP_MISSING_CREDENTIALS"` (nothing configured) or `"ERR_GCP_CREDENTIALS"` (a configured source failed). | ||
|
|
||
| ### `client.fetch(input, init?)` (GCP) | ||
|
|
||
| ```ts | ||
| await Bun.gcp.fetch("https://pubsub.googleapis.com/v1/projects/my-project/topics"); | ||
| await Bun.gcp.fetch("https://storage.googleapis.com/storage/v1/b/my-bucket/o", { scopes: "devstorage.read_only" }); | ||
| await Bun.gcp.fetch("https://my-service-abc123.a.run.app/", { audience: "https://my-service-abc123.a.run.app" }); | ||
| ``` | ||
|
|
||
| `init` accepts everything `fetch` does plus `scopes` (access token, the default) or `audience` (ID token). Bun sets `Authorization: Bearer <token>`, and `x-goog-user-project` when the credentials carry a `quota_project_id` (or `GOOGLE_CLOUD_QUOTA_PROJECT` is set) unless you set it yourself. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Fix the heading level so the following protocol sections stay under "Protocol support".
Line 294 uses
###, but it sits inside the### Protocol supportblock whose siblings (#### S3 URLs,#### File URLs,#### Data URLs,#### Blob URLs) all use####. An###here closes "Protocol support", so#### File URLs,#### Data URLs, and#### Blob URLsbecome children of "AWS- and Google-authenticated requests" in the TOC.The new section also documents a client API, not a URL protocol. Move it after the
#### Blob URLsblock and keep it as###, or keep it here and demote it to####.📝 Proposed fix (demote in place)
📝 Committable suggestion
🤖 Prompt for AI Agents