collabSupport/integrations/twilio/client.js
Joseph McQueen 7aa8c37d1d Add Twilio /calltest for store and direct-dial voice path testing.
Enables outbound PSTN probes via TwiML webhooks with Webex result cards, status polling, and optional store CDR enrichment.
2026-07-23 17:59:10 -04:00

90 lines
2.7 KiB
JavaScript

// 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,
};