feat(webhooks): admin replay for failed/dead-letter deliveries

Outbound webhook deliveries already retry with backoff, dead-letter
after maxAttempts, and notify super admins. This adds operator-level
replay: a per-row button on the deliveries log spawns a fresh pending
delivery + queues a new BullMQ job. The original failed row stays
intact so the response body remains for audit; the replay payload
carries retried_from/retried_at markers so receivers can deduplicate.

Inbound idempotency was already handled via the documentEvents
signatureHash unique index.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matt Ciaccio
2026-05-06 19:31:34 +02:00
parent 7274baf1e1
commit 44db579988
3 changed files with 137 additions and 0 deletions

View File

@@ -0,0 +1,30 @@
import { NextResponse } from 'next/server';
import { withAuth, withPermission } from '@/lib/api/helpers';
import { redeliverWebhookDelivery } from '@/lib/services/webhooks.service';
import { errorResponse, NotFoundError } from '@/lib/errors';
/**
* Admin replay for a previously failed/dead-letter webhook delivery.
* Spawns a fresh `pending` row + enqueues a new BullMQ job so the
* original delivery's failure response is preserved for audit while
* the replay flows through the standard worker (HMAC-signed, SSRF
* gated, dead-lettered after max retries).
*/
export const POST = withAuth(
withPermission('admin', 'manage_webhooks', async (_req, ctx, params) => {
try {
const { webhookId, deliveryId } = params;
if (!webhookId || !deliveryId) throw new NotFoundError('Delivery');
const result = await redeliverWebhookDelivery(ctx.portId, webhookId, deliveryId, {
userId: ctx.userId,
portId: ctx.portId,
ipAddress: ctx.ipAddress,
userAgent: ctx.userAgent,
});
return NextResponse.json({ data: result });
} catch (error) {
return errorResponse(error);
}
}),
);