A compact AWS Signature Version 4 (SigV4) client for fetch. Sign requests and call AWS REST APIs without the full AWS SDK.
Inspired by aws4fetch, with TypeScript types, injectable Web APIs, and Node.js 22+ as the baseline runtime.
- Node.js 22+ — native
fetch,Request,Headers,TextEncoder, and Web Crypto (crypto.subtle) are used directly - A small XML parser dependency is used for S3-compatible listing helpers
pnpm add sigv4fetchnpm install sigv4fetchimport { AwsClient } from 'sigv4fetch'
const aws = new AwsClient({
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
sessionToken: process.env.AWS_SESSION_TOKEN, // optional, for temporary credentials
})
const response = await aws.fetch('https://lambda.us-east-1.amazonaws.com/2015-03-31/functions/my-fn/invocations', {
method: 'POST',
body: JSON.stringify({ key: 'value' }),
headers: { 'Content-Type': 'application/json' },
})
console.log(await response.json())Service and region are inferred from the URL when omitted. You can also set defaults on the client:
const aws = new AwsClient({
accessKeyId: '...',
secretAccessKey: '...',
service: 's3',
region: 'us-west-2',
})High-level client for signing and sending requests.
import { AwsClient } from 'sigv4fetch'
const aws = new AwsClient({
accessKeyId: string // required
secretAccessKey: string // required
sessionToken?: string // STS / IAM role credentials
service?: string // default service (otherwise parsed from URL)
region?: string // default region (otherwise parsed from URL)
cache?: Map<string, ArrayBuffer> // signing key cache
retries?: number // default 10 (0 = no retries)
initRetryMs?: number // default 50, doubles each retry with jitter
api?: AwsApiInput // optional injected Web APIs (see below)
})Returns a signed Request. Accepts a URL string or Request.
const signed = await aws.sign('https://dynamodb.us-east-1.amazonaws.com/', {
method: 'POST',
body: JSON.stringify({ TableName: 'MyTable' }),
headers: {
'Content-Type': 'application/x-amz-json-1.0',
'X-Amz-Target': 'DynamoDB_20120810.ListTables',
},
})
// use with any fetch implementation
await fetch(signed)Signs the request and calls fetch. Retries on 5xx and 429 with exponential backoff and jitter.
Lower-level signer when you only need the signed method, URL, headers, and body.
import { AwsV4Signer } from 'sigv4fetch'
const signer = new AwsV4Signer({
url: 'https://example-bucket.s3.amazonaws.com/object.txt',
accessKeyId: '...',
secretAccessKey: '...',
method: 'GET',
})
const { method, url, headers, body } = await signer.sign()Pass signing overrides via init.aws:
await aws.fetch(url, {
method: 'PUT',
body: fileContents,
headers: { 'Content-Type': 'text/plain' },
aws: {
service: 's3',
region: 'eu-west-1',
datetime: '20150830T123600Z', // fixed timestamp (testing / replay)
},
})| Option | Description |
|---|---|
accessKeyId |
Override client access key |
secretAccessKey |
Override client secret key |
sessionToken |
Temporary credential token |
service |
AWS service name |
region |
AWS region |
datetime |
SigV4 timestamp (YYYYMMDDTHHmmssZ) |
signQuery |
Sign query string instead of Authorization header (presigned URLs) |
appendSessionToken |
Append X-Amz-Security-Token after signing (default for IoT gateway) |
allHeaders |
Sign all headers instead of the default subset |
singleEncode |
Only encode %2F once (mainly for testing) |
cache |
Per-request signing key cache |
const signer = new AwsV4Signer({
url: 'https://my-bucket.s3.amazonaws.com/report.pdf',
accessKeyId: '...',
secretAccessKey: '...',
signQuery: true,
})
const { url } = await signer.sign()
console.log(url.toString()) // shareable presigned URLS3 presigned URLs default to X-Amz-Expires=86400 (24 hours) when not set.
Use customS3Client for AWS S3 and most S3-compatible storage providers, including Cloudflare R2, Backblaze B2, DigitalOcean Spaces, Linode Object Storage, MinIO, Wasabi, Tigris, and other custom endpoints.
For virtual-host style endpoints:
import { customS3Client, listObjectsV2 } from 'sigv4fetch'
const s3 = customS3Client({
host: 's3.us-east-1.amazonaws.com',
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
region: 'us-east-1',
})
const objects = await listObjectsV2(s3, {
bucket: 'my-bucket',
sortBy: 'key',
})For path-style endpoints, such as many MinIO deployments:
const minio = customS3Client({
host: 'localhost:9000',
accessKeyId: 'minioadmin',
secretAccessKey: 'minioadmin',
region: 'us-east-1',
secure: false,
forcePathStyle: true,
})Cloudflare R2 also has a convenience helper:
import { cloudflareR2Client, listObjectsV2 } from 'sigv4fetch'
const r2 = cloudflareR2Client({
accountId: process.env.CLOUDFLARE_ACCOUNT_ID!,
accessKeyId: process.env.CLOUDFLARE_ACCESS_KEY_ID!,
secretAccessKey: process.env.CLOUDFLARE_SECRET_ACCESS_KEY!,
})
const objects = await listObjectsV2(r2, {
bucket: 'my-bucket',
prefix: 'uploads/',
sortBy: 'lastModified',
sortDirection: 'desc',
})
console.log(objects.contents)listObjectsV2 parses the XML returned by S3-compatible providers and can sort the returned contents array client-side. sortBy supports key, lastModified, size, eTag, and storageClass.
Common object helpers are included:
import {
deleteObject,
deleteObjects,
getObjectBlob,
headObject,
putObject,
} from 'sigv4fetch'
await putObject(s3, {
bucket: 'my-bucket',
key: 'uploads/hello.txt',
body: 'hello',
contentType: 'text/plain',
metadata: { source: 'example' },
})
const head = await headObject(s3, {
bucket: 'my-bucket',
key: 'uploads/hello.txt',
})
const object = await getObjectBlob(s3, {
bucket: 'my-bucket',
key: 'uploads/hello.txt',
})
await deleteObject(s3, {
bucket: 'my-bucket',
key: 'uploads/hello.txt',
})
await deleteObjects(s3, {
bucket: 'my-bucket',
objects: [{ key: 'uploads/a.txt' }, { key: 'uploads/b.txt' }],
})Provider compatibility depends on how closely the service implements the S3 API. Some providers require path-style URLs, a specific region value, or slightly different endpoint hosts.
Optionally inject fetch, Request, Headers, crypto, and TextEncoder. Useful for tests or custom fetch implementations:
import { AwsClient, getDefaultApi } from 'sigv4fetch'
const api = getDefaultApi()
const aws = new AwsClient({
accessKeyId: '...',
secretAccessKey: '...',
api: {
...api,
fetch: myCustomFetch,
},
})On Node 22+, globals are available out of the box — injection is optional.
Note: When injecting Request, pass Request instances created from the same constructor (input instanceof aws.api.Request).
| Environment | Support |
|---|---|
| Node.js 22+ | ✅ Native globals |
| Cloudflare Workers | ✅ (Web Crypto + fetch) |
| Browsers | ✅ |
| Deno / Bun | ✅ (with fetch + Web Crypto) |
Request bodies must be a string, ArrayBuffer, or ArrayBufferView unless you set the X-Amz-Content-Sha256 header yourself. Blob, FormData, and readable streams are not hashed automatically.
S3 requests use UNSIGNED-PAYLOAD by default for the payload hash unless you provide X-Amz-Content-Sha256.
pnpm install
pnpm run typecheck
pnpm run build
pnpm run dev # watch modepnpm run prepublishOnly # typecheck + build
npm publishMIT © nunesunil