diff --git a/packages/bots/telnyx/src/index.test.ts b/packages/bots/telnyx/src/index.test.ts index 85057bef..5976ec1d 100644 --- a/packages/bots/telnyx/src/index.test.ts +++ b/packages/bots/telnyx/src/index.test.ts @@ -184,6 +184,45 @@ describe('bot-telnyx webhook behavior', () => { await handle.close(); } }); + + it('falls back to the default signature tolerance for invalid config values', async () => { + const keys = generateKeyPairSync('ed25519'); + const publicKey = Buffer.from(keys.publicKey.export({ format: 'der', type: 'spki' })).toString('base64'); + const handler = vi.fn(); + const handle = await bot.register(ctx(), [{ match: { type: 'message' }, handle: handler }], { + from: '+15551234567', + webhookPort: 0, + publicKey, + signatureToleranceSeconds: Number.POSITIVE_INFINITY, + }) as { close(): Promise; port: number }; + + try { + const body = JSON.stringify({ + data: { + event_type: 'message.received', + payload: { + from: { phone_number: '+15559876543' }, + text: 'hello', + }, + }, + }); + const staleTimestamp = String(Math.floor(Date.now() / 1000) - 600); + const response = await fetch(`http://127.0.0.1:${handle.port}/telnyx/messaging`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'telnyx-timestamp': staleTimestamp, + 'telnyx-signature-ed25519': signature(keys.privateKey, staleTimestamp, body), + }, + body, + }); + + expect(response.status).toBe(403); + expect(handler).not.toHaveBeenCalled(); + } finally { + await handle.close(); + } + }); }); function ctx() { diff --git a/packages/bots/telnyx/src/index.ts b/packages/bots/telnyx/src/index.ts index 03e81b4a..e7de5913 100644 --- a/packages/bots/telnyx/src/index.ts +++ b/packages/bots/telnyx/src/index.ts @@ -130,7 +130,7 @@ class TelnyxWebhookServer { const signature = request.headers['telnyx-signature-ed25519']; const timestamp = request.headers['telnyx-timestamp']; if (typeof signature !== 'string' || typeof timestamp !== 'string') return false; - const tolerance = this.config.signatureToleranceSeconds ?? DEFAULT_SIGNATURE_TOLERANCE_SECONDS; + const tolerance = normalizeSignatureToleranceSeconds(this.config.signatureToleranceSeconds); if (!validTimestamp(timestamp, tolerance)) return false; return verifyTelnyxSignature(publicKey, timestamp, body, signature); } @@ -307,6 +307,12 @@ function validTimestamp(value: string, toleranceSeconds: number): boolean { return Math.abs(Date.now() / 1000 - parsed) <= toleranceSeconds; } +function normalizeSignatureToleranceSeconds(value: number | undefined): number { + if (value === undefined) return DEFAULT_SIGNATURE_TOLERANCE_SECONDS; + if (Number.isFinite(value) && value >= 0) return value; + return DEFAULT_SIGNATURE_TOLERANCE_SECONDS; +} + function parseEvent(body: string): TelnyxEvent { const parsed = parseJson(body); return typeof parsed === 'object' && parsed ? parsed as TelnyxEvent : {};