# Webhook receiver — parser, verifier class, receiver, hooks, error taxonomy **Package**: `@suaveplan/webhook-client` **Status**: PROPOSED **Type**: feature + spec-conformance ## Why `@suaveplan/webhook-client` (`packages/platform/webhook-client/`) currently ships only the outbound delivery surface: `WebhookClient`, `deliver`, signing helpers (`generateSignature`, `parseSignatureHeader`, `verifySignature`), and the delivery types. The legacy package shipped both delivery AND receive surfaces, and downstream consumers (admin webhooks endpoint, billing webhook ingest, vendor-event mux) need the inbound side: a parser, a verifier class abstraction, a `WebhookReceiver` orchestrator, a typed hook bus, and a structured error taxonomy with retry-hint metadata. Without these, every consumer hand-rolls the verify → parse → dispatch pipeline with inconsistent error semantics. All five legacy modules already exist (`packages/universal/webhook-client/src/parser/parser.ts` 130 L; `verifier/verifier.ts` 127 L; `receiver/receiver.ts` 135 L; `hooks.ts` 58 L; `errors/error-codes.ts` 164 L) and pass the genesis quality bar — porting them closes the parity gap before consumers fork. The package name is kept as `@suaveplan/webhook-client` (not renamed to `@suaveplan/webhook`) to avoid a breaking rename. The receiver lives at the subpath `@suaveplan/webhook-client/receiver/receiver` alongside the existing `core/client/webhook-client` subpath. ## What changes ### `@suaveplan/webhook-client` - ADDED: `WebhookParser` class — `parseJsonBody(request)`, `parseFormUrlencodedBody(request)`, `parseRequest(request)` (auto-detects content-type); returns a typed `ParsedWebhookEvent`. Validates input through Zod schemas; throws `WebhookClientError` with code `PARSER_INVALID_BODY` / `PARSER_UNSUPPORTED_CONTENT_TYPE` on failure. - ADDED: `WebhookVerifier` class — wraps the existing `verifySignature` helper with a class abstraction. Pluggable `signingScheme` parameter selects vendor-specific schemes: `stripe` (HMAC-SHA256 of `.`), `github` (HMAC-SHA256 of body, prefix `sha256=`), `slack` (HMAC-SHA256 of `v0::`), `generic` (default — raw HMAC-SHA256 of body). Throws `WebhookClientError` with codes `VERIFIER_BAD_SIGNATURE` / `VERIFIER_TIMESTAMP_OUT_OF_TOLERANCE`. - ADDED: `WebhookReceiver` class — orchestrator: verify → parse → dispatch via typed handler registry (`registerHandler(eventType, handler)`); emits `webhookHooks` events at every stage; structured errors per failure mode. `handle(request)` returns `Promise` carrying `{ status, parsedEvent, dispatchResult }`. - ADDED: `webhookHooks` typed event bus — five lifecycle events: `onWebhookDelivering(handler)`, `onWebhookReceived(handler)`, `onWebhookVerified(handler)`, `onWebhookHandled(handler)`, `onWebhookFailed(handler)`. Backed by `@suaveplan/events`. - ADDED: `WEBHOOK_CLIENT_CODES` extensions — `PARSER_INVALID_BODY`, `PARSER_UNSUPPORTED_CONTENT_TYPE`, `VERIFIER_BAD_SIGNATURE`, `VERIFIER_TIMESTAMP_OUT_OF_TOLERANCE`, `RECEIVER_HANDLER_NOT_FOUND`, `RECEIVER_HANDLER_FAILED`. Each code SHALL carry `metadata.retryHint: 'transient' | 'permanent'` so callers can tell "transient — retry" from "bad signature — drop". - ADDED: `ParsedWebhookEvent` interface — `{ id, type, payload: T, receivedAt, signature?, headers }`. - ADDED: `WebhookSchema` Zod-builder helper — `WebhookSchema(payloadSchema)` returns a Zod object schema for `ParsedWebhookEvent`. - ADDED: `WebhookHandler`, `WebhookHandlerRegistry`, `WebhookHookMap`, `SigningScheme`, `HandleResult` types. - ADDED: subpath exports `./parser/parser`, `./verifier/verifier`, `./receiver/receiver`, `./hooks/hooks`. - MODIFIED: root `src/index.ts` re-exports the parser, verifier, receiver, hooks, and the new error codes. - MODIFIED: `WEBHOOK_CLIENT_CODES` block — extended in place, no breaking renames. ## Out of scope - **Renaming the package to `@suaveplan/webhook`** — out of scope. The package is published at `@suaveplan/webhook-client` and renaming would force every consumer to update `package.json` and re-publish. Document the receiver as a sibling subpath; keep the name. - **Replay-protection idempotency store** — out of scope; tracked separately. The receiver records the event ID via OTel attributes but does not persist it. Consumers wire `@suaveplan/idempotency` if they need replay protection. - **Vendor-specific event-type registries** (Stripe `customer.created`, GitHub `pull_request.opened`, etc.) — out of scope. The receiver registry is generic; vendor-specific event-type taxonomies are downstream concerns. - **Receiver as an HTTP framework adapter** (express / hono / fastify wrappers) — out of scope. `WebhookReceiver.handle(request)` accepts a `Request` (Web Fetch API); framework adapters are tiny and downstream. ## Source mapping This change derives from `../suaveplan/enterprise/webhook-client.md` and the upstream package source. | Requirement | Source file | Source lines | Notes | |---|---|---|---| | `WebhookParser` class + JSON / form-urlencoded parsers | `packages/universal/webhook-client/src/parser/parser.ts` | L1-L130 | Verbatim port | | `WebhookVerifier` class | `packages/universal/webhook-client/src/verifier/verifier.ts` | L1-L127 | Verbatim port; reuse existing `generateSignature` / `verifySignature` from `utils/signature.js` | | `WebhookReceiver` class + handler registry | `packages/universal/webhook-client/src/receiver/receiver.ts` | L1-L135 | Verbatim port | | `webhookHooks` typed event bus | `packages/universal/webhook-client/src/hooks.ts` | L1-L58 | Verbatim port; back with `@suaveplan/events` instead of upstream's bespoke `Hooks` class | | Error code catalog with retry-hint metadata | `packages/universal/webhook-client/src/errors/error-codes.ts` | L1-L164 | Verbatim port | | `ParsedWebhookEvent`, `SigningScheme`, `WebhookHandler`, `WebhookHookMap`, `HandleResult` types | `packages/universal/webhook-client/src/types/webhook-client.types.ts` | L1-L64 | Verbatim port; co-locate with existing `webhook-client.types.ts` | ### Requirements with no source line | Requirement | Justification | |---|---| | `metadata.retryHint` on every new error code | Genesis-wide convention: structured error metadata so callers can branch without parsing message strings. Upstream encodes retry-hint in prose, not metadata. | | `webhookHooks` backed by `@suaveplan/events` | AGENTS.md §0 ("If an existing implementation covers 80%+ of the requirement, port or wrap it — do not start from scratch"). Upstream uses a bespoke `Hooks` class; genesis already has `@suaveplan/events`. | | `WebhookSchema` Zod-builder helper | AGENTS.md §13 row 2 ("Zod schemas wired at every declared boundary"). Upstream uses bare TS interfaces. | ## Verification approach Each requirement carries a `**Verified by:**` line in the spec delta. - `packages/platform/webhook-client/src/parser/parser/parser.test.ts` — JSON / form-urlencoded round-trip, content-type rejection, malformed-body rejection - `packages/platform/webhook-client/src/verifier/verifier/verifier.test.ts` — every signing scheme verified; bad signature rejected; timestamp tolerance enforced - `packages/platform/webhook-client/src/receiver/receiver/receiver.test.ts` — full pipeline (verify → parse → dispatch); handler-not-found path; handler-failure path - `packages/platform/webhook-client/src/receiver/receiver/receiver.spec-conformance.test.ts` — `webhookHooks` events fire in order; OTel spans emitted; error codes carry retry-hint metadata - `packages/platform/webhook-client/src/hooks/hooks/hooks.test.ts` — `webhookHooks` registration / unregistration / event dispatch order - `packages/platform/webhook-client/src/errors/error-codes.spec-conformance.test.ts` — every new code carries `metadata.retryHint`; codes registered via `defineCodes` ## Impact ### Affected dependents Run `bunx turbo ls --filter=...@suaveplan/webhook-client` to enumerate. - No dependents (verified by `grep -rl "@suaveplan/webhook-client" /home/ubuntu/web/genesis/packages/ --include="package.json"` → only `packages/platform/webhook-client/package.json` itself). Pure additive change. Existing `WebhookClient` / `deliver` / signing helpers unchanged. ## Migration impact None — additive only. Existing inbound hand-rolls in downstream consumers (none yet in genesis) can migrate to `WebhookReceiver` at their own pace. The receiver is opt-in: callers either continue calling `verifySignature` directly (pre-existing surface) or wire the full `WebhookReceiver` pipeline. The package name stays as `@suaveplan/webhook-client`; the receiver is documented as a sibling capability under the `./receiver/*` subpath. A renamed package (`@suaveplan/webhook`) is out of scope to avoid breaking the publish identity.