# 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).