# webhook-forge — HMAC webhook signing & verification

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...


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

```js
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`.


---
Source: shared/engines/adapters/domain/webhook-forge/README.md
Canonical: https://docs.leumas.tech/p/adapters/adapter-webhook-forge
