mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-18 22:54:46 +00:00
fix(telnyx): validate webhook client state base64
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 || "";
|
||||
|
||||
Reference in New Issue
Block a user