security(voice-call): detect Telnyx webhook replay

This commit is contained in:
Brian Mendonca
2026-02-24 14:42:00 -07:00
committed by Peter Steinberger
parent 53f9b7d4e7
commit a3c4f56b0b
4 changed files with 80 additions and 3 deletions

View File

@@ -1,6 +1,10 @@
import crypto from "node:crypto";
import { describe, expect, it } from "vitest";
import { verifyPlivoWebhook, verifyTwilioWebhook } from "./webhook-security.js";
import {
verifyPlivoWebhook,
verifyTelnyxWebhook,
verifyTwilioWebhook,
} from "./webhook-security.js";
function canonicalizeBase64(input: string): string {
return Buffer.from(input, "base64").toString("base64");
@@ -199,6 +203,37 @@ describe("verifyPlivoWebhook", () => {
});
});
describe("verifyTelnyxWebhook", () => {
it("marks replayed valid requests as replay without failing auth", () => {
const { publicKey, privateKey } = crypto.generateKeyPairSync("ed25519");
const pemPublicKey = publicKey.export({ format: "pem", type: "spki" }).toString();
const timestamp = String(Math.floor(Date.now() / 1000));
const rawBody = JSON.stringify({
data: { event_type: "call.initiated", payload: { call_control_id: "call-1" } },
nonce: crypto.randomUUID(),
});
const signedPayload = `${timestamp}|${rawBody}`;
const signature = crypto.sign(null, Buffer.from(signedPayload), privateKey).toString("base64");
const ctx = {
headers: {
"telnyx-signature-ed25519": signature,
"telnyx-timestamp": timestamp,
},
rawBody,
url: "https://example.com/voice/webhook",
method: "POST" as const,
};
const first = verifyTelnyxWebhook(ctx, pemPublicKey);
const second = verifyTelnyxWebhook(ctx, pemPublicKey);
expect(first.ok).toBe(true);
expect(first.isReplay).toBeFalsy();
expect(second.ok).toBe(true);
expect(second.isReplay).toBe(true);
});
});
describe("verifyTwilioWebhook", () => {
it("uses request query when publicUrl omits it", () => {
const authToken = "test-auth-token";