fix(telnyx): validate webhook client state base64

This commit is contained in:
Vincent Koc
2026-05-14 18:08:16 +08:00
parent c822824503
commit dbabfc550f
3 changed files with 39 additions and 6 deletions

View File

@@ -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.

View File

@@ -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",

View File

@@ -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 || "";