Initial Commit
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
# Webhook route schemas
|
||||
|
||||
## Purpose
|
||||
|
||||
The webhook-route schemas describe the OpenAPI contract for the
|
||||
`/webhooks` route tree, plus a `decodeUnsubscribeToken` helper. The
|
||||
delivery webhook accepts a normalised `DeliveryEvent` payload (the
|
||||
provider-specific parser lives in `@suaveplan/delivery-tracking`'s
|
||||
`WebhookEventParser`; this plugin only handles already-normalised
|
||||
events). The unsubscribe endpoint decodes a base64-JSON token carrying
|
||||
`{ contactId, campaignId? }` and falls through to the suppression
|
||||
store.
|
||||
|
||||
The token format is deliberately lightweight — base64-encoded JSON
|
||||
with no signature. Production deployments SHOULD wrap this endpoint in
|
||||
a per-tenant HMAC verification layer (e.g. via the rate-limit or
|
||||
auth-plugin middleware chain) before the token decode happens. This
|
||||
module is the parsing primitive; identity verification is layered on
|
||||
above.
|
||||
|
||||
## Features
|
||||
|
||||
- `DeliveryEventTypeSchema` — 10-member enum.
|
||||
- `DeliveryWebhookSchema` — body validator with `channel: "email"`
|
||||
default and ISO 8601 timestamp.
|
||||
- `UnsubscribeTokenParamSchema` — `:token` path validator.
|
||||
- `DeliveryWebhookResponseSchema`, `UnsubscribeResponseSchema` —
|
||||
response shapes.
|
||||
- `decodeUnsubscribeToken(token)` — base64 → JSON → Zod-validated
|
||||
payload, with three distinct error messages so the route handler
|
||||
can map all to `INVALID_UNSUBSCRIBE_TOKEN`.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { DeliveryWebhookSchema } from "@suaveplan/api-plugin-campaigns";
|
||||
|
||||
const event = DeliveryWebhookSchema.parse(await c.req.json());
|
||||
await store.record({ ...event, tenantId, id: crypto.randomUUID() });
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import {
|
||||
CAMPAIGNS_API_CODES,
|
||||
CampaignsApiError,
|
||||
decodeUnsubscribeToken,
|
||||
} from "@suaveplan/api-plugin-campaigns";
|
||||
|
||||
let payload;
|
||||
try {
|
||||
payload = decodeUnsubscribeToken(c.req.param("token"));
|
||||
} catch (cause) {
|
||||
throw new CampaignsApiError(
|
||||
CAMPAIGNS_API_CODES.INVALID_UNSUBSCRIBE_TOKEN,
|
||||
"could not decode unsubscribe token",
|
||||
{ cause, context: { statusCode: 400 } }
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `DeliveryEventTypeSchema: ZodEnum<10>`
|
||||
- `DeliveryWebhookSchema: ZodObject<...>`
|
||||
- `UnsubscribeTokenParamSchema: ZodObject<{ token: ZodString }>`
|
||||
- `DeliveryWebhookResponseSchema`, `UnsubscribeResponseSchema` —
|
||||
response shapes.
|
||||
- `decodeUnsubscribeToken(token: string): UnsubscribeTokenPayload` —
|
||||
throws `Error` on any failure mode.
|
||||
- `type DeliveryWebhookBody`, `type UnsubscribeTokenPayload`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`decodeUnsubscribeToken` throws plain `Error` (not
|
||||
`CampaignsApiError`) because the helper is consumed from a single
|
||||
route-handler site that wraps every throw in
|
||||
`CampaignsApiError(INVALID_UNSUBSCRIBE_TOKEN)`. Centralising the wrap
|
||||
at the call site keeps the helper portable for future consumers that
|
||||
may wish to map the failure mode differently (e.g. a CLI tool issuing
|
||||
test tokens). The three distinct error messages — base64,
|
||||
JSON, contactId — are preserved so server-side logs can distinguish
|
||||
between a tampered token (likely malicious), a malformed token (likely
|
||||
truncated by a mail-client URL-shortener), and a structurally-valid
|
||||
but content-invalid token (likely a tenant-config bug).
|
||||
Reference in New Issue
Block a user