first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -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.