refactor(voice-call): centralize Telnyx webhook verification

This commit is contained in:
Peter Steinberger
2026-02-14 19:00:14 +01:00
parent 8852250192
commit f47584fec8
6 changed files with 206 additions and 59 deletions

View File

@@ -14,6 +14,7 @@ import type {
WebhookVerificationResult,
} from "../types.js";
import type { VoiceCallProvider } from "./base.js";
import { verifyTelnyxWebhook } from "../webhook-security.js";
/**
* Telnyx Voice API provider implementation.
@@ -82,66 +83,11 @@ export class TelnyxProvider implements VoiceCallProvider {
* Verify Telnyx webhook signature using Ed25519.
*/
verifyWebhook(ctx: WebhookContext): WebhookVerificationResult {
if (this.options.skipVerification) {
console.warn("[telnyx] Webhook verification skipped (skipSignatureVerification=true)");
return { ok: true, reason: "verification skipped (skipSignatureVerification=true)" };
}
const result = verifyTelnyxWebhook(ctx, this.publicKey, {
skipVerification: this.options.skipVerification,
});
if (!this.publicKey) {
return {
ok: false,
reason: "Missing telnyx.publicKey (configure to verify webhooks)",
};
}
const signature = ctx.headers["telnyx-signature-ed25519"];
const timestamp = ctx.headers["telnyx-timestamp"];
if (!signature || !timestamp) {
return { ok: false, reason: "Missing signature or timestamp header" };
}
const signatureStr = Array.isArray(signature) ? signature[0] : signature;
const timestampStr = Array.isArray(timestamp) ? timestamp[0] : timestamp;
if (!signatureStr || !timestampStr) {
return { ok: false, reason: "Empty signature or timestamp" };
}
try {
const signedPayload = `${timestampStr}|${ctx.rawBody}`;
const signatureBuffer = Buffer.from(signatureStr, "base64");
const publicKeyBuffer = Buffer.from(this.publicKey, "base64");
const isValid = crypto.verify(
null, // Ed25519 doesn't use a digest
Buffer.from(signedPayload),
{
key: publicKeyBuffer,
format: "der",
type: "spki",
},
signatureBuffer,
);
if (!isValid) {
return { ok: false, reason: "Invalid signature" };
}
// Check timestamp is within 5 minutes
const eventTime = parseInt(timestampStr, 10) * 1000;
const now = Date.now();
if (Math.abs(now - eventTime) > 5 * 60 * 1000) {
return { ok: false, reason: "Timestamp too old" };
}
return { ok: true };
} catch (err) {
return {
ok: false,
reason: `Verification error: ${err instanceof Error ? err.message : String(err)}`,
};
}
return { ok: result.ok, reason: result.reason };
}
/**