From dbabfc550fecbd7376f6c668fee7a91a7c1c86b5 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Thu, 14 May 2026 18:08:16 +0800 Subject: [PATCH] fix(telnyx): validate webhook client state base64 --- CHANGELOG.md | 1 + .../voice-call/src/providers/telnyx.test.ts | 25 +++++++++++++++++++ extensions/voice-call/src/providers/telnyx.ts | 19 +++++++++----- 3 files changed, 39 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 49a45c1cc78..22127fe5efb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,6 +43,7 @@ Docs: https://docs.openclaw.ai - Microsoft Teams: reject malformed inline HTML image base64 padding instead of decoding corrupted `data:` image attachments. - Voice-call realtime: ignore malformed provider media-frame base64 before forwarding audio into bridge and transcription paths. - QQBot: reject malformed stored cron payload base64 before JSON decoding structured reminder data. +- Telnyx voice-call: use the raw `client_state` fallback when webhook state is malformed base64 instead of using silently corrupted decoded text. - Models config/auth: stop inferring provider env-var markers from broad `^[A-Z_][A-Z0-9_]*$` strings, and resolve config-backed provider `apiKey` values only through structured env SecretRefs (`secrets.providers[id]` / `secrets.defaults`), so unrelated env vars cannot accidentally become provider credentials. Thanks @sallyom. - Media fetch: skip allocating and buffering the response body for bodyless media responses (HEAD probes and 204-style empty bodies), avoiding wasted heap on streams that carry no payload. Thanks @shakkernerd. - CLI/onboarding: forward provider-specific auth flags (e.g. `--openai-api-key`) through the onboarding wizard so they reach provider auth methods via `ctx.opts`, letting `--openai-api-key "$OPENAI_API_KEY"` skip the redundant "use existing env var?" prompt in non-interactive harnesses. (#81669) Thanks @sjf. diff --git a/extensions/voice-call/src/providers/telnyx.test.ts b/extensions/voice-call/src/providers/telnyx.test.ts index b328765adc7..52085f61068 100644 --- a/extensions/voice-call/src/providers/telnyx.test.ts +++ b/extensions/voice-call/src/providers/telnyx.test.ts @@ -249,6 +249,31 @@ describe("TelnyxProvider.parseWebhookEvent", () => { expect(event?.to).toBe("+15550000000"); }); + it("uses raw client_state fallback when client_state is malformed base64", () => { + const provider = new TelnyxProvider({ + apiKey: "KEY123", + connectionId: "CONN456", + publicKey: undefined, + }); + const result = provider.parseWebhookEvent( + createCtx({ + rawBody: JSON.stringify({ + data: { + id: "evt-client-state", + event_type: "call.initiated", + payload: { + call_control_id: "call-fallback", + client_state: "call-1@@@", + }, + }, + }), + }), + ); + + expect(result.events).toHaveLength(1); + expect(result.events[0]?.callId).toBe("call-1@@@"); + }); + it("reads transcription text from Telnyx transcription_data payloads", () => { const provider = new TelnyxProvider({ apiKey: "KEY123", diff --git a/extensions/voice-call/src/providers/telnyx.ts b/extensions/voice-call/src/providers/telnyx.ts index aeda7b75025..6d9db2e2866 100644 --- a/extensions/voice-call/src/providers/telnyx.ts +++ b/extensions/voice-call/src/providers/telnyx.ts @@ -47,6 +47,18 @@ function normalizeTelnyxDirection( } } +function normalizeBase64ForCompare(value: string): string { + return value.replace(/=+$/u, "").replace(/-/gu, "+").replace(/_/gu, "/"); +} + +function decodeClientStateBase64(value: string): string | null { + const buffer = Buffer.from(value, "base64"); + if (normalizeBase64ForCompare(buffer.toString("base64")) !== normalizeBase64ForCompare(value)) { + return null; + } + return buffer.toString("utf8"); +} + export class TelnyxProvider implements VoiceCallProvider { readonly name = "telnyx" as const; @@ -142,12 +154,7 @@ export class TelnyxProvider implements VoiceCallProvider { // Decode client_state from Base64 (we encode it in initiateCall) let callId = ""; if (data.payload?.client_state) { - try { - callId = Buffer.from(data.payload.client_state, "base64").toString("utf8"); - } catch { - // Fallback if not valid Base64 - callId = data.payload.client_state; - } + callId = decodeClientStateBase64(data.payload.client_state) ?? data.payload.client_state; } if (!callId) { callId = data.payload?.call_control_id || "";