collabSupport/routes/twilioCallTest.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

89 lines
2.5 KiB
JavaScript

// routes/twilioCallTest.js
// Twilio voice + status webhooks for /calltest.
import express from 'express';
import { validateTwilioSignature } from '../integrations/twilio/client.js';
import { logger } from '../utils/logger.js';
import {
getWebhookBaseUrl,
resolveCallTestWebhookUrl,
} from '../services/callTest/config.js';
import {
handleStatusCallback,
handleVoiceWebhook,
isCallTestConfigured,
} from '../services/callTest/callTestService.js';
const router = express.Router();
function verifyTwilio(req, res, next) {
if (!isCallTestConfigured()) {
return res.status(503).send('Call test not configured');
}
const signature = req.get('X-Twilio-Signature') || req.get('x-twilio-signature');
const url = resolveCallTestWebhookUrl(req);
const params = req.body || {};
if (!url) {
return next();
}
try {
if (!validateTwilioSignature(signature, url, params)) {
logger(
'calltest:webhook',
`Invalid Twilio signature for ${req.originalUrl} ` +
`(validated as ${url}, base=${getWebhookBaseUrl()}, bodyKeys=${Object.keys(params).length})`,
'warn',
);
return res.status(403).send('Forbidden');
}
} catch (err) {
logger('calltest:webhook', `Signature check error: ${err.message}`, 'error');
return res.status(503).send('Not configured');
}
return next();
}
router.post('/twilio/calltest/voice', verifyTwilio, (req, res) => {
const testId = req.query.testId;
const mode = req.query.mode === 'store' ? 'store' : 'dial';
const step = String(req.query.step || '').trim();
if (!testId || !step) {
return res.status(400).type('text/xml').send(
'<?xml version="1.0" encoding="UTF-8"?><Response><Say>Missing test parameters.</Say><Hangup/></Response>',
);
}
logger('calltest:webhook', `Voice testId=${testId} mode=${mode} step=${step}`, 'info');
const twiml = handleVoiceWebhook(testId, mode, step);
res.type('text/xml').send(twiml);
});
router.post('/twilio/calltest/status', verifyTwilio, async (req, res) => {
const testId = req.query.testId;
if (!testId) {
return res.status(400).send('Missing testId');
}
logger(
'calltest:webhook',
`Status testId=${testId} CallStatus=${req.body?.CallStatus} CallSid=${req.body?.CallSid}`,
'info',
);
try {
await handleStatusCallback(testId, req.body || {});
res.status(200).send('OK');
} catch (err) {
logger('calltest:webhook', `Status handler error: ${err.message}`, 'error');
res.status(500).send('Error');
}
});
export default router;