UNPKG

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
"use strict"; 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;