// integrations/twilio/client.js // Thin Twilio Voice client for /calltest outbound calls + webhook validation. // Uses axios (already a project dep) instead of the twilio SDK to keep the // install surface small. import axios from 'axios'; import twilio from 'twilio'; import { logger } from '../../utils/logger.js'; const LOG_SCOPE = 'twilio:client'; function requireEnv(name) { const v = process.env[name]; if (!v || !String(v).trim()) { throw new Error(`Missing required env: ${name}`); } return String(v).trim(); } export function isTwilioConfigured() { return !!( process.env.TWILIO_ACCOUNT_SID && process.env.TWILIO_AUTH_TOKEN && process.env.TWILIO_FROM_NUMBER && process.env.TWILIO_WEBHOOK_BASE_URL ); } export function isCallTestEnabled() { const flag = String(process.env.CALLTEST_ENABLED || '').toLowerCase(); return flag === 'true' || flag === '1' || flag === 'yes'; } function twilioAuth() { const accountSid = requireEnv('TWILIO_ACCOUNT_SID'); const authToken = requireEnv('TWILIO_AUTH_TOKEN'); return { accountSid, authToken }; } /** * Twilio request signature validation (delegates to the official SDK, which * handles array params, port variants, and legacy query-string encoding). * @see https://www.twilio.com/docs/usage/security#validating-requests */ export function validateTwilioSignature(signature, url, params) { const { authToken } = twilioAuth(); if (!signature || !url) return false; return twilio.validateRequest(authToken, signature, url, params || {}); } /** * Place an outbound voice call. Twilio fetches `voiceUrl` when the callee answers. */ export async function createOutboundCall({ to, voiceUrl, statusCallback, timeoutSec = 30 }) { const from = requireEnv('TWILIO_FROM_NUMBER'); const { accountSid, authToken } = twilioAuth(); logger(LOG_SCOPE, `Creating outbound call to ${to} from ${from}`, 'debug'); const body = new URLSearchParams({ To: to, From: from, Url: voiceUrl, Method: 'POST', StatusCallback: statusCallback, StatusCallbackMethod: 'POST', Timeout: String(timeoutSec), }); for (const event of ['initiated', 'ringing', 'answered', 'completed']) { body.append('StatusCallbackEvent', event); } const url = `https://api.twilio.com/2010-04-01/Accounts/${accountSid}/Calls.json`; const resp = await axios.post(url, body.toString(), { auth: { username: accountSid, password: authToken }, headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, timeout: 30_000, }); const call = resp.data; logger(LOG_SCOPE, `Call created sid=${call.sid} status=${call.status}`, 'debug'); return call; } export default { isTwilioConfigured, isCallTestEnabled, createOutboundCall, validateTwilioSignature, };