Files
suaveplan/openspec/changes/webhook-receiver/tasks.md
2026-05-27 23:29:53 +01:00

122 lines
9.9 KiB
Markdown

# Tasks — Webhook receiver
> Every checkbox MUST reflect work actually done in this change. Do not retroactively tick.
## 0. Pre-flight (non-negotiable)
- [ ] 0.1 Confirm clean working tree (`git status` reports nothing) and no in-flight worktrees with uncommitted work (`git worktree list` then `git status` in each)
- [ ] 0.2 If any worktrees exist under `.claude/worktrees/`, account for each one: still-active agent, work-pending-merge, or stale-and-prune. Stale worktrees MUST be removed (`git worktree remove`) before opening a new one — code that has been lost twice on main is the failure mode this guards against.
- [ ] 0.3 Confirm the package set in `MANIFEST.md` matches `find packages -maxdepth 3 -name package.json` count. Drift here means a prior change archived without regenerating the manifest.
## Audit Note (2026-05-02)
**Minimal progress** — DEFERRED, do not archive.
- `packages/platform/webhook-client/src/` has only the outbound surface: `core/`, `errors/`, `types/`, `utils/`. No `parser/`, `verifier/`, `receiver/`, `hooks/`, or `schemas/` directories.
- `grep -rln "WebhookParser\|WebhookVerifier\|WebhookReceiver\|webhookHooks\|ParsedWebhookEvent" packages/platform/webhook-client/src/` returns zero hits.
- `bunx turbo lint typecheck test --filter=@suaveplan/webhook-client` is GREEN — but the entire inbound surface is unimplemented.
- All 64 tasks remain: port `WebhookParser` (130 L), `WebhookVerifier` (127 L) with stripe/github/slack/generic schemes, `WebhookReceiver` (135 L) orchestrator, `webhookHooks` event bus over `@suaveplan/events`, six new error codes with `metadata.retryHint`, `WebhookSchema<T>` Zod builder, all types (`ParsedWebhookEvent<T>`, `SigningScheme`, `WebhookHandler<T>`, etc.), four subpath exports, OTel histogram + counter, sibling `.md`s.
## 1. Spec authoring
- [ ] 1.1 Read `../suaveplan/enterprise/webhook-client.md` end-to-end
- [ ] 1.2 Read `openspec/specs/webhook/spec.md` (canonical capability spec; this change introduces it if absent)
- [ ] 1.3 Read `openspec/AGENTS.md` Sections 13 and 14
- [ ] 1.4 Draft `proposal.md` from `openspec/templates/proposal.md`
- [ ] 1.5 Fill `## Source mapping` table with upstream line ranges
- [ ] 1.6 Draft `specs/webhook/spec.md` from `openspec/templates/spec-delta.md`
- [ ] 1.7 Every requirement carries a `**Verified by:**` line
- [ ] 1.8 `openspec change validate webhook-receiver` exits with no errors
## 2. Implementation — `WebhookParser`
- [ ] 2.1 Port `WebhookParser` from `packages/universal/webhook-client/src/parser/parser.ts` (L1-L130) into `packages/platform/webhook-client/src/parser/parser/parser.ts`
- [ ] 2.2 Implement `parseJsonBody(request)`, `parseFormUrlencodedBody(request)`, `parseRequest(request)` (auto-detects from `content-type` header)
- [ ] 2.3 Validate parsed payload through caller-supplied Zod schema (or default `z.unknown()`); reject with `WebhookClientError` code `PARSER_INVALID_BODY` / `PARSER_UNSUPPORTED_CONTENT_TYPE`
- [ ] 2.4 Return `ParsedWebhookEvent<T>` carrying `{ id, type, payload, receivedAt, signature?, headers }`
- [ ] 2.5 Sibling `.md` (≥200 words, basic + advanced examples)
## 3. Implementation — `WebhookVerifier`
- [ ] 3.1 Port `WebhookVerifier` from `packages/universal/webhook-client/src/verifier/verifier.ts` (L1-L127) into `packages/platform/webhook-client/src/verifier/verifier/verifier.ts`
- [ ] 3.2 Reuse existing `generateSignature` / `verifySignature` from `packages/platform/webhook-client/src/utils/signature.ts` for the `generic` scheme
- [ ] 3.3 Implement `signingScheme: 'stripe' | 'github' | 'slack' | 'generic'` — Stripe (HMAC-SHA256 of `<timestamp>.<body>`), GitHub (HMAC-SHA256 of body, prefix `sha256=`), Slack (HMAC-SHA256 of `v0:<timestamp>:<body>`), generic (raw HMAC-SHA256 of body)
- [ ] 3.4 Enforce `timestampToleranceMs` (default 5 minutes) for schemes that include a timestamp; reject with `VERIFIER_TIMESTAMP_OUT_OF_TOLERANCE`
- [ ] 3.5 Reject bad signatures with `VERIFIER_BAD_SIGNATURE`
- [ ] 3.6 Sibling `.md`
## 4. Implementation — `WebhookReceiver`
- [ ] 4.1 Port `WebhookReceiver` from `packages/universal/webhook-client/src/receiver/receiver.ts` (L1-L135) into `packages/platform/webhook-client/src/receiver/receiver/receiver.ts`
- [ ] 4.2 Implement `registerHandler<T>(eventType, handler)`, `unregisterHandler(eventType)`, `handle(request)` orchestrator
- [ ] 4.3 Pipeline: emit `onWebhookReceived` → verify (emit `onWebhookVerified`) → parse → dispatch to handler (emit `onWebhookHandled` or `onWebhookFailed`)
- [ ] 4.4 Throw `RECEIVER_HANDLER_NOT_FOUND` when no handler is registered for the event type
- [ ] 4.5 Throw `RECEIVER_HANDLER_FAILED` when the handler itself throws; preserve `cause`
- [ ] 4.6 Return `HandleResult` carrying `{ status, parsedEvent, dispatchResult }`
- [ ] 4.7 Sibling `.md`
## 5. Implementation — `webhookHooks` event bus
- [ ] 5.1 Port `webhookHooks` from `packages/universal/webhook-client/src/hooks.ts` (L1-L58) into `packages/platform/webhook-client/src/hooks/hooks/hooks.ts`
- [ ] 5.2 Back the bus with `@suaveplan/events` (not upstream's bespoke `Hooks` class) — `webhookHooks` is an `EventBus<WebhookHookMap>`
- [ ] 5.3 Export `onWebhookDelivering`, `onWebhookReceived`, `onWebhookVerified`, `onWebhookHandled`, `onWebhookFailed` typed-handler registration helpers
- [ ] 5.4 Sibling `.md`
## 6. Error codes
- [ ] 6.1 Extend `WEBHOOK_CLIENT_CODES` in `packages/platform/webhook-client/src/errors/webhook-client-error.ts` with: `PARSER_INVALID_BODY`, `PARSER_UNSUPPORTED_CONTENT_TYPE`, `VERIFIER_BAD_SIGNATURE`, `VERIFIER_TIMESTAMP_OUT_OF_TOLERANCE`, `RECEIVER_HANDLER_NOT_FOUND`, `RECEIVER_HANDLER_FAILED`
- [ ] 6.2 Each new code carries `metadata.retryHint: 'transient' | 'permanent'` — `transient` for handler-failure, `permanent` for bad-signature / unsupported-content-type / parser-invalid-body / handler-not-found / timestamp-out-of-tolerance
- [ ] 6.3 Update `error-codes.md` doc to document each code's `retryHint`
## 7. Types and schemas
- [ ] 7.1 Add `ParsedWebhookEvent<T>`, `SigningScheme`, `WebhookHandler<T>`, `WebhookHandlerRegistry`, `WebhookHookMap`, `HandleResult`, `WebhookReceiverOptions`, `WebhookVerifierOptions`, `WebhookParserOptions` to `packages/platform/webhook-client/src/types/webhook-client.types.ts`
- [ ] 7.2 Add `WebhookSchema<T>(payloadSchema)` Zod-builder helper to `packages/platform/webhook-client/src/schemas/webhook.schema.ts`
- [ ] 7.3 Sibling `.md` for the schema module
## 8. Wiring
- [ ] 8.1 Update `packages/platform/webhook-client/src/index.ts` to re-export the parser, verifier, receiver, hooks, schema-builder, and new types
- [ ] 8.2 Add subpath exports `./parser/parser/parser`, `./verifier/verifier/verifier`, `./receiver/receiver/receiver`, `./hooks/hooks/hooks`, `./schemas/webhook.schema` to `packages/platform/webhook-client/package.json`
- [ ] 8.3 Update root `README.md` `## Modules` section to link the new co-located `.md`s
## 9. Tests
- [ ] 9.1 `packages/platform/webhook-client/src/parser/parser/parser.test.ts` — JSON round-trip, form-urlencoded round-trip, unsupported content-type rejection, malformed-body rejection
- [ ] 9.2 `packages/platform/webhook-client/src/verifier/verifier/verifier.test.ts` — each scheme (stripe, github, slack, generic) round-trip; tampered signature rejected; out-of-tolerance timestamp rejected
- [ ] 9.3 `packages/platform/webhook-client/src/receiver/receiver/receiver.test.ts` — full pipeline with handler dispatch; handler-not-found path; handler-throws path
- [ ] 9.4 `packages/platform/webhook-client/src/receiver/receiver/receiver.spec-conformance.test.ts` — `webhookHooks` events fire in order (`onWebhookReceived` before `onWebhookVerified` before `onWebhookHandled`); OTel `webhook.receiver.handle.duration_ms` histogram; error codes carry `metadata.retryHint`
- [ ] 9.5 `packages/platform/webhook-client/src/hooks/hooks/hooks.test.ts` — `webhookHooks.on/off/emit` round-trip
- [ ] 9.6 `packages/platform/webhook-client/src/errors/error-codes.spec-conformance.test.ts` — every new code is registered; every new code carries `metadata.retryHint`
- [ ] 9.7 Every `it(...)` carries `{ timeout: 5000 }`
## 10. Documentation
- [ ] 10.1 Sibling `.md` for every new implementation `.ts` (≥200 words, basic + advanced examples, full API reference)
- [ ] 10.2 README links the new `.md`s under `## Modules`
- [ ] 10.3 Update `error-codes.md` to enumerate each new code with its `retryHint` and a one-line description
## 11. Pre-archive verification (the 5-gate triple)
- [ ] 11.1 `bun run check` — Biome 0/0/0 across the monorepo
- [ ] 11.2 `bunx turbo lint typecheck test --filter=@suaveplan/webhook-client` — green
- [ ] 11.3 `bunx turbo test:coverage --filter=@suaveplan/webhook-client` — 100/100/100/100
- [ ] 11.4 `bun scripts/openspec-conformance.ts @suaveplan/webhook-client` — exit 0, no `error` findings
- [ ] 11.5 `bunx turbo build --filter=@suaveplan/webhook-client` — green
## 12. Pre-archive 7-row checklist (AGENTS.md Section 13)
- [ ] 12.1 Generics threaded; `expectTypeOf` assertions on `ParsedWebhookEvent<T>` and `WebhookHandler<T>`
- [ ] 12.2 Zod schemas wired at every public boundary (`WebhookSchema`, `WebhookReceiverOptionsSchema`, `WebhookVerifierOptionsSchema`)
- [ ] 12.3 Every new error code thrown from at least one production path
- [ ] 12.4 Strict-mode tested: schemes with vs without timestamp tolerance
- [ ] 12.5 OTel histogram `webhook.receiver.handle.duration_ms` and counter `webhook.receiver.handle_total` (tagged by status) emitted from production paths and asserted with a real in-memory `MeterProvider`
- [ ] 12.6 Lifecycle dedup: handler registry rejects duplicate registrations for the same event type unless `replace: true` is set
- [ ] 12.7 No retroactive tick — every box reflects work done in this change
## 13. Archive
- [ ] 13.1 Fold spec delta into `openspec/specs/webhook/spec.md` (creating it if absent)
- [ ] 13.2 Move `openspec/changes/webhook-receiver/` → `openspec/archive/<yyyy-mm-dd>-webhook-receiver/`
- [ ] 13.3 Refresh `MANIFEST.md`