← all parts

Part · webhooks.dispatch

What's actually behind webhooks.dispatch

The part exactly as partkit add webhooks.dispatch vendors it into your repo — verified, locked, every byte readable. Nothing here is mocked.

webhooks.dispatchv1.1.0

✓ attested🔒 read-onlywebhooks.dispatch@1

Lives at parts/webhooks.dispatch/ in your repo — open, owned, readable. Not buried in node_modules. 916 lines of source you can audit.

17 conformance tests passedverified 2026-06-15↗ CI run
content hash fb8efa0006…9949f4pinned in parts.lockctrlai guard fails CI if a single byte changes
tested against node 25.3.0

Public API — what your seam calls

  • dispatcher(db: SqlExecutor): Dispatcher
  • Dispatcher { registerEndpoint(input: RegisterEndpointInput): Promise<RegisteredEndpoint>; dispatch(input: DispatchInput): Promise<DispatchResult>; deliverDue(opts?: DeliverDueOptions): Promise<DeliveryReport>; listAttempts(messageId: string): Promise<DeliveryAttempt[]> }
  • class DispatchError extends Error { code: DispatchErrorCode }
  • types: SqlExecutor, Dispatcher, RegisterEndpointInput, RegisteredEndpoint, DispatchInput, DispatchResult, DeliverDueOptions, DeliveryReport, DeliveryAttempt, DeliveryOutcome, DispatchErrorCode

Invariants — guarantees the contract pins

  1. Importing the part performs no I/O and never throws; registerEndpoint/dispatch validate input with typed errors and zero network calls, and every storage failure surfaces as a typed DispatchError (raw driver errors never escape)
  2. dispatch NEVER performs the HTTP delivery inline — it persists to the outbox and returns a messageId; a slow or hostile customer endpoint cannot block or fail the caller. Delivery happens only in deliverDue
  3. Every delivery carries a valid Standard Webhooks signature over `${id}.${timestamp}.${body}` — byte-identical to what webhooks.ingest's standardwebhooks adapter verifies; any tampered byte makes verification fail
  4. Delivery is retried with capped exponential backoff on network error / 5xx / 429 (honoring Retry-After), up to a bounded attempt count; a 4xx (except 429) is permanent and not retried; every attempt — outcome, status, latency, next-retry — is recorded, and an exhausted message moves to dead-letter, never silently dropped
  5. At-least-once with idempotency: the same idempotencyKey enqueued twice yields exactly one outbox row; each delivery carries a stable webhook-id for receiver-side dedupe
  6. SSRF defense: registerEndpoint and delivery refuse non-public destinations — http:// (https only), loopback, link-local, RFC-1918 private ranges, unique-local IPv6, and the cloud metadata address 169.254.169.254 — enforced at delivery time on the DNS-resolved IP, not just at registration, to defeat DNS rebinding
  7. The endpoint secret is returned exactly once at registration, stored only as needed to sign, and never appears in error messages, in DispatchError, or in listAttempts
  8. The part operates solely through the provided SqlExecutor seam — it imports no database driver — and every statement it issues targets only its own webhooks_dispatch_* tables; every input is parameterized, so SQL metacharacters are stored literally and never executed

Owns in your Postgres

webhooks_dispatch_endpointswebhooks_dispatch_outboxwebhooks_dispatch_attempts

Dependencies

zero-dep — runs on your Postgres
SOURCEparts/webhooks.dispatch/19 files · click to read
parts/webhooks.dispatch/src/index.tstypescript · 1,673 bytes
/**
 * webhooks.dispatch — public interface. The ONLY legal import surface.
 * Contract: ../contract.json · What your app must provide: ../seams.md
 *
 * The verified OUTBOUND webhook sender: register customer endpoints, dispatch
 * signed events to an outbox (never inline), and deliver out-of-band with retry,
 * backoff, a delivery log, dead-letter, and SSRF defense. The API-facing sibling
 * of webhooks.ingest — same Standard Webhooks signature, so a customer verifies
 * our deliveries with the same code.
 */
import { createDispatcher } from "./internal/store";
import type { Dispatcher, SqlExecutor } from "./internal/types";

export { DispatchError } from "./internal/errors";
export type { DispatchErrorCode } from "./internal/errors";
export type {
  DeliverDueOptions,
  DeliveryAttempt,
  DeliveryOutcome,
  DeliveryReport,
  Dispatcher,
  DispatchInput,
  DispatchResult,
  RegisteredEndpoint,
  RegisterEndpointInput,
  SqlExecutor,
} from "./internal/types";

/**
 * Bind the dispatcher to a database connection (the SqlExecutor seam).
 * Constructing it performs no I/O and never throws (contract invariant 1) — the
 * database is touched only when a method runs, so it is serverless-safe. Pass a
 * per-request executor from your pool.
 *
 *   const wh = dispatcher(db);
 *   const { id, secret } = await wh.registerEndpoint({ ownerId, url });   // secret shown once
 *   await wh.dispatch({ endpointId: id, eventType: "invoice.paid", payload });
 *   // …then drive deliverDue() from jobs.queue or a cron (seams.md §5):
 *   await wh.deliverDue();
 */
export function dispatcher(db: SqlExecutor): Dispatcher {
  return createDispatcher(db);
}