Closes the "webhook management UI and delivery retry visibility" roadmap item. Delivery previously recorded a bare pending/delivered/failed with attempts fixed at 1, and there was no way to see or retry anything. Every attempt now records its HTTP status, response or error snippet, duration, attempt count, and next retry time. Failures retry automatically with exponential backoff (1m, 2m, 4m, 8m, capped at an hour) up to the endpoint's max_attempts, then settle as "exhausted". Retries ride the existing OUTBOUND_QUEUE with a delaySeconds rather than introducing a third queue binding, so nothing new has to be provisioned; isWebhookRetryMessage discriminates them from outbound mail and env.d.ts widens the queue's type to the union accordingly. The admin page gains full management: add/enable/disable/delete, event selection, per-endpoint delivery stats, a test delivery button, and a deliveries table with manual retry. Delivery payloads now also carry X-Email-Platform-Delivery and -Attempt headers alongside the existing HMAC signature. Also returns 401 instead of 500 on unauthenticated requests to these routes, via a new requireSessionUser helper; requireUser throws a bare Error, which Next surfaces as a 500. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PGeWDVSkXtVEmDAN2AtB9h
22 lines
662 B
TypeScript
22 lines
662 B
TypeScript
import type { InboundQueueMessage } from "./src/lib/email/inbound";
|
|
import type { WebhookRetryMessage } from "./src/lib/email/webhooks";
|
|
|
|
export function isInboundQueueMessage(payload: unknown): payload is InboundQueueMessage {
|
|
return (
|
|
typeof payload === "object" &&
|
|
payload !== null &&
|
|
"rawR2Key" in payload &&
|
|
"from" in payload &&
|
|
"to" in payload
|
|
);
|
|
}
|
|
|
|
export function isWebhookRetryMessage(payload: unknown): payload is WebhookRetryMessage {
|
|
return (
|
|
typeof payload === "object" &&
|
|
payload !== null &&
|
|
(payload as { kind?: unknown }).kind === "webhook.retry" &&
|
|
typeof (payload as { deliveryId?: unknown }).deliveryId === "string"
|
|
);
|
|
}
|