* Verify a Linq webhook signature using the Standard Webhooks scheme. * Linq signs `${webhook-id}.${webhook-timestamp}.${rawBody}` with HMAC-SHA256 using * the base64-decoded `whsec_...` signing secret, and delivers the result as one or * more space-separated `v1, ` signatures in the `webh
( secret: string, msgId: string, timestamp: string, signatures: string, rawBody: string )
| 28 | * more space-separated `v1,<base64>` signatures in the `webhook-signature` header. |
| 29 | */ |
| 30 | function verifyLinqSignature( |
| 31 | secret: string, |
| 32 | msgId: string, |
| 33 | timestamp: string, |
| 34 | signatures: string, |
| 35 | rawBody: string |
| 36 | ): boolean { |
| 37 | try { |
| 38 | const ts = Number.parseInt(timestamp, 10) |
| 39 | const now = Math.floor(Date.now() / 1000) |
| 40 | if (Number.isNaN(ts) || Math.abs(now - ts) > MAX_TIMESTAMP_SKEW_SECONDS) { |
| 41 | return false |
| 42 | } |
| 43 | |
| 44 | const secretBytes = Buffer.from(secret.replace(/^whsec_/, ''), 'base64') |
| 45 | const toSign = `${msgId}.${timestamp}.${rawBody}` |
| 46 | const expectedSignature = hmacSha256Base64(toSign, secretBytes) |
| 47 | |
| 48 | for (const versionedSig of signatures.split(' ')) { |
| 49 | const parts = versionedSig.split(',') |
| 50 | if (parts.length !== 2) continue |
| 51 | if (safeCompare(parts[1], expectedSignature)) { |
| 52 | return true |
| 53 | } |
| 54 | } |
| 55 | return false |
| 56 | } catch (error) { |
| 57 | logger.error('Error verifying Linq webhook signature:', error) |
| 58 | return false |
| 59 | } |
| 60 | } |
| 61 | |
| 62 | /** Parse a comma/whitespace-separated list of phone numbers into a clean array. */ |
| 63 | function parsePhoneNumbers(value: unknown): string[] { |
no test coverage detected