UNPKG

hook-engine

Version:

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

363 lines (362 loc) 13.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.MultiTenantHandler = void 0; const events_1 = require("events"); const webhook_errors_1 = require("../errors/webhook-errors"); class MultiTenantHandler extends events_1.EventEmitter { constructor(eventProcessor, config) { super(); this.tenantConfigs = new Map(); this.tenantMetrics = new Map(); this.rateLimitWindows = new Map(); this.tenantResourceUsage = new Map(); this.eventProcessor = eventProcessor; this.config = { ...config, defaultRateLimit: config.defaultRateLimit || { eventsPerSecond: 100, burstSize: 200, windowSize: 1000 }, defaultIsolation: config.defaultIsolation || { enableResourceIsolation: true, maxMemoryPerTenant: 100, // 100MB maxConcurrentEvents: 50, enableNetworkIsolation: false }, enableTenantMetrics: config.enableTenantMetrics ?? true, enableGlobalRateLimit: config.enableGlobalRateLimit ?? false }; } /** * Register tenant configuration */ registerTenant(tenantConfig) { this.tenantConfigs.set(tenantConfig.tenantId, tenantConfig); this.eventProcessor.addTenantConfig(tenantConfig); // Initialize metrics if (this.config.enableTenantMetrics) { this.tenantMetrics.set(tenantConfig.tenantId, { totalEvents: 0, successfulEvents: 0, failedEvents: 0, lastEventAt: 0, averageProcessingTime: 0, rateLimitHits: 0 }); } // Initialize resource tracking this.tenantResourceUsage.set(tenantConfig.tenantId, { memory: 0, activeEvents: 0 }); this.emit('tenantRegistered', tenantConfig); } /** * Unregister tenant */ unregisterTenant(tenantId) { const removed = this.tenantConfigs.delete(tenantId); if (removed) { this.tenantMetrics.delete(tenantId); this.rateLimitWindows.delete(tenantId); this.tenantResourceUsage.delete(tenantId); this.emit('tenantUnregistered', tenantId); } return removed; } /** * Process webhook event with multi-tenant support */ async processWebhookEvent(rawBody, headers, adapter, tenantId) { const startTime = Date.now(); try { // Extract tenant if not provided if (!tenantId) { const parsedPayload = adapter.parsePayload(rawBody); tenantId = adapter.extractTenant?.(parsedPayload, { headers }) || 'default'; } // Validate tenant if (!this.validateTenant(tenantId)) { throw new webhook_errors_1.WebhookValidationError(`Invalid tenant: ${tenantId}`); } // Check rate limits if (!this.checkRateLimit(tenantId)) { this.updateTenantMetrics(tenantId, { rateLimitHit: true }); throw new webhook_errors_1.WebhookRateLimitError(0, 1000, { tenant: tenantId }); } // Check resource limits if (!this.checkResourceLimits(tenantId)) { throw new webhook_errors_1.WebhookValidationError(`Resource limit exceeded for tenant: ${tenantId}`); } // Parse and normalize event const parsedPayload = adapter.parsePayload(rawBody); const normalizedEvent = adapter.normalize(parsedPayload, { tenant: tenantId, includeRaw: true }); // Validate tenant access to event if (!this.validateTenantEventAccess(tenantId, normalizedEvent)) { throw new webhook_errors_1.WebhookValidationError(`Tenant access denied for event`); } // Track resource usage this.trackResourceUsage(tenantId, 'start'); try { // Process event through tenant-specific pipelines const result = await this.eventProcessor.processEvent(normalizedEvent, adapter); this.updateTenantMetrics(tenantId, { success: result.success, processingTime: result.processingTime }); return normalizedEvent; } finally { this.trackResourceUsage(tenantId, 'end'); } } catch (error) { this.updateTenantMetrics(tenantId || 'unknown', { success: false, processingTime: Date.now() - startTime }); throw error; } } /** * Process batch webhook events */ async processBatchWebhookEvents(rawBody, headers, adapter, tenantId) { if (!adapter.supportsBatch) { throw new webhook_errors_1.WebhookAdapterError('batch processing not supported'); } const startTime = Date.now(); try { // Parse batch payload const batchPayload = adapter.parseBatchPayload(rawBody); // Extract tenant if not provided if (!tenantId && batchPayload.length > 0) { tenantId = adapter.extractTenant?.(batchPayload[0], { headers }) || 'default'; } // Validate tenant if (!this.validateTenant(tenantId)) { throw new webhook_errors_1.WebhookValidationError(`Invalid tenant: ${tenantId}`); } // Check batch rate limits (stricter for batches) const batchRateLimit = this.getBatchRateLimit(tenantId, batchPayload.length); if (!this.checkBatchRateLimit(tenantId, batchPayload.length, batchRateLimit)) { throw new webhook_errors_1.WebhookRateLimitError(0, 1000, { tenant: tenantId, batch: true }); } // Normalize batch const batchEvent = adapter.normalizeBatch(batchPayload, { tenant: tenantId, includeRaw: true }); // Validate tenant access to all events for (const event of batchEvent.events) { if (!this.validateTenantEventAccess(tenantId, event)) { throw new webhook_errors_1.WebhookValidationError(`Tenant access denied for batch event`); } } // Track resource usage this.trackResourceUsage(tenantId, 'start', batchEvent.events.length); try { // Process batch const result = await this.eventProcessor.processBatch(batchEvent, adapter); this.updateTenantMetrics(tenantId, { success: result.successCount > 0, processingTime: result.processingTime, batchSize: batchEvent.events.length }); return batchEvent.events; } finally { this.trackResourceUsage(tenantId, 'end', batchEvent.events.length); } } catch (error) { this.updateTenantMetrics(tenantId || 'unknown', { success: false, processingTime: Date.now() - startTime }); throw error; } } /** * Validate tenant exists and is active */ validateTenant(tenantId) { return this.tenantConfigs.has(tenantId) || tenantId === 'default'; } /** * Check rate limits for tenant */ checkRateLimit(tenantId) { const tenantConfig = this.tenantConfigs.get(tenantId); const rateLimit = tenantConfig?.rateLimits || this.config.defaultRateLimit; const now = Date.now(); const windowKey = tenantId; let window = this.rateLimitWindows.get(windowKey); if (!window || (now - window.windowStart) >= rateLimit.windowSize) { // New window window = { count: 0, windowStart: now, burstCount: 0 }; this.rateLimitWindows.set(windowKey, window); } // Check burst limit if (window.burstCount >= rateLimit.burstSize) { return false; } // Check rate limit const expectedCount = ((now - window.windowStart) / 1000) * rateLimit.eventsPerSecond; if (window.count >= expectedCount) { return false; } // Update counters window.count++; window.burstCount++; return true; } /** * Check batch rate limits */ checkBatchRateLimit(tenantId, batchSize, rateLimit) { // For batches, we check if the entire batch can be processed within limits for (let i = 0; i < batchSize; i++) { if (!this.checkRateLimit(tenantId)) { return false; } } return true; } /** * Get batch-specific rate limit (usually more restrictive) */ getBatchRateLimit(tenantId, batchSize) { const tenantConfig = this.tenantConfigs.get(tenantId); const baseLimit = tenantConfig?.rateLimits || this.config.defaultRateLimit; // Reduce limits for large batches const batchFactor = Math.max(0.1, 1 / Math.sqrt(batchSize)); return { eventsPerSecond: Math.floor(baseLimit.eventsPerSecond * batchFactor), burstSize: Math.floor(baseLimit.burstSize * batchFactor), windowSize: baseLimit.windowSize }; } /** * Check resource limits for tenant */ checkResourceLimits(tenantId) { const usage = this.tenantResourceUsage.get(tenantId); if (!usage) return true; const isolation = this.config.defaultIsolation; // Check memory limit if (isolation.enableResourceIsolation && usage.memory > isolation.maxMemoryPerTenant) { return false; } // Check concurrent events limit if (usage.activeEvents >= isolation.maxConcurrentEvents) { return false; } return true; } /** * Validate tenant access to specific event */ validateTenantEventAccess(tenantId, event) { const tenantConfig = this.tenantConfigs.get(tenantId); if (!tenantConfig) return tenantId === 'default'; // Check allowed sources if (tenantConfig.allowedSources && !tenantConfig.allowedSources.includes(event.source)) { return false; } // Check blocked sources if (tenantConfig.blockedSources && tenantConfig.blockedSources.includes(event.source)) { return false; } return true; } /** * Track resource usage for tenant */ trackResourceUsage(tenantId, action, eventCount = 1) { const usage = this.tenantResourceUsage.get(tenantId); if (!usage) return; if (action === 'start') { usage.activeEvents += eventCount; // Estimate memory usage (simplified) usage.memory += eventCount * 0.1; // 0.1MB per event estimate } else { usage.activeEvents = Math.max(0, usage.activeEvents - eventCount); usage.memory = Math.max(0, usage.memory - eventCount * 0.1); } } /** * Update tenant metrics */ updateTenantMetrics(tenantId, update) { if (!this.config.enableTenantMetrics) return; let metrics = this.tenantMetrics.get(tenantId); if (!metrics) { metrics = { totalEvents: 0, successfulEvents: 0, failedEvents: 0, lastEventAt: 0, averageProcessingTime: 0, rateLimitHits: 0 }; this.tenantMetrics.set(tenantId, metrics); } const eventCount = update.batchSize || 1; metrics.totalEvents += eventCount; metrics.lastEventAt = Date.now(); if (update.success !== undefined) { if (update.success) { metrics.successfulEvents += eventCount; } else { metrics.failedEvents += eventCount; } } if (update.processingTime !== undefined) { const totalTime = metrics.averageProcessingTime * (metrics.totalEvents - eventCount) + update.processingTime; metrics.averageProcessingTime = totalTime / metrics.totalEvents; } if (update.rateLimitHit) { metrics.rateLimitHits++; } } /** * Get tenant metrics */ getTenantMetrics(tenantId) { return this.tenantMetrics.get(tenantId); } /** * Get all tenant metrics */ getAllTenantMetrics() { return new Map(this.tenantMetrics); } /** * Get tenant resource usage */ getTenantResourceUsage(tenantId) { return this.tenantResourceUsage.get(tenantId); } /** * Reset tenant metrics */ resetTenantMetrics(tenantId) { this.tenantMetrics.delete(tenantId); } } exports.MultiTenantHandler = MultiTenantHandler;