diff --git a/extensions/voice-call/src/providers/twilio.ts b/extensions/voice-call/src/providers/twilio.ts index 0dee65501..c06046bf1 100644 --- a/extensions/voice-call/src/providers/twilio.ts +++ b/extensions/voice-call/src/providers/twilio.ts @@ -15,10 +15,11 @@ import type { WebhookVerificationResult, } from "../types.js"; import { escapeXml, mapVoiceToPolly } from "../voice-mapping.js"; -import { verifyTwilioWebhook } from "../webhook-security.js"; import type { VoiceCallProvider } from "./base.js"; import type { OpenAITTSProvider } from "./tts-openai.js"; import { chunkAudio } from "./tts-openai.js"; +import { twilioApiRequest } from "./twilio/api.js"; +import { verifyTwilioProviderWebhook } from "./twilio/webhook.js"; /** * Twilio Voice API provider implementation. @@ -79,45 +80,26 @@ export class TwilioProvider implements VoiceCallProvider { } } - /** - * Set the current public webhook URL (called when tunnel starts). - */ setPublicUrl(url: string): void { this.currentPublicUrl = url; } - /** - * Get the current public webhook URL. - */ getPublicUrl(): string | null { return this.currentPublicUrl; } - /** - * Set the OpenAI TTS provider for streaming TTS. - * When set, playTts will use OpenAI audio via media streams. - */ setTTSProvider(provider: OpenAITTSProvider): void { this.ttsProvider = provider; } - /** - * Set the media stream handler for sending audio. - */ setMediaStreamHandler(handler: MediaStreamHandler): void { this.mediaStreamHandler = handler; } - /** - * Register a call's stream SID for audio routing. - */ registerCallStream(callSid: string, streamSid: string): void { this.callStreamMap.set(callSid, streamSid); } - /** - * Unregister a call's stream SID. - */ unregisterCallStream(callSid: string): void { this.callStreamMap.delete(callSid); } @@ -130,25 +112,14 @@ export class TwilioProvider implements VoiceCallProvider { params: Record, options?: { allowNotFound?: boolean }, ): Promise { - const response = await fetch(`${this.baseUrl}${endpoint}`, { - method: "POST", - headers: { - Authorization: `Basic ${Buffer.from(`${this.accountSid}:${this.authToken}`).toString("base64")}`, - "Content-Type": "application/x-www-form-urlencoded", - }, - body: new URLSearchParams(params), + return await twilioApiRequest({ + baseUrl: this.baseUrl, + accountSid: this.accountSid, + authToken: this.authToken, + endpoint, + body: params, + allowNotFound: options?.allowNotFound, }); - - if (!response.ok) { - if (options?.allowNotFound && response.status === 404) { - return undefined as T; - } - const errorText = await response.text(); - throw new Error(`Twilio API error: ${response.status} ${errorText}`); - } - - const text = await response.text(); - return text ? (JSON.parse(text) as T) : (undefined as T); } /** @@ -160,23 +131,12 @@ export class TwilioProvider implements VoiceCallProvider { * @see https://www.twilio.com/docs/usage/webhooks/webhooks-security */ verifyWebhook(ctx: WebhookContext): WebhookVerificationResult { - const result = verifyTwilioWebhook(ctx, this.authToken, { - publicUrl: this.currentPublicUrl || undefined, - allowNgrokFreeTier: this.options.allowNgrokFreeTier ?? true, - skipVerification: this.options.skipVerification, + return verifyTwilioProviderWebhook({ + ctx, + authToken: this.authToken, + currentPublicUrl: this.currentPublicUrl, + options: this.options, }); - - if (!result.ok) { - console.warn(`[twilio] Webhook verification failed: ${result.reason}`); - if (result.verificationUrl) { - console.warn(`[twilio] Verification URL: ${result.verificationUrl}`); - } - } - - return { - ok: result.ok, - reason: result.reason, - }; } /** diff --git a/extensions/voice-call/src/providers/twilio/api.ts b/extensions/voice-call/src/providers/twilio/api.ts new file mode 100644 index 000000000..5cf9cfd28 --- /dev/null +++ b/extensions/voice-call/src/providers/twilio/api.ts @@ -0,0 +1,29 @@ +export async function twilioApiRequest(params: { + baseUrl: string; + accountSid: string; + authToken: string; + endpoint: string; + body: Record; + allowNotFound?: boolean; +}): Promise { + const response = await fetch(`${params.baseUrl}${params.endpoint}`, { + method: "POST", + headers: { + Authorization: `Basic ${Buffer.from(`${params.accountSid}:${params.authToken}`).toString("base64")}`, + "Content-Type": "application/x-www-form-urlencoded", + }, + body: new URLSearchParams(params.body), + }); + + if (!response.ok) { + if (params.allowNotFound && response.status === 404) { + return undefined as T; + } + const errorText = await response.text(); + throw new Error(`Twilio API error: ${response.status} ${errorText}`); + } + + const text = await response.text(); + return text ? (JSON.parse(text) as T) : (undefined as T); +} + diff --git a/extensions/voice-call/src/providers/twilio/webhook.ts b/extensions/voice-call/src/providers/twilio/webhook.ts new file mode 100644 index 000000000..f59342f14 --- /dev/null +++ b/extensions/voice-call/src/providers/twilio/webhook.ts @@ -0,0 +1,30 @@ +import type { WebhookContext, WebhookVerificationResult } from "../../types.js"; +import { verifyTwilioWebhook } from "../../webhook-security.js"; + +import type { TwilioProviderOptions } from "../twilio.js"; + +export function verifyTwilioProviderWebhook(params: { + ctx: WebhookContext; + authToken: string; + currentPublicUrl?: string | null; + options: TwilioProviderOptions; +}): WebhookVerificationResult { + const result = verifyTwilioWebhook(params.ctx, params.authToken, { + publicUrl: params.currentPublicUrl || undefined, + allowNgrokFreeTier: params.options.allowNgrokFreeTier ?? true, + skipVerification: params.options.skipVerification, + }); + + if (!result.ok) { + console.warn(`[twilio] Webhook verification failed: ${result.reason}`); + if (result.verificationUrl) { + console.warn(`[twilio] Verification URL: ${result.verificationUrl}`); + } + } + + return { + ok: result.ok, + reason: result.reason, + }; +} +