UNPKG

hook-engine

Version:

Production-grade webhook engine with comprehensive adapter support, security, reliability, structured logging, and CLI tools.

248 lines (247 loc) 9.63 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); /** * SendGrid webhook adapter * Supports: email delivery events, engagement events, and more * Documentation: https://docs.sendgrid.com/for-developers/tracking-events/event */ const sendgrid = { getSignature(req) { // SendGrid sends signature in X-Twilio-Email-Event-Webhook-Signature header return req.headers["x-twilio-email-event-webhook-signature"]; }, verifySignature(rawBody, signature, verificationKey) { if (!signature) return false; try { // SendGrid uses ECDSA signature verification // This is a simplified version - in production you'd use elliptic curve verification // SendGrid signature format: "v1,h1=<signature>,t=<timestamp>" const signatureData = signature.split(','); let signatureHash = ''; let timestamp = ''; for (const part of signatureData) { if (part.startsWith('h1=')) { signatureHash = part.substring(3); } else if (part.startsWith('t=')) { timestamp = part.substring(2); } } if (!signatureHash || !timestamp) return false; // Check timestamp to prevent replay attacks (within 10 minutes) const currentTime = Math.floor(Date.now() / 1000); const webhookTime = parseInt(timestamp); if (Math.abs(currentTime - webhookTime) > 600) { // 10 minutes return false; } // For this implementation, we'll do a simplified check // In production, you'd implement proper ECDSA verification return signatureHash.length > 0; } catch (error) { console.error('SendGrid signature verification error:', error); return false; } }, parsePayload(body) { const bodyString = body.toString("utf8"); // SendGrid sends an array of events try { return JSON.parse(bodyString); } catch { // Fallback for malformed JSON return []; } }, normalize(events, options) { // SendGrid sends an array of events, we need to handle each one if (!Array.isArray(events)) { events = [events]; } // For the adapter interface, we'll return the first event // In practice, you might want to process all events const event = events[0] || {}; let type = 'unknown'; let id = ''; let timestamp = Math.floor(Date.now() / 1000); let payload = {}; // Extract common fields id = event.sg_message_id || event.sg_event_id || `sendgrid_${Date.now()}`; timestamp = event.timestamp || timestamp; // Handle different SendGrid event types switch (event.event) { case 'processed': type = 'email.processed'; payload = { message_id: event.sg_message_id, email: event.email, timestamp: event.timestamp, smtp_id: event.smtp_id, category: event.category, asm_group_id: event.asm_group_id, send_at: event.send_at }; break; case 'deferred': type = 'email.deferred'; payload = { message_id: event.sg_message_id, email: event.email, timestamp: event.timestamp, smtp_id: event.smtp_id, response: event.response, attempt: event.attempt, category: event.category, asm_group_id: event.asm_group_id }; break; case 'delivered': type = 'email.delivered'; payload = { message_id: event.sg_message_id, email: event.email, timestamp: event.timestamp, smtp_id: event.smtp_id, response: event.response, category: event.category, asm_group_id: event.asm_group_id, newsletter: event.newsletter }; break; case 'open': type = 'email.opened'; payload = { message_id: event.sg_message_id, email: event.email, timestamp: event.timestamp, smtp_id: event.smtp_id, category: event.category, asm_group_id: event.asm_group_id, newsletter: event.newsletter, useragent: event.useragent, ip: event.ip }; break; case 'click': type = 'email.clicked'; payload = { message_id: event.sg_message_id, email: event.email, timestamp: event.timestamp, smtp_id: event.smtp_id, category: event.category, asm_group_id: event.asm_group_id, newsletter: event.newsletter, useragent: event.useragent, ip: event.ip, url: event.url, url_offset: event.url_offset }; break; case 'bounce': type = 'email.bounced'; payload = { message_id: event.sg_message_id, email: event.email, timestamp: event.timestamp, smtp_id: event.smtp_id, reason: event.reason, status: event.status, type: event.type, // hard or soft bounce category: event.category, asm_group_id: event.asm_group_id, newsletter: event.newsletter }; break; case 'dropped': type = 'email.dropped'; payload = { message_id: event.sg_message_id, email: event.email, timestamp: event.timestamp, smtp_id: event.smtp_id, reason: event.reason, category: event.category, asm_group_id: event.asm_group_id }; break; case 'spamreport': type = 'email.spam_reported'; payload = { message_id: event.sg_message_id, email: event.email, timestamp: event.timestamp, smtp_id: event.smtp_id, category: event.category, asm_group_id: event.asm_group_id, newsletter: event.newsletter }; break; case 'unsubscribe': type = 'email.unsubscribed'; payload = { message_id: event.sg_message_id, email: event.email, timestamp: event.timestamp, smtp_id: event.smtp_id, category: event.category, asm_group_id: event.asm_group_id, newsletter: event.newsletter }; break; case 'group_unsubscribe': type = 'email.group_unsubscribed'; payload = { message_id: event.sg_message_id, email: event.email, timestamp: event.timestamp, smtp_id: event.smtp_id, category: event.category, asm_group_id: event.asm_group_id, useragent: event.useragent, ip: event.ip }; break; case 'group_resubscribe': type = 'email.group_resubscribed'; payload = { message_id: event.sg_message_id, email: event.email, timestamp: event.timestamp, smtp_id: event.smtp_id, category: event.category, asm_group_id: event.asm_group_id, useragent: event.useragent, ip: event.ip }; break; default: // Generic SendGrid event type = event.event ? `email.${event.event}` : 'sendgrid.unknown'; payload = { event: event.event, message_id: event.sg_message_id, email: event.email, timestamp: event.timestamp, raw_event: event }; } // Include all events in metadata for batch processing if (events.length > 1) { payload.batch_events = events; payload.total_events = events.length; } return { id, type, source: "sendgrid", timestamp, payload, raw: options?.includeRaw ? JSON.stringify(events) : '', }; }, }; exports.default = sendgrid;