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@rcstitchapi 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 body →
UNSIGNED-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' }) → itsapplication/x-www-form-urlencodedencoding — 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: truewith a multipart body throws — leave it unset to sendUNSIGNED-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: 20150830T124600ZmanualClock() 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
JSON Schema
JsonSchema.adapt turns a runtime-discovered JSON Schema into a StitchSchema (a Standard Schema) a stitch accepts — validated with an engine you supply, so it ships none of its own.
Vercel AI SDK
stitchTool exposes a stitch as a Vercel AI SDK tool() the model can call — the stitch runs as the tool's execute, so the model gets typed, validated data and never the credential.