fix(voice-call): stabilize plivo v2 replay keys

This commit is contained in:
Vincent Koc
2026-03-23 09:26:47 -07:00
parent 50f6a2f136
commit b0ce53a79c
2 changed files with 51 additions and 1 deletions

View File

@@ -256,6 +256,52 @@ describe("verifyPlivoWebhook", () => {
expectReplayResultPair(first, second);
});
it("treats query-only V2 variants as the same verified request", () => {
const authToken = "test-auth-token";
const nonce = "nonce-replay-v2";
const verificationUrl = "https://example.com/voice/webhook";
const signature = plivoV2Signature({
authToken,
urlNoQuery: verificationUrl,
nonce,
});
const baseHeaders = {
host: "example.com",
"x-forwarded-proto": "https",
"x-plivo-signature-v2": signature,
"x-plivo-signature-v2-nonce": nonce,
};
const rawBody = "CallUUID=uuid&CallStatus=in-progress";
const first = verifyPlivoWebhook(
{
headers: baseHeaders,
rawBody,
url: `${verificationUrl}?flow=answer&callId=abc`,
method: "POST",
query: { flow: "answer", callId: "abc" },
},
authToken,
);
const second = verifyPlivoWebhook(
{
headers: baseHeaders,
rawBody,
url: `${verificationUrl}?flow=getinput&callId=abc`,
method: "POST",
query: { flow: "getinput", callId: "abc" },
},
authToken,
);
expect(first.ok).toBe(true);
expect(first.verifiedRequestKey).toBeDefined();
expect(second.ok).toBe(true);
expect(second.verifiedRequestKey).toBe(first.verifiedRequestKey);
expect(second.isReplay).toBe(true);
});
it("returns a stable request key when verification is skipped", () => {
const ctx = {
headers: {},

View File

@@ -724,6 +724,10 @@ function getBaseUrlNoQuery(url: string): string {
return `${u.protocol}//${u.host}${u.pathname}`;
}
function createPlivoV2ReplayKey(url: string, nonce: string): string {
return `plivo:v2:${sha256Hex(`${getBaseUrlNoQuery(url)}\n${nonce}`)}`;
}
function timingSafeEqualString(a: string, b: string): boolean {
if (a.length !== b.length) {
const dummy = Buffer.from(a);
@@ -967,7 +971,7 @@ export function verifyPlivoWebhook(
reason: "Invalid Plivo V2 signature",
};
}
const replayKey = `plivo:v2:${sha256Hex(`${verificationUrl}\n${nonceV2}`)}`;
const replayKey = createPlivoV2ReplayKey(verificationUrl, nonceV2);
const isReplay = markReplay(plivoReplayCache, replayKey);
return { ok: true, version: "v2", verificationUrl, isReplay, verifiedRequestKey: replayKey };
}