Files
skills/docs/communication/messaging/sms-providers/src/aws-sns/sigv4.md
2026-05-29 08:20:56 +01:00

3.8 KiB

SigV4 — @suaveplan/sms-providers

Purpose

Inline AWS Signature V4 signer used by AwsSnsProvider. Replaces the @aws-sdk/signature-v4 dependency tree (15+ MB) with an under-80-LOC implementation of the canonical-request → string-to-sign → signing-key → final-signature chain. HMAC computations are delegated to the injectable HmacSign seam from @suaveplan/sms-providers, so the signer stays universal and tests can substitute a deterministic adapter.

The signer returns a fresh headers object containing the input headers plus Host, X-Amz-Date, X-Amz-Content-Sha256, the optional X-Amz-Security-Token (when a session token is supplied), and the Authorization header in the documented form AWS4-HMAC-SHA256 Credential=…, SignedHeaders=…, Signature=<64 hex>.

Features

  • 64-character lowercase-hex signature per the AWS spec.
  • Canonical request built from method + path + canonical headers (lower-cased keys, trimmed values) + signed-headers list + body-sha256 hex.
  • dateStamp (YYYYMMDD) and amzDate (YYYYMMDDTHHMMSSZ) derived from a caller-supplied Date so tests can pin the clock and produce stable signatures.
  • Signing key chain: kDate → kRegion → kService → kSigning via HMAC-SHA256 over AWS4<secret>, region, service, aws4_request.
  • Body SHA-256 hashing routed through globalThis.crypto.subtle.digest("SHA-256", …) (universal: Bun, browsers, Workers, Deno, Node 19+).

Usage: Basic

import { defaultHmac } from "@suaveplan/sms-providers";
import { signRequest, sha256OfUtf8 } from "@suaveplan/sms-providers/aws-sns/sigv4";

const body = "Action=Publish&Message=hi";
const out = await signRequest({
    accessKeyId: "AKIA…",
    secretAccessKey: "…",
    region: "us-east-1",
    service: "sns",
    method: "POST",
    host: "sns.us-east-1.amazonaws.com",
    path: "/",
    headers: { "Content-Type": "application/x-www-form-urlencoded" },
    bodySha256Hex: await sha256OfUtf8(body),
    now: new Date(),
    hmac: defaultHmac,
});
// out.headers.Authorization === "AWS4-HMAC-SHA256 Credential=AKIA…/<dateStamp>/us-east-1/sns/aws4_request, SignedHeaders=…, Signature=<64 hex>"

Usage: Advanced

import { signRequest, sha256OfUtf8 } from "@suaveplan/sms-providers/aws-sns/sigv4";
import { createHmac } from "node:crypto";

// Drive the signer with a Node-side HMAC adapter and a pinned date so
// the resulting Authorization header is byte-stable between test runs.
const hmac = async (algo: "SHA-1" | "SHA-256", key: Uint8Array, msg: Uint8Array) =>
    new Uint8Array(
        createHmac(algo === "SHA-1" ? "sha1" : "sha256", Buffer.from(key))
            .update(Buffer.from(msg))
            .digest()
    );
const out = await signRequest({
    accessKeyId: "AKIDEXAMPLE",
    secretAccessKey: "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
    region: "us-east-1",
    service: "sns",
    method: "POST",
    host: "sns.us-east-1.amazonaws.com",
    path: "/",
    headers: { "Content-Type": "application/x-www-form-urlencoded" },
    bodySha256Hex: await sha256OfUtf8("Action=Publish"),
    now: new Date("2026-04-24T10:00:00Z"),
    hmac,
});

API Reference

Symbol Kind Notes
signRequest(input) function Returns { headers } with the SigV4 signature applied.
sha256OfUtf8(s) function SHA-256-of-UTF-8 helper returning lowercase hex.
SignRequestInput interface Required inputs to the signer.
SignRequestOutput interface Returned headers object.

Implementation Notes

The canonical-request newline before the body-sha256 is intentional per the AWS docs (the canonical request ends with \n<body-sha256>, no trailing newline). The signed-headers list is computed by lower-casing every input key, sorting, and joining with ; — this matches the canonical-request header rendering exactly.