Enables outbound PSTN probes via TwiML webhooks with Webex result cards, status polling, and optional store CDR enrichment.
90 lines
2.7 KiB
JavaScript
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,
|
|
};
|