From a1b4a0066ba3db1daf526301188b09e28786c456 Mon Sep 17 00:00:00 2001 From: scoootscooob Date: Sun, 1 Mar 2026 18:24:31 -0800 Subject: [PATCH] fix(voice-call): accept externally-initiated Twilio outbound-api calls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #30900 — Calls initiated directly via the Twilio REST API (Direction=outbound-api) were rejected as "unknown call" because processEvent only auto-registered calls with direction=inbound. External outbound-api calls now get registered in the CallManager so the media stream is accepted. Inbound policy checks still only apply to true inbound calls. Co-Authored-By: Claude Opus 4.6 --- .../voice-call/src/manager/events.test.ts | 44 +++++++++++++++++++ extensions/voice-call/src/manager/events.ts | 14 +++++- 2 files changed, 56 insertions(+), 2 deletions(-) diff --git a/extensions/voice-call/src/manager/events.test.ts b/extensions/voice-call/src/manager/events.test.ts index ec2a26cd051..d8488b5cf59 100644 --- a/extensions/voice-call/src/manager/events.test.ts +++ b/extensions/voice-call/src/manager/events.test.ts @@ -235,6 +235,50 @@ describe("processEvent (functional)", () => { expect(ctx.activeCalls.size).toBe(0); }); + it("auto-registers externally-initiated outbound-api calls", () => { + const ctx = createContext(); + const event: NormalizedEvent = { + id: "evt-external-1", + type: "call.initiated", + callId: "CA-external-123", + providerCallId: "CA-external-123", + timestamp: Date.now(), + direction: "outbound", + from: "+15550000000", + to: "+15559876543", + }; + + processEvent(ctx, event); + + // Call should be registered in activeCalls and providerCallIdMap + expect(ctx.activeCalls.size).toBe(1); + expect(ctx.providerCallIdMap.get("CA-external-123")).toBeDefined(); + const call = [...ctx.activeCalls.values()][0]; + expect(call?.providerCallId).toBe("CA-external-123"); + expect(call?.from).toBe("+15550000000"); + expect(call?.to).toBe("+15559876543"); + }); + + it("does not reject externally-initiated outbound calls even with disabled inbound policy", () => { + const { ctx, hangupCalls } = createRejectingInboundContext(); + const event: NormalizedEvent = { + id: "evt-external-2", + type: "call.initiated", + callId: "CA-external-456", + providerCallId: "CA-external-456", + timestamp: Date.now(), + direction: "outbound", + from: "+15550000000", + to: "+15559876543", + }; + + processEvent(ctx, event); + + // External outbound calls bypass inbound policy — they should be accepted + expect(ctx.activeCalls.size).toBe(1); + expect(hangupCalls).toHaveLength(0); + }); + it("deduplicates by dedupeKey even when event IDs differ", () => { const now = Date.now(); const ctx = createContext(); diff --git a/extensions/voice-call/src/manager/events.ts b/extensions/voice-call/src/manager/events.ts index 2d39a96bf74..3aee6a1fdff 100644 --- a/extensions/voice-call/src/manager/events.ts +++ b/extensions/voice-call/src/manager/events.ts @@ -104,8 +104,18 @@ export function processEvent(ctx: EventContext, event: NormalizedEvent): void { callIdOrProviderCallId: event.callId, }); - if (!call && event.direction === "inbound" && event.providerCallId) { - if (!shouldAcceptInbound(ctx.config, event.from)) { + // Auto-register untracked calls arriving via webhook. This covers both + // true inbound calls and externally-initiated outbound-api calls (e.g. calls + // placed directly via the Twilio REST API pointing at our webhook URL). + const isUnregisteredWebhookCall = + !call && + event.providerCallId && + (event.direction === "inbound" || event.direction === "outbound"); + + if (isUnregisteredWebhookCall) { + // Apply inbound policy for true inbound calls; external outbound-api calls + // are implicitly trusted because the caller controls the webhook URL. + if (event.direction === "inbound" && !shouldAcceptInbound(ctx.config, event.from)) { const pid = event.providerCallId; if (!ctx.provider) { console.warn(