Initial Commit
This commit is contained in:
84
openspec/changes/webhook-receiver/proposal.md
Normal file
84
openspec/changes/webhook-receiver/proposal.md
Normal 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.
|
||||
Reference in New Issue
Block a user