diff --git a/libs/growth/src/lib/webhooks.spec.ts b/libs/growth/src/lib/webhooks.spec.ts index 0ffb7b461..c748c4db3 100644 --- a/libs/growth/src/lib/webhooks.spec.ts +++ b/libs/growth/src/lib/webhooks.spec.ts @@ -312,6 +312,41 @@ describe('processVerifiedResendWebhook', () => { } }); + it('accepts the documented Resend payload shape: message_id present and null optional ids', async () => { + // Resend's wire payload gained `message_id` after SDK 6.10 pinned its + // types, and transactional mail carries null broadcast/template ids. + // Both must parse, or every real event answers 400 and delivery state + // never leaves "submitted". + const harness = webhookHarness(); + + await expect( + processVerifiedResendWebhook( + harness.executor, + { + providerEventId: 'msg_documented_shape', + payload: event('email.delivered', { + broadcast_id: null, + template_id: null, + message_id: '<111-222-333@email.example.com>', + }), + }, + harness.dependencies + ) + ).resolves.toMatchObject({ applied: true }); + }); + + it('still rejects unknown data keys so the closed schema stays enforced', async () => { + const harness = executorWith({}); + + await expect( + processVerifiedResendWebhook(harness.executor, { + providerEventId: 'msg_unknown_key', + payload: event('email.delivered', { headers: [] }), + }) + ).rejects.toThrow(/Invalid Resend webhook payload/u); + expect(harness.runTransaction).not.toHaveBeenCalled(); + }); + it('marks only a permanent bounce as a hard-bounce stop', async () => { const hard = webhookHarness(); await processVerifiedResendWebhook( diff --git a/libs/growth/src/lib/webhooks.ts b/libs/growth/src/lib/webhooks.ts index 6fd3eae27..3347ef6f7 100644 --- a/libs/growth/src/lib/webhooks.ts +++ b/libs/growth/src/lib/webhooks.ts @@ -32,11 +32,14 @@ const DELIVERY_STATUS_PRECEDENCE: Readonly< suppressed: 3, complained: 4, }; +// Resend's wire payload gained `message_id` after the pinned SDK types were +// written, and transactional mail carries null broadcast/template ids. const BASE_DATA_KEYS = new Set([ 'broadcast_id', 'created_at', 'email_id', 'from', + 'message_id', 'subject', 'tags', 'template_id', @@ -184,9 +187,14 @@ function validateProviderBaseData(data: Record): void { throw new Error('Invalid Resend webhook payload'); } for (const recipient of data['to']) boundedText(recipient, 254); - if (data['broadcast_id'] !== undefined) - boundedText(data['broadcast_id'], 256); - if (data['template_id'] !== undefined) boundedText(data['template_id'], 256); + for (const key of ['broadcast_id', 'template_id'] as const) { + if (data[key] !== undefined && data[key] !== null) { + boundedText(data[key], 256); + } + } + if (data['message_id'] !== undefined && data['message_id'] !== null) { + boundedText(data['message_id'], 998); + } } function validateClosedDetails(