* Verify a Resend webhook signature using the Svix signing scheme. * Resend uses Svix under the hood: HMAC-SHA256 of `${svix-id}.${svix-timestamp}.${body}` * signed with the base64-decoded `whsec_...` secret.
( secret: string, msgId: string, timestamp: string, signatures: string, rawBody: string )
| 26 | * signed with the base64-decoded `whsec_...` secret. |
| 27 | */ |
| 28 | function verifySvixSignature( |
| 29 | secret: string, |
| 30 | msgId: string, |
| 31 | timestamp: string, |
| 32 | signatures: string, |
| 33 | rawBody: string |
| 34 | ): boolean { |
| 35 | try { |
| 36 | const ts = Number.parseInt(timestamp, 10) |
| 37 | const now = Math.floor(Date.now() / 1000) |
| 38 | if (Number.isNaN(ts) || Math.abs(now - ts) > 5 * 60) { |
| 39 | return false |
| 40 | } |
| 41 | |
| 42 | const secretBytes = Buffer.from(secret.replace(/^whsec_/, ''), 'base64') |
| 43 | const toSign = `${msgId}.${timestamp}.${rawBody}` |
| 44 | const expectedSignature = hmacSha256Base64(toSign, secretBytes) |
| 45 | |
| 46 | const providedSignatures = signatures.split(' ') |
| 47 | for (const versionedSig of providedSignatures) { |
| 48 | const parts = versionedSig.split(',') |
| 49 | if (parts.length !== 2) continue |
| 50 | const sig = parts[1] |
| 51 | if (safeCompare(sig, expectedSignature)) { |
| 52 | return true |
| 53 | } |
| 54 | } |
| 55 | return false |
| 56 | } catch (error) { |
| 57 | logger.error('Error verifying Resend Svix signature:', error) |
| 58 | return false |
| 59 | } |
| 60 | } |
| 61 | |
| 62 | export const resendHandler: WebhookProviderHandler = { |
| 63 | async verifyAuth({ |
no test coverage detected