UNPKG

hook-engine

Version:

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

198 lines (197 loc) 7.28 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.HookEngine = void 0; const config_1 = require("../config"); const error_handler_1 = require("../errors/error-handler"); const retry_1 = require("./retry"); const storage_1 = require("../storage"); const adapters_1 = require("../adapters"); const body_1 = require("../utils/body"); const webhook_errors_1 = require("../errors/webhook-errors"); const timing_1 = require("../utils/timing"); /** * Main hook engine class */ class HookEngine { constructor(userConfig) { // Load and validate configuration this.config = (0, config_1.loadConfig)(userConfig); // Initialize error handler this.errorHandler = new error_handler_1.ErrorHandler({ strategy: 'retry', maxRetries: this.config.retry.maxAttempts, retryDelay: this.config.retry.initialDelayMs, notifyOnError: this.config.environment === 'production' }); // Initialize retry engine this.retryEngine = new retry_1.RetryEngine(this.config.retry); // Initialize storage this.storage = (0, storage_1.createStorageAdapter)(this.config.storage); } /** * Process incoming webhook request */ async processWebhook(req) { const timer = new timing_1.Timer(); try { // Parse webhook event const event = await this.parseWebhookEvent(req); // Check for duplicates if (await this.storage.isDuplicate(event.id)) { return { success: true, event, message: `Duplicate event ${event.id} skipped`, duration: timer.elapsed() }; } // Store event await this.storage.storeEvent(event.id, event); await this.storage.markSeen(event.id); return { success: true, event, message: `Event ${event.id} processed successfully`, duration: timer.elapsed() }; } catch (error) { const duration = timer.elapsed(); // Handle error through error handler await this.errorHandler.handle(error, { timestamp: new Date(), metadata: { duration } }); return { success: false, error: error.message, message: `Failed to process webhook: ${error.message}`, duration }; } } /** * Process webhook with custom business logic and retry */ async processWebhookWithRetry(req, businessLogic) { const timer = new timing_1.Timer(); try { // Parse webhook event const event = await this.parseWebhookEvent(req); // Check for duplicates if (await this.storage.isDuplicate(event.id)) { throw new webhook_errors_1.WebhookDuplicateError(event.id); } // Store event await this.storage.storeEvent(event.id, event); await this.storage.markSeen(event.id); // Execute business logic with retry const result = await this.retryEngine.execute(() => businessLogic(event), event); if (!result.success) { return { success: false, event, error: result.error?.message, message: `Business logic failed after ${result.attempts} attempts`, duration: timer.elapsed(), attempts: result.attempts }; } return { success: true, event, message: `Event ${event.id} processed successfully`, duration: timer.elapsed(), attempts: result.attempts }; } catch (error) { const duration = timer.elapsed(); // Handle error through error handler await this.errorHandler.handle(error, { timestamp: new Date(), metadata: { duration } }); return { success: false, error: error.message, message: `Failed to process webhook: ${error.message}`, duration }; } } /** * Get engine statistics */ getStats() { return { retry: this.retryEngine.getStats(), errors: this.errorHandler.getErrorStats(), storage: this.storage instanceof Object && 'getStats' in this.storage ? this.storage.getStats() : null }; } /** * Shutdown the engine gracefully */ async shutdown() { await this.storage.close(); } async parseWebhookEvent(req) { // Determine source from request or use first available adapter const source = this.determineSource(req); const adapterConfig = this.config.adapters.find(a => a.source === source && a.enabled); if (!adapterConfig) { throw new webhook_errors_1.WebhookAdapterError(source); } // Create legacy config for backward compatibility const legacyConfig = { source: adapterConfig.source, secret: adapterConfig.secret }; // Get raw body const rawBody = await (0, body_1.normalizeRequestBody)(req); const adapter = (0, adapters_1.getAdapter)(legacyConfig.source); if (!adapter) { throw new webhook_errors_1.WebhookAdapterError(legacyConfig.source); } // Verify signature const signature = adapter.getSignature(req); if (!signature) { throw new webhook_errors_1.WebhookSignatureError(legacyConfig.source, { reason: 'Missing signature header' }); } const isValid = adapter.verifySignature(rawBody, signature, legacyConfig.secret); if (!isValid) { throw new webhook_errors_1.WebhookSignatureError(legacyConfig.source, { reason: 'Invalid signature' }); } // Parse and normalize const parsed = adapter.parsePayload(rawBody); const normalized = adapter.normalize(parsed); return { id: normalized.id, type: normalized.type, timestamp: normalized.timestamp, source: legacyConfig.source, payload: normalized.payload, raw: parsed, }; } determineSource(req) { // Try to determine source from headers const userAgent = req.headers['user-agent'] || ''; const contentType = req.headers['content-type'] || ''; // Check for Stripe if (req.headers['stripe-signature']) { return 'stripe'; } // Check for GitHub if (req.headers['x-github-event']) { return 'github'; } // Default to first enabled adapter const firstAdapter = this.config.adapters.find(a => a.enabled); return firstAdapter?.source || 'unknown'; } } exports.HookEngine = HookEngine;