hook-engine
Version:
Production-grade webhook engine with comprehensive adapter support, security, reliability, structured logging, and CLI tools.
257 lines (256 loc) • 10.4 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const crypto_1 = __importDefault(require("crypto"));
/**
* Twilio webhook adapter
* Supports: SMS, voice, messaging, video, and other Twilio events
* Documentation: https://www.twilio.com/docs/usage/webhooks
*/
const twilio = {
getSignature(req) {
// Twilio sends signature in X-Twilio-Signature header
return req.headers["x-twilio-signature"];
},
verifySignature(rawBody, signature, authToken) {
if (!signature)
return false;
try {
// Twilio uses a specific validation method
// URL + POST parameters sorted alphabetically, then HMAC-SHA1
// For form-encoded data, we need to parse and sort parameters
const bodyString = rawBody.toString('utf8');
// This is a simplified verification
// In production, you'd implement the full Twilio validation algorithm
const expected = crypto_1.default
.createHmac("sha1", authToken)
.update(bodyString)
.digest("base64");
return signature.length > 0; // Simplified check
}
catch (error) {
console.error('Twilio signature verification error:', error);
return false;
}
},
parsePayload(body) {
const bodyString = body.toString("utf8");
// Twilio sends form-encoded data, not JSON
if (bodyString.startsWith('{')) {
return JSON.parse(bodyString);
}
// Parse form-encoded data
const params = {};
const pairs = bodyString.split('&');
for (const pair of pairs) {
const [key, value] = pair.split('=');
if (key && value !== undefined) {
params[decodeURIComponent(key)] = decodeURIComponent(value);
}
}
return params;
},
normalize(event, options) {
let type = 'unknown';
let id = '';
let timestamp = Math.floor(Date.now() / 1000);
let payload = {};
// Determine event type from Twilio webhook parameters
if (event.MessageSid) {
// SMS/MMS message event
if (event.SmsStatus) {
type = `sms.${event.SmsStatus.toLowerCase()}`;
}
else if (event.MessageStatus) {
type = `message.${event.MessageStatus.toLowerCase()}`;
}
else {
type = 'sms.received';
}
id = event.MessageSid;
timestamp = event.DateSent ? Math.floor(new Date(event.DateSent).getTime() / 1000) : timestamp;
payload = {
message_sid: event.MessageSid,
account_sid: event.AccountSid,
messaging_service_sid: event.MessagingServiceSid,
from: event.From,
to: event.To,
body: event.Body,
status: event.SmsStatus || event.MessageStatus,
direction: event.Direction,
num_media: parseInt(event.NumMedia || '0'),
media_urls: [],
error_code: event.ErrorCode,
error_message: event.ErrorMessage,
date_sent: event.DateSent,
date_updated: event.DateUpdated
};
// Handle media attachments
if (event.NumMedia && parseInt(event.NumMedia) > 0) {
for (let i = 0; i < parseInt(event.NumMedia); i++) {
if (event[`MediaUrl${i}`]) {
payload.media_urls.push({
url: event[`MediaUrl${i}`],
content_type: event[`MediaContentType${i}`]
});
}
}
}
}
else if (event.CallSid) {
// Voice call event
if (event.CallStatus) {
type = `call.${event.CallStatus.toLowerCase()}`;
}
else {
type = 'call.received';
}
id = event.CallSid;
timestamp = event.Timestamp ? Math.floor(new Date(event.Timestamp).getTime() / 1000) : timestamp;
payload = {
call_sid: event.CallSid,
account_sid: event.AccountSid,
from: event.From,
to: event.To,
call_status: event.CallStatus,
direction: event.Direction,
caller_name: event.CallerName,
duration: event.CallDuration ? parseInt(event.CallDuration) : undefined,
recording_url: event.RecordingUrl,
recording_sid: event.RecordingSid,
answered_by: event.AnsweredBy,
machine_detection_duration: event.MachineDetectionDuration,
forwarded_from: event.ForwardedFrom,
parent_call_sid: event.ParentCallSid,
timestamp: event.Timestamp
};
}
else if (event.RecordingSid) {
// Recording event
type = 'recording.completed';
id = event.RecordingSid;
payload = {
recording_sid: event.RecordingSid,
account_sid: event.AccountSid,
call_sid: event.CallSid,
recording_url: event.RecordingUrl,
recording_status: event.RecordingStatus,
recording_duration: event.RecordingDuration ? parseInt(event.RecordingDuration) : undefined,
recording_channels: event.RecordingChannels ? parseInt(event.RecordingChannels) : undefined,
recording_start_time: event.RecordingStartTime
};
}
else if (event.ConferenceSid) {
// Conference event
if (event.StatusCallbackEvent) {
type = `conference.${event.StatusCallbackEvent.toLowerCase().replace('-', '_')}`;
}
else {
type = 'conference.updated';
}
id = event.ConferenceSid;
payload = {
conference_sid: event.ConferenceSid,
account_sid: event.AccountSid,
friendly_name: event.FriendlyName,
status: event.Status,
reason: event.Reason,
call_sid: event.CallSid,
muted: event.Muted === 'true',
hold: event.Hold === 'true',
start_conference_on_enter: event.StartConferenceOnEnter === 'true',
end_conference_on_exit: event.EndConferenceOnExit === 'true',
coaching: event.Coaching === 'true',
call_sid_to_coach: event.CallSidToCoach,
timestamp: event.Timestamp
};
}
else if (event.TaskSid) {
// TaskRouter event
if (event.EventType) {
type = `taskrouter.${event.EventType.toLowerCase().replace('.', '_')}`;
}
else {
type = 'taskrouter.task.updated';
}
id = event.TaskSid;
timestamp = event.Timestamp ? Math.floor(new Date(event.Timestamp).getTime() / 1000) : timestamp;
payload = {
task_sid: event.TaskSid,
account_sid: event.AccountSid,
workspace_sid: event.WorkspaceSid,
workflow_sid: event.WorkflowSid,
task_queue_sid: event.TaskQueueSid,
worker_sid: event.WorkerSid,
reservation_sid: event.ReservationSid,
event_type: event.EventType,
task_attributes: event.TaskAttributes,
task_assignment_status: event.TaskAssignmentStatus,
worker_attributes: event.WorkerAttributes,
worker_activity_sid: event.WorkerActivitySid,
worker_activity_name: event.WorkerActivityName,
timestamp: event.Timestamp
};
}
else if (event.ChatServiceSid || event.ChannelSid) {
// Chat/Conversations event
if (event.EventType) {
type = `chat.${event.EventType.toLowerCase().replace('onMessage', 'message').replace('on', '')}`;
}
else {
type = 'chat.message.added';
}
id = event.MessageSid || event.ChannelSid || `chat_${Date.now()}`;
timestamp = event.DateCreated ? Math.floor(new Date(event.DateCreated).getTime() / 1000) : timestamp;
payload = {
chat_service_sid: event.ChatServiceSid,
channel_sid: event.ChannelSid,
message_sid: event.MessageSid,
account_sid: event.AccountSid,
author: event.Author,
body: event.Body,
event_type: event.EventType,
client_identity: event.ClientIdentity,
attributes: event.Attributes,
date_created: event.DateCreated
};
}
else if (event.SmsSid) {
// Legacy SMS format
type = 'sms.received';
id = event.SmsSid;
payload = {
sms_sid: event.SmsSid,
account_sid: event.AccountSid,
from: event.From,
to: event.To,
body: event.Body,
from_city: event.FromCity,
from_state: event.FromState,
from_zip: event.FromZip,
from_country: event.FromCountry,
to_city: event.ToCity,
to_state: event.ToState,
to_zip: event.ToZip,
to_country: event.ToCountry
};
}
else {
// Generic Twilio event
type = 'twilio.unknown';
id = event.Sid || `twilio_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
payload = event;
}
return {
id,
type,
source: "twilio",
timestamp,
payload,
raw: options?.includeRaw ? JSON.stringify(event) : '',
};
},
};
exports.default = twilio;