fix(telnyx): validate webhook client state base64 · openclaw/openclaw@dbabfc5
vincentkoc
·
2026-05-14
·
via Recent Commits to openclaw:main
File tree
extensions/voice-call/src/providers
| Original file line number | Diff line number | Diff line change |
|---|
@@ -43,6 +43,7 @@ Docs: https://docs.openclaw.ai
|
43 | 43 | - Microsoft Teams: reject malformed inline HTML image base64 padding instead of decoding corrupted `data:` image attachments. |
44 | 44 | - Voice-call realtime: ignore malformed provider media-frame base64 before forwarding audio into bridge and transcription paths. |
45 | 45 | - QQBot: reject malformed stored cron payload base64 before JSON decoding structured reminder data. |
| 46 | +- Telnyx voice-call: use the raw `client_state` fallback when webhook state is malformed base64 instead of using silently corrupted decoded text. |
46 | 47 | - 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. |
47 | 48 | - 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. |
48 | 49 | - 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. |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -249,6 +249,31 @@ describe("TelnyxProvider.parseWebhookEvent", () => {
|
249 | 249 | expect(event?.to).toBe("+15550000000"); |
250 | 250 | }); |
251 | 251 | |
| 252 | +it("uses raw client_state fallback when client_state is malformed base64", () => { |
| 253 | +const provider = new TelnyxProvider({ |
| 254 | +apiKey: "KEY123", |
| 255 | +connectionId: "CONN456", |
| 256 | +publicKey: undefined, |
| 257 | +}); |
| 258 | +const result = provider.parseWebhookEvent( |
| 259 | +createCtx({ |
| 260 | +rawBody: JSON.stringify({ |
| 261 | +data: { |
| 262 | +id: "evt-client-state", |
| 263 | +event_type: "call.initiated", |
| 264 | +payload: { |
| 265 | +call_control_id: "call-fallback", |
| 266 | +client_state: "call-1@@@", |
| 267 | +}, |
| 268 | +}, |
| 269 | +}), |
| 270 | +}), |
| 271 | +); |
| 272 | + |
| 273 | +expect(result.events).toHaveLength(1); |
| 274 | +expect(result.events[0]?.callId).toBe("call-1@@@"); |
| 275 | +}); |
| 276 | + |
252 | 277 | it("reads transcription text from Telnyx transcription_data payloads", () => { |
253 | 278 | const provider = new TelnyxProvider({ |
254 | 279 | apiKey: "KEY123", |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -47,6 +47,18 @@ function normalizeTelnyxDirection(
|
47 | 47 | } |
48 | 48 | } |
49 | 49 | |
| 50 | +function normalizeBase64ForCompare(value: string): string { |
| 51 | +return value.replace(/=+$/u, "").replace(/-/gu, "+").replace(/_/gu, "/"); |
| 52 | +} |
| 53 | + |
| 54 | +function decodeClientStateBase64(value: string): string | null { |
| 55 | +const buffer = Buffer.from(value, "base64"); |
| 56 | +if (normalizeBase64ForCompare(buffer.toString("base64")) !== normalizeBase64ForCompare(value)) { |
| 57 | +return null; |
| 58 | +} |
| 59 | +return buffer.toString("utf8"); |
| 60 | +} |
| 61 | + |
50 | 62 | export class TelnyxProvider implements VoiceCallProvider { |
51 | 63 | readonly name = "telnyx" as const; |
52 | 64 | |
@@ -142,12 +154,7 @@ export class TelnyxProvider implements VoiceCallProvider {
|
142 | 154 | // Decode client_state from Base64 (we encode it in initiateCall) |
143 | 155 | let callId = ""; |
144 | 156 | if (data.payload?.client_state) { |
145 | | -try { |
146 | | -callId = Buffer.from(data.payload.client_state, "base64").toString("utf8"); |
147 | | -} catch { |
148 | | -// Fallback if not valid Base64 |
149 | | -callId = data.payload.client_state; |
150 | | -} |
| 157 | +callId = decodeClientStateBase64(data.payload.client_state) ?? data.payload.client_state; |
151 | 158 | } |
152 | 159 | if (!callId) { |
153 | 160 | callId = data.payload?.call_control_id || ""; |
|
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。