8.7 KiB
@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 — Defines the
DeadLetterSinkinterface andDeadLetterEntryshape used byWebhookService.sendto persist delivery at…- dead-letter — Defines the
DeadLetterSinkinterface andDeadLetterEntryshape used byWebhookService.sendto persist delivery at… - memory — Provides the default
DeadLetterSinkimplementation shipped with the plugin: an unboundedArray<DeadLetterEntry>bac…
- dead-letter — Defines the
- error-codes — Defines the ten stable
WEBHOOK_*error codes raised by the api-plugin-webhook package and the concreteWebhookError…- error-codes — Defines the ten stable
WEBHOOK_*error codes raised by the api-plugin-webhook package and the concreteWebhookError…
- error-codes — Defines the ten stable
- otel — Defines the four OpenTelemetry instruments emitted by the webhook plugin: two counters and two histograms covering ever…
- instruments — Defines the four OpenTelemetry instruments emitted by the webhook plugin: two counters and two histograms covering ever…
- plugin — Top-level
definePluginconsumer for the webhook plugin. Returns a typedPluginDescriptor<WebhookPluginConfig>whose…- plugin — Top-level
definePluginconsumer for the webhook plugin. Returns a typedPluginDescriptor<WebhookPluginConfig>whose…
- plugin — Top-level
- routes — 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 — Builds the six CRUD + test route entries the webhook plugin mounts on the host's Hono app. Each entry pairs a Zod-typed…
- routes — Bundles the Zod schemas for every CRUD + test route the webhook plugin mounts: list response, create body, create/read…
- routes — Bundles the Zod schemas for every CRUD + test route the webhook plugin mounts: list response, create body, create/read…
- endpoint — Zod schemas for the webhook endpoint shapes — the persistent
WebhookEndpointrow, thePOST /webhookscreate body, a… - options — Zod schema gating
createWebhookPlugin(options). Validates every primitive option (route prefix, retry caps, attempt c… - registry-augmentation — Declares the
webhookServiceslot on@suaveplan/api-plugin/service-registry'sServiceRegistryinterface via TypeSc… - webhook-service — Wraps
@suaveplan/webhook-client'sdeliverfunction with the plugin's orchestration layer: endpoint lookup, tenant s… - endpoint-store — Defines the pluggable persistence seam for webhook endpoints. Every method is async-only so downstream packages can plu…
- memory — Provides the default
EndpointStoreshipped with the plugin: aMap-backed implementation suitable for tests, single-… - types — Defines the cross-feature data shapes used by every module in
@suaveplan/api-plugin-webhook: the persistent `WebhookE…- types — Defines the cross-feature data shapes used by every module in
@suaveplan/api-plugin-webhook: the persistent `WebhookE…
- types — Defines the cross-feature data shapes used by every module in
Install
bun add @suaveplan/api-plugin-webhook
Quick start
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. maxAttemptsdefaults to 5; 4xx (except 429) terminates immediately; 429 and 5xx retry; network errors and timeouts retry.- Exhausted deliveries land in the
DeadLetterSinkwith the full attempt history before surfacingWEBHOOK_DELIVERY_EXHAUSTEDto the caller. - Per-delivery dedup via
(endpointId, payload.id)— concurrent same-id sends collapse to a single delivery, either through the injectedDistributedIdempotencyGuardor an in-processMap.
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—createWebhookPluginfactory + onInit lifecycle.src/services/webhook-service.md—WebhookServiceimpl.src/routes/build-routes.md— CRUD + test routes.src/store/endpoint-store.md—EndpointStoreinterface.src/store/memory.md— default in-memory store.src/dead-letter/dead-letter.md—DeadLetterSinkinterface.src/dead-letter/memory.md— default in-memory sink.src/schemas/options.md— options Zod schema.src/schemas/endpoint.md— endpoint Zod schemas.src/schemas/routes/routes.md— per-route Zod schemas.src/error-codes/error-codes.md—WEBHOOK_*error registry.src/otel/instruments.md— OpenTelemetry instruments.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