* Verify an incident.io webhook signature using the Svix signing scheme. * incident.io webhooks are powered by Svix: HMAC-SHA256 of * `${webhook-id}.${webhook-timestamp}.${body}` signed with the base64-decoded * `whsec_...` secret, compared against the `webhook-signature` header which may * carr
( secret: string, msgId: string, timestamp: string, signatures: string, rawBody: string )
| 23 | * @see https://docs.incident.io/integrations/webhooks |
| 24 | */ |
| 25 | function verifyIncidentioSignature( |
| 26 | secret: string, |
| 27 | msgId: string, |
| 28 | timestamp: string, |
| 29 | signatures: string, |
| 30 | rawBody: string |
| 31 | ): boolean { |
| 32 | try { |
| 33 | const ts = Number.parseInt(timestamp, 10) |
| 34 | const now = Math.floor(Date.now() / 1000) |
| 35 | if (Number.isNaN(ts) || Math.abs(now - ts) > INCIDENTIO_WEBHOOK_TIMESTAMP_SKEW_SECONDS) { |
| 36 | return false |
| 37 | } |
| 38 | |
| 39 | const secretBytes = Buffer.from(secret.replace(/^whsec_/, ''), 'base64') |
| 40 | const toSign = `${msgId}.${timestamp}.${rawBody}` |
| 41 | const expectedSignature = hmacSha256Base64(toSign, secretBytes) |
| 42 | |
| 43 | const providedSignatures = signatures.split(' ') |
| 44 | for (const versionedSig of providedSignatures) { |
| 45 | const parts = versionedSig.split(',') |
| 46 | if (parts.length !== 2) continue |
| 47 | const sig = parts[1] |
| 48 | if (safeCompare(sig, expectedSignature)) { |
| 49 | return true |
| 50 | } |
| 51 | } |
| 52 | return false |
| 53 | } catch (error) { |
| 54 | logger.error('Error verifying incident.io Svix signature:', error) |
| 55 | return false |
| 56 | } |
| 57 | } |
| 58 | |
| 59 | function asObject(value: unknown): Record<string, unknown> | null { |
| 60 | if (value && typeof value === 'object' && !Array.isArray(value)) { |
no test coverage detected