Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,84 @@
# 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<T>`. 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 `<timestamp>.<body>`), `github` (HMAC-SHA256 of body, prefix `sha256=`), `slack` (HMAC-SHA256 of `v0:<timestamp>:<body>`), `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<T>(eventType, handler)`); emits `webhookHooks` events at every stage; structured errors per failure mode. `handle(request)` returns `Promise<HandleResult>` 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<T>` interface — `{ id, type, payload: T, receivedAt, signature?, headers }`.
- ADDED: `WebhookSchema<T>` Zod-builder helper — `WebhookSchema(payloadSchema)` returns a Zod object schema for `ParsedWebhookEvent<T>`.
- ADDED: `WebhookHandler<T>`, `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<T>`, `SigningScheme`, `WebhookHandler<T>`, `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<T>` 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.