webhook-forge
Webhook signing + verification adapter pack — generic HMAC (sha256/384/512, hex/base64) with a timing-safe compare, Stripe's t=/v1= scheme with timestamp tolerance, GitHub's X-Hub-Signature-256, a...
webhook-forge — HMAC webhook signing & verification
Sign outgoing webhooks and verify incoming ones with node:crypto HMAC, across the schemes real providers use (generic HMAC, Stripe, GitHub). Pure ESM, Node built-ins only, zero npm deps. Self-contained and deterministic — no network, no stored secrets, timing-safe comparisons.
Tools
| Tool | Input | Output |
|---|---|---|
sign | { payload, secret, scheme?, encoding?, options?.header } | Generic HMAC signature + ready-to-send header. |
verify | { payload, secret, signature, scheme?, encoding? } | { valid, reason } via timing-safe compare (accepts a sha256= prefix). |
stripeSignature | { payload, secret, timestamp? } | Stripe-Signature: t=<ts>,v1=<hex> header. |
stripeVerify | { payload, secret, signature, toleranceSeconds? } | Recomputes v1, timing-safe compare and timestamp tolerance (anti-replay). |
githubVerify | { payload, secret, signature } | Verifies GitHub's sha256=<hex> X-Hub-Signature-256. |
timestampToleranceCheck | { timestamp, toleranceSeconds? } | Is a unix timestamp fresh (within ± tolerance of now)? |
replayGuard | { id, options?.ttlSeconds } | In-memory seen-id guard: first sight passes, repeats inside the TTL are rejected. |
Usage
import pack from './index.js';
// outgoing
const s = pack.adapters.sign({ payload: '{"a":1}', secret: 'whsec_x', scheme: 'hmac-sha256' });
// s.header -> { name:'X-Signature', value:'<hex>' }
// incoming (Stripe)
const r = pack.adapters.stripeVerify({ payload: rawBody, secret: 'whsec_x', signature: header, toleranceSeconds: 300 });
// r.valid, r.reason, r.ageSeconds
Important: for verification, pass the exact raw body string you received. Passing an object re-stringifies it and can reorder keys, breaking the signature — the tools warn via warnObjectPayload.
AI mode
Verification is always decided deterministically. With options.ai: true and a reachable model, a failed verify additionally returns an aiHint explaining the likely cause (the promise resolves to the same result object with the hint attached). A model never changes the valid/invalid verdict.
DRY boundary
HMAC signing/verification of webhook payloads. Not general hashing/encoding (see crypto) and it makes no HTTP requests. Dockerfile tooling lives in dockerfile; dependency auditing in dependency-auditor.