Release candidate — 1.0.0-rc.7
StitchAPI

AWS SigV4

awsSigV4 signs each request with AWS Signature Version 4 — the request-signing AuthStrategy that core's built-in bearer/apiKey/basic/oauth2 don't cover. Edge-safe Web Crypto, no dependencies.

Core's built-in auth strategies cover bearer, apiKey, basic, cookieSession, and oauth2 — but not request signing, which AWS APIs, S3-compatible object stores, and many SigV4-protected endpoints require. @stitchapi/aws-sigv4 adds it as an AuthStrategy.

awsSigV4(...) signs the fully-built request — method, URI, query, headers, and payload hash — at call time, attaching the Authorization and x-amz-* headers. As with every StitchAPI auth strategy, the caller (an agent) gets a capability, not the credential: the keys resolve per call and never reach the call site or a trace.

Crypto is the platform's Web Crypto (crypto.subtle), so it runs unchanged on Node 20+, edge runtimes (Workers / Deno), and the browser; Node 18 falls back to node:crypto. No runtime dependencies.

Example

npm install @stitchapi/aws-sigv4@rc stitchapi@rc

stitchapi is the only peer dependency.

Sign a stitch

Attach awsSigV4(...) as the stitch's auth. Pass the region, service, and credentials (as Secrets — a string or a call-time getter like env(...)):

import { awsSigV4 } from '@stitchapi/aws-sigv4';
import { stitch } from 'stitchapi';
import { env } from 'stitchapi/auth';

const putObject = stitch({
    baseUrl: 'https://my-bucket.s3.us-east-1.amazonaws.com',
    path: '/{key}',
    method: 'PUT',
    auth: awsSigV4({
        region: 'us-east-1',
        service: 's3',
        accessKeyId: env('AWS_ACCESS_KEY_ID'),
        secretAccessKey: env('AWS_SECRET_ACCESS_KEY'),
        // sessionToken: env('AWS_SESSION_TOKEN'), // temporary STS credentials
    }),
});

The signature is computed on the final request — after path templating and query building — so it always matches the bytes the transport sends. A sessionToken adds and signs x-amz-security-token for temporary credentials.

Payload signing

The x-amz-content-sha256 header is set for you:

  • No body → the empty-payload hash (always correct).
  • String body → its SHA-256 (exact bytes).
  • Non-string bodyUNSIGNED-PAYLOAD — safe over HTTPS, and what S3 and many services accept.

Set signBody: true to hash the body instead. The hash is taken over the exact bytes the transport sends, chosen by the stitch's wire.body:

  • JSON (wire: { body: 'json' } or unset) → JSON.stringify(body).
  • Form (wire: { body: 'form' }) → its application/x-www-form-urlencoded encoding — e.g. the query-protocol bodies SQS, SNS, and STS use.
  • Multipart (wire: { body: 'multipart' }) → cannot be payload-signed: the transport generates a non-deterministic boundary, so the hash could never match the bytes sent. signBody: true with a multipart body throws — leave it unset to send UNSIGNED-PAYLOAD, or pass a pre-serialised string body to sign it.

Signing time

x-amz-date is stamped from the stitch's clock, the same injectable seam that drives retry backoff, throttle pacing, and OAuth2 token freshness — not from new Date(). On the default systemClock that is wall-clock time, so nothing changes in production; injecting a manualClock() makes the signature's timestamp something a test can drive:

import { awsSigV4 } from '@stitchapi/aws-sigv4';
import { stitch } from 'stitchapi';
import { manualClock } from 'stitchapi/testing';

const clock = manualClock(Date.UTC(2015, 7, 30, 12, 36, 0));
const call = stitch({
    baseUrl: 'https://my-bucket.s3.us-east-1.amazonaws.com',
    path: '/objects',
    clock,
    auth: awsSigV4({
        region: 'us-east-1',
        service: 's3',
        accessKeyId: 'AKID…',
        secretAccessKey: '…',
    }),
});

await call(); // signs x-amz-date: 20150830T123600Z
await clock.advance(600_000); // ten virtual minutes
await call(); // signs x-amz-date: 20150830T124600Z

manualClock() starts at epoch 0, which stamps 19700101T000000Z — about twenty thousand days of apparent skew, and a real AWS endpoint answers it with RequestTimeTooSkewed. Seed the clock when the signature has to be plausible: manualClock(Date.now()).

Signing at the last moment is also what keeps a signature from going stale in your own backlog: because a throttle waits before the request is signed, a queued call cannot age out of its own signature window here. The signature that expired in your own queue measures that ordering, and what real clock drift still costs you.

Low-level signer

signRequestV4(params) is the pure signing function the strategy wraps — exported for out-of-band signing (presigned URLs, custom flows) and verified against the official AWS aws-sig-v4-test-suite vectors.

import { EMPTY_PAYLOAD_SHA256, signRequestV4 } from '@stitchapi/aws-sigv4';

const { authorization } = await signRequestV4({
    method: 'GET',
    url: 'https://example.amazonaws.com/',
    headers: {
        host: 'example.amazonaws.com',
        'x-amz-date': '20150830T123600Z',
    },
    payloadHash: EMPTY_PAYLOAD_SHA256,
    accessKeyId: 'AKID…',
    secretAccessKey: '…',
    region: 'us-east-1',
    service: 'service',
    amzDate: '20150830T123600Z',
});

Generic per-API HMAC signing isn't shipped as a strategy — each vendor canonicalises differently, so there's no single contract to standardise. SigV4 is the one signing scheme common enough to ship; for a one-off scheme, write a small custom AuthStrategy (an apply(req, ctx) that sets your header).

See also

On this page