Webhook
Outbound webhook delivery with cryptographic signing in a single Webhook-Signature header, retry + dead-letter via b.retry, and idempotency keys baked into the signed string so a captured signature cannot be replayed with a fresh id. Inbound verification is the symmetric primitive: verifier() returns a middleware that parses the header, enforces the timestamp window, finds a matching kid, runs constant-time signature compare, and (when configured) consults a nonce store for replay defense.
Algorithms: hmac-sha3-512 (symmetric, kid → Buffer/string secret) or pqc-pem (asymmetric — SLH-DSA-SHAKE-256f / ML-DSA-87 / ML-DSA-65, auto-detected by Node from the PEM). No classical (Ed25519 / RSA / ECDSA) signature scheme is exposed.
Signed string is prefix-bound to defend against algorithm- and key-substitution attacks: . Header is the Stripe-shape t=; t and id are reserved segment names, every other pair is a kid → signature mapping. The signer emits exactly one kid; the verifier accepts any number so operators rotating keys point the verifier at both old + new keys and migrate signers progressively.
PQC signatures are emitted as base64url (~40 KB for SLH-DSA-SHAKE- 256f, vs ~59 KB hex) to fit common front-end header caps; the verifier accepts EITHER encoding for transition windows.
Replay defense: passing a nonceStore (any object exposing checkAndInsert(nonce, expireAt) → bool/Promise) records seen ids; a second delivery with the same id rejects with REPLAY. b.nonceStore is the reference implementation; operators plug in Redis / SQL by satisfying the same shape.
Audit defaults are ON for both success and failure on both sides — the inbound verify IS the auditable boundary event, not a precursor to one. Operators with extreme volume opt out via auditSuccess: false; failures remain on regardless.
b.webhook.dispatcher(opts) #
{
externalDb: b.externalDb, // required — storage backend
endpointsTable: string, // default frameworkSchema.tableName("webhook_endpoints")
deliveriesTable: string, // default frameworkSchema.tableName("webhook_deliveries")
maxAttempts: number, // default 8 → then dead-letter
retryBackoff: { initialMs, maxMs, factor }, // default 5s / 60min / 2x
claimReclaimMs: number, // default 5 min stale-in-flight lease
batchSize: number, // default 100 deliveries per processRetries
signatureHeader: string, // forwarded to b.webhook.signer
allowedProtocols: object, // b.safeUrl protocol set (default ALLOW_HTTP_TLS)
allowInternalDestinations: boolean, // default false — refuse SSRF (private/loopback/metadata)
httpRequest: function, // (url, body, headers) → { status } — inject for tests
now: function, // clock injection → ms epoch
dnsLookup: function, // (host) → [{ address, family }] — override the SSRF destination resolver
}
Build a durable signed-webhook delivery store backed by the operator's b.externalDb. The returned object exposes:
- declareSchema(xdb?) — idempotent CREATE TABLE for the endpoints + deliveries tables (run once at boot, like b.outbox.declareSchema). - registerEndpoint({ endpointId, url, eventTypes, secret }) — persist a subscriber. The URL is validated through b.safeUrl (SSRF destinations refused); the secret is sealed at rest with b.vault.seal. eventTypes is an array of event names, or ["*"] to receive every event. - removeEndpoint(endpointId) / listEndpoints(). - dispatch(eventType, payload) — fan the event out to every subscribed endpoint as its own durable delivery row, sign each via b.webhook.signer, and attempt delivery once inline. Returns { delivered, failed, deliveries: [...] }. - processRetries() — poll/alarm entry point: claim every delivery whose next_attempt_at is due, re-attempt, back off on the b.outbox curve, and dead-letter after maxAttempts. Reaps deliveries stranded in-flight by a crashed worker. Returns { attempted, delivered, dead }. - deliveries.list({ endpointId?, status?, limit? }) / deliveries.get(id) / deliveries.retry(id) — operator-console surface. - dlq.list({ limit? }) / dlq.replay(id) — dead-letter inspect + replay.
Each delivery carries a stable X-Webhook-Delivery-Id (so a re-delivery is deduped by the receiver, not rejected as a replay) plus the signer's fresh per-attempt nonce in the signature (replay defense at the signature layer).
var wd = b.webhook.dispatcher({ externalDb: b.externalDb });
await wd.declareSchema();
await wd.registerEndpoint({
endpointId: "acct_42",
url: "https://partner.example/hooks",
eventTypes: ["invoice.paid", "invoice.refunded"],
secret: "whsec_partner_secret",
});
await wd.dispatch("invoice.paid", { id: "inv_1", amount: 4200 });
// later, from a cron / alarm:
await wd.processRetries();
b.webhook.signer(opts) #
{
algo: "hmac-sha3-512" | "pqc-pem",
keys: { [kid]: Buffer | string } // hmac
| { [kid]: { privateKey, publicKey } } // pqc-pem
defaultKid: string, // required when keys has >1 kid
pqcAlgorithm: "slh-dsa-shake-256f" | "ml-dsa-87" | "ml-dsa-65",
signatureHeader: string, // default "Webhook-Signature"
idGenerator: function () => string,
now: function () => number, // ms
retry: object, // b.retry.withRetry opts
http: object, // b.httpClient.request opts
audit: object, // b.audit handle
auditFailures: boolean, // default true
auditSuccess: boolean, // default true
}
Build an outbound signer. Returns { sign, headers, send }: sign computes the signature header pair for a body without doing I/O; headers returns just the headers map; send performs the POST via b.httpClient.request wrapped in b.retry.withRetry. Each call generates a fresh idempotency id (ULID-shaped via b.crypto. generateToken by default; operators override with idGenerator) that's bound into the signed string so captured signatures cannot replay with a different id.
var b = require("@blamejs/core");
var signer = b.webhook.signer({
algo: "hmac-sha3-512",
keys: { v1: Buffer.from("0123456789abcdef0123456789abcdef") },
defaultKid: "v1",
});
var headers = signer.headers('{"event":"user.created"}');
// → { "Webhook-Signature": "t=1714500000,id=...,v1=" }
b.webhook.verifier(opts) #
{
algo: "hmac-sha3-512" | "pqc-pem",
keys: { [kid]: Buffer | string } // hmac
| { [kid]: string | Buffer }, // pqc-pem (PEM public key)
pqcAlgorithm: "slh-dsa-shake-256f" | "ml-dsa-87" | "ml-dsa-65",
toleranceMs: number, // default 5 minutes
clockSkewMs: number, // default 1 minute
signatureHeader: string, // default "Webhook-Signature"
nonceStore: { checkAndInsert(nonce, expireAt) },
now: function () => number,
audit: object,
auditFailures: boolean, // default true
auditSuccess: boolean, // default true
}
Build an inbound verifier. Returns { verify, middleware }: verify checks an explicit { body, headers } pair and resolves to { algo, kid, timestamp, id } on success; middleware is an Express-style middleware that pulls req.bodyRaw (requires b.middleware.bodyParser({ keepRawBody: true })), verifies, and stashes the result on req.webhook. Failures throw WebhookError with a stable code (MISSING_HEADER / BAD_HEADER_FORMAT / EXPIRED / FUTURE / UNKNOWN_KID / BAD_SIGNATURE / REPLAY / ...) and the middleware translates them to HTTP 401 / 500.
var b = require("@blamejs/core");
var verifier = b.webhook.verifier({
algo: "hmac-sha3-512",
keys: { v1: Buffer.from("0123456789abcdef0123456789abcdef") },
toleranceMs: b.constants.TIME.minutes(5),
});
// wire into a router:
// router.use(b.middleware.bodyParser({ keepRawBody: true }));
// router.post("/inbound", verifier.middleware(), function (req, res) {
// // req.webhook = { algo, kid, timestamp, id }
// });
var mw = verifier.middleware();
// → function (req, res, next) { ... }
b.webhook.verify(input) #
{
alg: "hmac-sha256-stripe",
secret: string | Buffer, // whsec_... bytes verbatim
header: string, // Stripe-Signature value
body: string | Buffer, // raw request body
toleranceMs: number, // default 5 min, min 30 s
nonceStore: { checkAndInsert(nonce, expireAt) }, // optional atomic replay defense
}
Stripe-spec inbound webhook signature verifier. Validates the Stripe-Signature: t= header against an HMAC-SHA-256 over the literal string using the operator's whsec_... secret bytes verbatim (the prefix IS the key). Refuses signatures older than the tolerance window (default 5 min, minimum 30 s). When nonceStore is supplied the verifier atomically records the accepted v1 signature so a replay within the tolerance window is refused; the store speaks the same checkAndInsert(nonce, expireAt) → bool contract as b.webhook.verifier and b.nonceStore.create, so the framework's own replay store plugs in directly and concurrent redeliveries cannot race. Constant-time compare via b.crypto.timingSafeEqual.
await b.webhook.verify({
alg: "hmac-sha256-stripe",
secret: "whsec_abc...",
header: req.headers["stripe-signature"],
body: rawBodyBuffer,
nonceStore: b.nonceStore.create({ backend: "memory" }),
});
// → { ok: true, timestamp: 1700000000, scheme: "v1" }
b.webhook.sign(input) #
Round-trip companion to b.webhook.verify for the hmac-sha256-stripe algorithm. Returns the Stripe-Signature header value t= for a given body + secret + (optional) timestamp. Operators emitting Stripe-shaped webhooks downstream — and the test surface — use this to produce the matching header.
var header = b.webhook.sign({
alg: "hmac-sha256-stripe",
secret: "whsec_abc...",
body: '{"id":"evt_1"}',
});
Last updated 2026-08-08T16:39:49.652Z by seeder.