fix(voice-call): validate realtime media frame base64 · openclaw/openclaw@fe97f1f
vincentkoc
·
2026-05-14
·
via Recent Commits to openclaw:main
File tree
extensions/voice-call/src/webhook
| Original file line number | Diff line number | Diff line change |
|---|
@@ -41,6 +41,7 @@ Docs: https://docs.openclaw.ai
|
41 | 41 | - File transfer: reject malformed inline `file_write` base64 before computing hashes or invoking paired nodes, avoiding Node's lenient base64 decoder. |
42 | 42 | - QA channel: skip malformed inline inbound attachment base64 instead of staging silently corrupted media for agent turns. |
43 | 43 | - Microsoft Teams: reject malformed inline HTML image base64 padding instead of decoding corrupted `data:` image attachments. |
| 44 | +- Voice-call realtime: ignore malformed provider media-frame base64 before forwarding audio into bridge and transcription paths. |
44 | 45 | - 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. |
45 | 46 | - 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. |
46 | 47 | - 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 |
|---|
@@ -39,6 +39,9 @@ describe("TwilioStreamFrameAdapter", () => {
|
39 | 39 | expect( |
40 | 40 | adapter.parseInbound(JSON.stringify({ event: "start", start: { streamSid: "MZ-only" } })), |
41 | 41 | ).toEqual({ kind: "ignored" }); |
| 42 | +expect( |
| 43 | +adapter.parseInbound(JSON.stringify({ event: "media", media: { payload: "AAA@@@" } })), |
| 44 | +).toEqual({ kind: "ignored" }); |
42 | 45 | }); |
43 | 46 | |
44 | 47 | it("serializes outbound frames with the streamSid captured at start", () => { |
@@ -174,6 +177,9 @@ describe("TelnyxStreamFrameAdapter", () => {
|
174 | 177 | const adapter = new TelnyxStreamFrameAdapter(); |
175 | 178 | expect(adapter.parseInbound("not json")).toEqual({ kind: "ignored" }); |
176 | 179 | expect(adapter.parseInbound(JSON.stringify({ event: "media" }))).toEqual({ kind: "ignored" }); |
| 180 | +expect( |
| 181 | +adapter.parseInbound(JSON.stringify({ event: "media", media: { payload: "AAA@@@" } })), |
| 182 | +).toEqual({ kind: "ignored" }); |
177 | 183 | expect(adapter.parseInbound(JSON.stringify({ event: "something-else" }))).toEqual({ |
178 | 184 | kind: "ignored", |
179 | 185 | }); |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -42,6 +42,15 @@ function tryParseJson(rawMessage: string): Record<string, unknown> | null {
|
42 | 42 | return null; |
43 | 43 | } |
44 | 44 | |
| 45 | +function normalizeBase64ForCompare(value: string): string { |
| 46 | +return value.replace(/=+$/u, "").replace(/-/gu, "+").replace(/_/gu, "/"); |
| 47 | +} |
| 48 | + |
| 49 | +function isValidBase64Payload(value: string): boolean { |
| 50 | +const buffer = Buffer.from(value, "base64"); |
| 51 | +return normalizeBase64ForCompare(buffer.toString("base64")) === normalizeBase64ForCompare(value); |
| 52 | +} |
| 53 | + |
45 | 54 | export class TwilioStreamFrameAdapter implements StreamFrameAdapter { |
46 | 55 | readonly providerName = "twilio" as const; |
47 | 56 | private streamSid = ""; |
@@ -71,7 +80,7 @@ export class TwilioStreamFrameAdapter implements StreamFrameAdapter {
|
71 | 80 | ? (msg.media as Record<string, unknown>) |
72 | 81 | : undefined; |
73 | 82 | const payload = typeof mediaData?.payload === "string" ? mediaData.payload : undefined; |
74 | | -if (!payload) { |
| 83 | +if (!payload || !isValidBase64Payload(payload)) { |
75 | 84 | return { kind: "ignored" }; |
76 | 85 | } |
77 | 86 | return { |
@@ -151,7 +160,7 @@ export class TelnyxStreamFrameAdapter implements StreamFrameAdapter {
|
151 | 160 | ? (msg.media as Record<string, unknown>) |
152 | 161 | : undefined; |
153 | 162 | const payload = typeof mediaData?.payload === "string" ? mediaData.payload : undefined; |
154 | | -if (!payload) { |
| 163 | +if (!payload || !isValidBase64Payload(payload)) { |
155 | 164 | return { kind: "ignored" }; |
156 | 165 | } |
157 | 166 | return { |
|
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。