/** * Smart Twilio SDK wrapper with retry, rate limiting, and error normalization. */ import Twilio from 'twilio'; import type { TwilioCredentials } from './auth.js'; export class TwilioClient { private client: ReturnType; private credentials: TwilioCredentials; constructor(credentials: TwilioCredentials) { this.credentials = credentials; this.client = Twilio(credentials.apiKey, credentials.apiSecret, { accountSid: credentials.accountSid, }); } /** Get the raw Twilio client for direct SDK access */ get raw() { return this.client; } get accountSid() { return this.credentials.accountSid; } /** * Execute a Twilio API call with retry logic and error normalization. */ async execute( operation: (client: ReturnType) => Promise, options: { retries?: number; retryDelay?: number } = {} ): Promise { const { retries = 2, retryDelay = 1000 } = options; let lastError: Error | undefined; for (let attempt = 0; attempt <= retries; attempt++) { try { return await operation(this.client); } catch (err: any) { lastError = err; // Don't retry on auth errors or client errors if (err.status && err.status >= 400 && err.status < 500 && err.status !== 429) { throw this.normalizeError(err); } // Rate limited — wait and retry if (err.status === 429 && attempt < retries) { const delay = retryDelay * Math.pow(2, attempt); await new Promise(resolve => setTimeout(resolve, delay)); continue; } // Server error — retry if (err.status && err.status >= 500 && attempt < retries) { await new Promise(resolve => setTimeout(resolve, retryDelay)); continue; } throw this.normalizeError(err); } } throw this.normalizeError(lastError!); } private normalizeError(err: any): Error { if (err.code && err.message) { return new Error(`Twilio Error ${err.code}: ${err.message}${err.moreInfo ? ` (${err.moreInfo})` : ''}`); } return err instanceof Error ? err : new Error(String(err)); } /** * Validate that credentials work by fetching account info. */ async validateCredentials(): Promise<{ valid: boolean; accountName?: string; error?: string }> { try { const account = await this.client.api.accounts(this.credentials.accountSid).fetch(); return { valid: true, accountName: account.friendlyName }; } catch (err: any) { return { valid: false, error: this.normalizeError(err).message }; } } }