# @suaveplan/api-plugin-webhook - Signed outbound webhook delivery plugin for @suaveplan/api. Wraps @suaveplan/webhook-client behind a definePlugin descriptor, registers a typed webhookService in ServiceRegistry, mounts tenant-scoped CRUD + test routes through @suaveplan/api-openapi, drives HMAC-SHA256 delivery with exponential-backoff retry + dead-letter sink, and dedups concurrent deliveries via @suaveplan/idempotency. Signed outbound webhook delivery plugin for `@suaveplan/api`. Wraps `@suaveplan/webhook-client` behind a `definePlugin` descriptor, registers a typed `webhookService` in the host `ServiceRegistry`, mounts tenant-scoped CRUD + test routes through `@suaveplan/api-openapi`, drives HMAC-SHA256 delivery with exponential-backoff retry and a dead-letter sink, and dedups concurrent deliveries via `@suaveplan/idempotency`. ## Documentation tree - [dead-letter](./src/dead-letter/dead-letter.md) — Defines the `DeadLetterSink` interface and `DeadLetterEntry` shape used by `WebhookService.send` to persist delivery at… - [dead-letter](./src/dead-letter/dead-letter.md) — Defines the `DeadLetterSink` interface and `DeadLetterEntry` shape used by `WebhookService.send` to persist delivery at… - [memory](./src/dead-letter/memory.md) — Provides the default `DeadLetterSink` implementation shipped with the plugin: an unbounded `Array` bac… - [error-codes](./src/error-codes/error-codes.md) — Defines the ten stable `WEBHOOK_*` error codes raised by the api-plugin-webhook package and the concrete `WebhookError`… - [error-codes](./src/error-codes/error-codes.md) — Defines the ten stable `WEBHOOK_*` error codes raised by the api-plugin-webhook package and the concrete `WebhookError`… - [otel](./src/otel/instruments.md) — Defines the four OpenTelemetry instruments emitted by the webhook plugin: two counters and two histograms covering ever… - [instruments](./src/otel/instruments.md) — Defines the four OpenTelemetry instruments emitted by the webhook plugin: two counters and two histograms covering ever… - [plugin](./src/plugin/plugin.md) — Top-level `definePlugin` consumer for the webhook plugin. Returns a typed `PluginDescriptor` whose… - [plugin](./src/plugin/plugin.md) — Top-level `definePlugin` consumer for the webhook plugin. Returns a typed `PluginDescriptor` whose… - [routes](./src/routes/build-routes.md) — Builds the six CRUD + test route entries the webhook plugin mounts on the host's Hono app. Each entry pairs a Zod-typed… - [build-routes](./src/routes/build-routes.md) — Builds the six CRUD + test route entries the webhook plugin mounts on the host's Hono app. Each entry pairs a Zod-typed… - [routes](./src/schemas/routes/routes.md) — Bundles the Zod schemas for every CRUD + test route the webhook plugin mounts: list response, create body, create/read… - [routes](./src/schemas/routes/routes.md) — Bundles the Zod schemas for every CRUD + test route the webhook plugin mounts: list response, create body, create/read… - [endpoint](./src/schemas/endpoint.md) — Zod schemas for the webhook endpoint shapes — the persistent `WebhookEndpoint` row, the `POST /webhooks` create body, a… - [options](./src/schemas/options.md) — Zod schema gating `createWebhookPlugin(options)`. Validates every primitive option (route prefix, retry caps, attempt c… - [registry-augmentation](./src/services/registry-augmentation.md) — Declares the `webhookService` slot on `@suaveplan/api-plugin/service-registry`'s `ServiceRegistry` interface via TypeSc… - [webhook-service](./src/services/webhook-service.md) — Wraps `@suaveplan/webhook-client`'s `deliver` function with the plugin's orchestration layer: endpoint lookup, tenant s… - [endpoint-store](./src/store/endpoint-store.md) — Defines the pluggable persistence seam for webhook endpoints. Every method is async-only so downstream packages can plu… - [memory](./src/store/memory.md) — Provides the default `EndpointStore` shipped with the plugin: a `Map`-backed implementation suitable for tests, single-… - [types](./src/types/types.md) — Defines the cross-feature data shapes used by every module in `@suaveplan/api-plugin-webhook`: the persistent `WebhookE… - [types](./src/types/types.md) — Defines the cross-feature data shapes used by every module in `@suaveplan/api-plugin-webhook`: the persistent `WebhookE… ## Install ```bash bun add @suaveplan/api-plugin-webhook ``` ## Quick start ```ts import { defineApp } from "@suaveplan/api"; import { createAuthPlugin } from "@suaveplan/api-plugin-auth"; import { createWebhookPlugin } from "@suaveplan/api-plugin-webhook"; const app = defineApp() .use(createAuthPlugin({ betterAuth: { /* ... */ } })) .use(createWebhookPlugin()); // Register and fire from anywhere with access to the ServiceRegistry: const webhookService = registry.getService("webhookService"); const endpoint = await webhookService.register({ tenantId: null, url: "https://partner.example.com/hooks", secret: crypto.randomUUID(), events: ["order.created"], }); await webhookService.send(endpoint.id, "order.created", { orderId: 42 }); ``` ## Surface Six CRUD + test routes mounted under `routePrefix` (default `/webhooks`), every route gated by `requireAuth()` from `@suaveplan/api-plugin-auth` and scoped by the request principal's `tenantId` when present: | Method | Path | Purpose | |---|---|---| | `GET` | `/webhooks` | List endpoints visible to the principal | | `POST` | `/webhooks` | Create an endpoint (caller supplies the HMAC secret) | | `GET` | `/webhooks/:id` | Read one endpoint | | `PATCH` | `/webhooks/:id` | Partial update | | `DELETE` | `/webhooks/:id` | Hard delete (idempotent) | | `POST` | `/webhooks/:id/test` | Fire a synthetic `webhook.ping` | The same surface is available programmatically via the `webhookService` slot in `ServiceRegistry`: - `send(endpointId, event, data)` - `sendAll(event, data, filter?)` - `register(input)` / `update(id, patch)` / `unregister(id)` - `listEndpoints(filter?)` / `getEndpoint(id)` - `test(endpointId)` ## Delivery semantics - HMAC-SHA256 default (SHA-512 opt-in per endpoint), header naming follows webhook-client (`X-Hub-Signature-256` / `X-Hub-Signature-512`). - Standard outbound headers: `Content-Type: application/json`, `X-Webhook-Id`, `X-Webhook-Event`, `X-Webhook-Timestamp`. - Exponential backoff with full jitter, doubling per attempt, capped at `maxRetryDelayMs`. - `maxAttempts` defaults to 5; 4xx (except 429) terminates immediately; 429 and 5xx retry; network errors and timeouts retry. - Exhausted deliveries land in the `DeadLetterSink` with the full attempt history before surfacing `WEBHOOK_DELIVERY_EXHAUSTED` to the caller. - Per-delivery dedup via `(endpointId, payload.id)` — concurrent same-id sends collapse to a single delivery, either through the injected `DistributedIdempotencyGuard` or an in-process `Map`. ## Persistence The plugin ships `MemoryEndpointStore` (default) and `MemoryDeadLetterSink` (default) so it boots zero-config. Production deployments configure durable siblings against the `EndpointStore` / `DeadLetterSink` interfaces — a SurrealDB-backed store lands as `@suaveplan/api-plugin-webhook-surreal`. ## Modules - [`src/plugin/plugin.md`](./src/plugin/plugin.md) — `createWebhookPlugin` factory + onInit lifecycle. - [`src/services/webhook-service.md`](./src/services/webhook-service.md) — `WebhookService` impl. - [`src/routes/build-routes.md`](./src/routes/build-routes.md) — CRUD + test routes. - [`src/store/endpoint-store.md`](./src/store/endpoint-store.md) — `EndpointStore` interface. - [`src/store/memory.md`](./src/store/memory.md) — default in-memory store. - [`src/dead-letter/dead-letter.md`](./src/dead-letter/dead-letter.md) — `DeadLetterSink` interface. - [`src/dead-letter/memory.md`](./src/dead-letter/memory.md) — default in-memory sink. - [`src/schemas/options.md`](./src/schemas/options.md) — options Zod schema. - [`src/schemas/endpoint.md`](./src/schemas/endpoint.md) — endpoint Zod schemas. - [`src/schemas/routes/routes.md`](./src/schemas/routes/routes.md) — per-route Zod schemas. - [`src/error-codes/error-codes.md`](./src/error-codes/error-codes.md) — `WEBHOOK_*` error registry. - [`src/otel/instruments.md`](./src/otel/instruments.md) — OpenTelemetry instruments. - [`src/types/types.md`](./src/types/types.md) — shared types. ## OpenTelemetry - Counter `webhook_plugin.deliveries_total{event,status,endpoint_id}` - Counter `webhook_plugin.dead_letters_total{event,endpoint_id}` - Histogram `webhook_plugin.delivery_attempts{event,status}` - Histogram `webhook_plugin.send_duration_ms{status}` Status label values: `delivered`, `exhausted`, `not_found`, `inactive`, `invalid`. ## License MIT