diff --git a/packages/payments/coinpay/src/index.test.ts b/packages/payments/coinpay/src/index.test.ts index 0fcf43cb..7d899050 100644 --- a/packages/payments/coinpay/src/index.test.ts +++ b/packages/payments/coinpay/src/index.test.ts @@ -194,6 +194,22 @@ describe('payment-coinpay', () => { { webhookToleranceSeconds: 0 }, )).rejects.toThrow('CoinPay signature timestamp is invalid'); }); + + it('falls back to the default timestamp tolerance for invalid config values', async () => { + const raw = JSON.stringify({ type: 'payment.confirmed' }); + const secret = 'whsec_test'; + const timestamp = String(Math.floor(Date.now() / 1000) - 600); + const signature = createHmac('sha256', secret) + .update(`${timestamp}.${raw}`) + .digest('hex'); + + await expect(payment.verifyWebhook( + ctx({ COINPAY_WEBHOOK_SECRET: secret }), + raw, + `t=${timestamp},v1=${signature}`, + { webhookToleranceSeconds: Number.NaN }, + )).rejects.toThrow('CoinPay webhook signature timestamp outside tolerance'); + }); }); function ctx(secrets: Record) { diff --git a/packages/payments/coinpay/src/index.ts b/packages/payments/coinpay/src/index.ts index 2f615699..526591f1 100644 --- a/packages/payments/coinpay/src/index.ts +++ b/packages/payments/coinpay/src/index.ts @@ -183,6 +183,8 @@ function verifyCoinPaySignature( secret: string, toleranceSeconds = 300, ): void { + const effectiveToleranceSeconds = normalizeWebhookToleranceSeconds(toleranceSeconds); + // Parse all key=value pairs, preserving duplicate keys as arrays. // CoinPay can send multiple v1 signatures during secret rotation; // using Object.fromEntries() would silently drop all but one. @@ -207,9 +209,9 @@ function verifyCoinPaySignature( const timestampSeconds = parseWebhookTimestamp(timestamp); if (timestampSeconds === undefined) throw new Error('CoinPay signature timestamp is invalid'); - if (toleranceSeconds > 0) { + if (effectiveToleranceSeconds > 0) { const age = Math.floor(Date.now() / 1000) - timestampSeconds; - if (Math.abs(age) > toleranceSeconds) throw new Error('CoinPay webhook signature timestamp outside tolerance'); + if (Math.abs(age) > effectiveToleranceSeconds) throw new Error('CoinPay webhook signature timestamp outside tolerance'); } const expected = createHmac('sha256', secret) @@ -232,6 +234,12 @@ function verifyCoinPaySignature( } } +function normalizeWebhookToleranceSeconds(value: number): number { + if (value === 0) return 0; + if (Number.isFinite(value) && value > 0) return value; + return 300; +} + function parseWebhookTimestamp(timestamp: string): number | undefined { if (!/^\d+$/.test(timestamp)) return undefined; const parsed = Number(timestamp);