first commit
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
# Webhook routes
|
||||
|
||||
## Purpose
|
||||
|
||||
The webhook-route module owns the two webhook endpoints: a delivery
|
||||
event ingestion endpoint that normalises and persists provider events
|
||||
through `DeliveryEventStore`, and a one-click unsubscribe endpoint
|
||||
driven by a base64-JSON token. The delivery handler is the authoritative
|
||||
fan-out point: it persists the event and — for the three "negative"
|
||||
events (`unsubscribed`, `bounced`, `complained`) — additionally
|
||||
inserts a `CampaignSuppressionEntry` so the sender pipeline can
|
||||
short-circuit future deliveries to the same contact.
|
||||
|
||||
The unsubscribe endpoint hardcodes `channel: "email"` because the
|
||||
token format does not carry a channel field (the design assumes
|
||||
unsubscribe links are always embedded in email messages). Multi-channel
|
||||
unsubscribe is a future extension — when that ships, the token shape
|
||||
will gain a `channel` field and the handler will decode it.
|
||||
|
||||
## Features
|
||||
|
||||
- 2 builders: `buildDeliveryWebhookRoute`, `buildUnsubscribeRoute`.
|
||||
- Delivery handler fan-out: records the event, then optionally adds
|
||||
a suppression entry for `unsubscribed` / `bounced` / `complained`.
|
||||
- Idempotent suppression: a duplicate `add` is short-circuited by a
|
||||
pre-existing `check` hit, so retries from the provider do not raise
|
||||
`DUPLICATE_SUPPRESSION`.
|
||||
- Strict tenant guard: every handler enforces `requireTenant` so
|
||||
provider webhooks must traverse a tenant-aware auth layer before
|
||||
hitting the handler.
|
||||
- Token decode failures map to `INVALID_UNSUBSCRIBE_TOKEN` (HTTP 400);
|
||||
schema failures on the delivery body map to `WEBHOOK_REJECTED`.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import {
|
||||
buildDeliveryWebhookRoute,
|
||||
buildUnsubscribeRoute,
|
||||
} from "@suaveplan/api-plugin-campaigns";
|
||||
|
||||
app.post(buildDeliveryWebhookRoute().route.path, (c) =>
|
||||
buildDeliveryWebhookRoute().handler(c)
|
||||
);
|
||||
app.get(buildUnsubscribeRoute().route.path, (c) =>
|
||||
buildUnsubscribeRoute().handler(c)
|
||||
);
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import {
|
||||
buildDeliveryWebhookRoute,
|
||||
buildUnsubscribeRoute,
|
||||
definePluginRoutes,
|
||||
} from "@suaveplan/api-plugin-campaigns";
|
||||
|
||||
definePluginRoutes(
|
||||
[buildDeliveryWebhookRoute(), buildUnsubscribeRoute()],
|
||||
{ app, registry, pluginName: "campaigns" }
|
||||
);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `buildDeliveryWebhookRoute(): PluginRouteEntry`
|
||||
- `buildUnsubscribeRoute(): PluginRouteEntry`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The negative-event mapping (`unsubscribed → unsubscribe`,
|
||||
`bounced → bounce`, `complained → complaint`) is exhaustive but
|
||||
intentionally narrow — we do not auto-suppress on `dropped` or
|
||||
`deferred` because those are transient provider states that should
|
||||
trigger investigation rather than permanent suppression. A future
|
||||
change can expand the mapping if operators request a `dropped → manual`
|
||||
auto-suppression policy. The handler returns `{ eventId, suppressed }`
|
||||
so the calling provider can correlate retries against a stable id and
|
||||
operators can confirm via response inspection whether the fan-out
|
||||
fired.
|
||||
Reference in New Issue
Block a user