import { CarrierApi, CarrierCall, CarrierSms, CarrierDecision } from './carrier-types'; interface TwilioConfig { apiKey: string; apiSecret: string; accountSid: string; apiBaseUrl?: string; decisionTimeout?: number; } export class TwilioCarrier implements CarrierApi { private readonly config: TwilioConfig; private readonly apiBaseUrl: string; constructor(config: TwilioConfig) { this.config = { ...config, apiBaseUrl: config.apiBaseUrl ?? 'https://api.twilio.com', decisionTimeout: config.decisionTimeout ?? 200, }; this.apiBaseUrl = this.config.apiBaseUrl; } async getCall(callSid: string): Promise { const response = await fetch( `${this.apiBaseUrl}/2010-04-01/Accounts/${this.config.accountSid}/Calls/${callSid}.json`, { method: 'GET', headers: { 'Authorization': `Basic ${Buffer.from(`${this.config.apiKey}:${this.config.apiSecret}`).toString('base64')}`, 'Accept': 'application/json', }, timeout: this.config.decisionTimeout, } ); if (!response.ok) { throw new Error(`Twilio API error: ${response.status}`); } const data = await response.json() as TwilioCallResponse; return this.mapToCarrierCall(data); } async blockCall(callSid: string): Promise { await this.executeCarrierAction('block', callSid); } async flagCall(callSid: string): Promise { await this.executeCarrierAction('flag', callSid); } async allowCall(callSid: string): Promise { await this.executeCarrierAction('allow', callSid); } async getSms(messageSid: string): Promise { const response = await fetch( `${this.apiBaseUrl}/2010-04-01/Accounts/${this.config.accountSid}/Messages/${messageSid}.json`, { method: 'GET', headers: { 'Authorization': `Basic ${Buffer.from(`${this.config.apiKey}:${this.config.apiSecret}`).toString('base64')}`, 'Accept': 'application/json', }, timeout: this.config.decisionTimeout, } ); if (!response.ok) { throw new Error(`Twilio API error: ${response.status}`); } const data = await response.json() as TwilioSmsResponse; return this.mapToCarrierSms(data); } async blockSms(messageSid: string): Promise { await this.executeCarrierAction('block', messageSid, 'sms'); } async flagSms(messageSid: string): Promise { await this.executeCarrierAction('flag', messageSid, 'sms'); } async allowSms(messageSid: string): Promise { await this.executeCarrierAction('allow', messageSid, 'sms'); } async isHealthy(): Promise { try { const response = await fetch( `${this.apiBaseUrl}/2010-04-01/Accounts/${this.config.accountSid}.json`, { method: 'GET', headers: { 'Authorization': `Basic ${Buffer.from(`${this.config.apiKey}:${this.config.apiSecret}`).toString('base64')}`, }, timeout: 5000, } ); return response.ok; } catch { return false; } } private async executeCarrierAction( action: 'block' | 'flag' | 'allow', sid: string, type: 'call' | 'sms' = 'call' ): Promise { const endpoint = type === 'call' ? `${this.apiBaseUrl}/2010-04-01/Accounts/${this.config.accountSid}/Calls/${sid}.json` : `${this.apiBaseUrl}/2010-04-01/Accounts/${this.config.accountSid}/Messages/${sid}.json`; // Twilio uses Status parameter to control call/SMS state const statusUpdate: string = action === 'block' ? 'completed' : action === 'flag' ? 'ringing' : 'in-progress'; await fetch(endpoint, { method: 'POST', headers: { 'Authorization': `Basic ${Buffer.from(`${this.config.apiKey}:${this.config.apiSecret}`).toString('base64')}`, 'Content-Type': 'application/x-www-form-urlencoded', }, body: `Status=${statusUpdate}`, timeout: this.config.decisionTimeout, }); } private mapToCarrierCall(data: TwilioCallResponse): CarrierCall { return { callSid: data.sid, from: data.from, to: data.to, status: this.mapCallStatus(data.status), startTime: new Date(data.startTime), duration: data.duration ? parseInt(data.duration) : undefined, metadata: { twilioPrice: data.price, twilioDirection: data.direction, twilioApiVersion: data.apiVersion, }, }; } private mapToCarrierSms(data: TwilioSmsResponse): CarrierSms { return { messageSid: data.sid, from: data.from, to: data.to, body: data.body, direction: this.mapSmsDirection(data.direction), status: this.mapSmsStatus(data.status), timestamp: new Date(data.dateSent || data.dateCreated), metadata: { twilioNumSegments: data.numSegments, twilioNumMedia: data.numMedia, twilioError: data.errorMessage, }, }; } private mapCallStatus(status: string): CarrierCall['status'] { const statusMap: Record = { 'initiated': 'initiated', 'ringing': 'ringing', 'in-progress': 'in-progress', 'completed': 'completed', 'failed': 'failed', 'busy': 'failed', 'no-answer': 'failed', }; return statusMap[status] ?? 'failed'; } private mapSmsDirection(direction: string): CarrierSms['direction'] { return direction === 'inbound' ? 'inbound' : 'outbound'; } private mapSmsStatus(status: string): CarrierSms['status'] { const statusMap: Record = { 'queued': 'queued', 'sent': 'sent', 'delivered': 'delivered', 'failed': 'failed', 'undelivered': 'failed', }; return statusMap[status] ?? 'failed'; } } interface TwilioCallResponse { sid: string; from: string; to: string; status: string; startTime: string; duration?: string; price?: string; direction?: string; apiVersion?: string; } interface TwilioSmsResponse { sid: string; from: string; to: string; body: string; direction: string; status: string; dateSent?: string; dateCreated: string; numSegments?: string; numMedia?: string; errorMessage?: string; }