UNPKG

expo-geofencing

Version:

Production-ready geofencing and activity recognition for Expo React Native with offline support, security features, and enterprise-grade reliability

418 lines (417 loc) 15.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.WebhookManager = void 0; class WebhookManager { constructor(secretKey) { this.webhooks = new Map(); this.deliveryQueue = []; this.stats = new Map(); this.rateLimitTracking = new Map(); this.isProcessing = false; this.secretKey = secretKey || this.generateSecretKey(); this.startProcessing(); } // Webhook Configuration Management addWebhook(config) { const id = this.generateId(); const webhook = { ...config, id, isActive: config.isActive ?? true, timeout: config.timeout || 30000, retryConfig: { maxRetries: config.retryConfig?.maxRetries ?? 3, backoffStrategy: config.retryConfig?.backoffStrategy ?? 'exponential', baseDelay: config.retryConfig?.baseDelay ?? 1000, maxDelay: config.retryConfig?.maxDelay ?? 60000 } }; this.webhooks.set(id, webhook); this.initializeStats(id); return id; } removeWebhook(webhookId) { const removed = this.webhooks.delete(webhookId); if (removed) { this.stats.delete(webhookId); this.rateLimitTracking.delete(webhookId); // Remove pending deliveries for this webhook this.deliveryQueue = this.deliveryQueue.filter(d => d.webhookId !== webhookId); } return removed; } updateWebhook(webhookId, updates) { const existing = this.webhooks.get(webhookId); if (!existing) return false; this.webhooks.set(webhookId, { ...existing, ...updates }); return true; } getWebhook(webhookId) { return this.webhooks.get(webhookId); } getAllWebhooks() { return Array.from(this.webhooks.values()); } getActiveWebhooks() { return this.getAllWebhooks().filter(w => w.isActive); } // Event Triggering async triggerGeofenceEvent(event) { const eventType = event.eventType === 'enter' ? 'geofence.enter' : 'geofence.exit'; await this.triggerWebhooks(eventType, event, { regionId: event.regionId }); } async triggerActivityEvent(event) { await this.triggerWebhooks('activity.change', event, { activity: event.activity }); } async triggerHealthAlert(alert) { await this.triggerWebhooks('health.alert', alert, { severity: alert.severity }); } async triggerLocationUpdate(location) { await this.triggerWebhooks('location.update', location); } async triggerSystemStatus(status) { await this.triggerWebhooks('system.status', status); } async triggerWebhooks(eventType, data, filterContext) { const relevantWebhooks = this.getWebhooksForEvent(eventType, filterContext); for (const webhook of relevantWebhooks) { if (!this.checkRateLimit(webhook)) { console.warn(`Rate limit exceeded for webhook ${webhook.id}`); continue; } const payload = { event: eventType, timestamp: Date.now(), webhookId: webhook.id, data, metadata: webhook.metadata, signature: this.generateSignature(data) }; this.queueDelivery(webhook, payload); } } getWebhooksForEvent(eventType, filterContext) { return this.getActiveWebhooks().filter(webhook => { // Check if webhook listens to this event type if (!webhook.events.includes(eventType)) { return false; } // Apply filters if configured if (webhook.filterConfig && filterContext) { return this.applyFilters(webhook.filterConfig, filterContext); } return true; }); } applyFilters(filterConfig, context) { // Region filter if (filterConfig.regions && context.regionId) { if (!filterConfig.regions.includes(context.regionId)) { return false; } } // Activity filter if (filterConfig.activities && context.activity) { if (!filterConfig.activities.includes(context.activity)) { return false; } } // Severity filter if (filterConfig.severities && context.severity) { if (!filterConfig.severities.includes(context.severity)) { return false; } } // Custom conditions (simplified implementation) if (filterConfig.conditions) { for (const [key, value] of Object.entries(filterConfig.conditions)) { if (context[key] !== value) { return false; } } } return true; } // Delivery Queue Management queueDelivery(webhook, payload) { const delivery = { id: this.generateId(), webhookId: webhook.id, payload, attempt: 1, timestamp: Date.now(), status: 'pending' }; this.deliveryQueue.push(delivery); } async processDeliveryQueue() { if (this.isProcessing || this.deliveryQueue.length === 0) { return; } this.isProcessing = true; try { const pendingDeliveries = this.deliveryQueue.filter(d => d.status === 'pending' || (d.status === 'retrying' && (!d.nextRetryAt || Date.now() >= d.nextRetryAt))); // Process up to 10 deliveries concurrently const batch = pendingDeliveries.slice(0, 10); const promises = batch.map(delivery => this.attemptDelivery(delivery)); // Wait for all promises to complete await Promise.all(promises.map(p => p.catch(() => { }))); // Clean up completed deliveries this.deliveryQueue = this.deliveryQueue.filter(d => d.status === 'pending' || d.status === 'retrying'); } finally { this.isProcessing = false; } } async attemptDelivery(delivery) { const webhook = this.webhooks.get(delivery.webhookId); if (!webhook || !webhook.isActive) { delivery.status = 'failed'; delivery.error = 'Webhook not found or inactive'; return; } const startTime = Date.now(); try { const response = await this.makeHttpRequest(webhook, delivery.payload); const responseTime = Date.now() - startTime; delivery.status = 'success'; delivery.responseStatus = response.status; delivery.responseBody = response.body; this.updateStats(webhook.id, true, responseTime); } catch (error) { const responseTime = Date.now() - startTime; delivery.status = 'failed'; delivery.error = error.message; delivery.responseStatus = error.status; this.updateStats(webhook.id, false, responseTime); // Schedule retry if attempts remaining if (delivery.attempt < webhook.retryConfig.maxRetries) { delivery.status = 'retrying'; delivery.attempt++; delivery.nextRetryAt = this.calculateNextRetryTime(webhook.retryConfig, delivery.attempt); } } } async makeHttpRequest(webhook, payload) { const headers = { ...webhook.headers }; // Add authentication headers this.addAuthHeaders(headers, webhook.authConfig); // Add content type headers['Content-Type'] = 'application/json'; // Add signature header if (payload.signature) { headers['X-Webhook-Signature'] = payload.signature; } const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), webhook.timeout); try { const response = await fetch(webhook.url, { method: webhook.method, headers, body: JSON.stringify(payload), signal: controller.signal }); clearTimeout(timeoutId); const body = await response.text(); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${body}`); } return { status: response.status, body }; } catch (error) { clearTimeout(timeoutId); if (error.name === 'AbortError') { throw new Error(`Request timeout after ${webhook.timeout}ms`); } throw error; } } addAuthHeaders(headers, authConfig) { if (!authConfig || authConfig.type === 'none') { return; } switch (authConfig.type) { case 'bearer': headers['Authorization'] = `Bearer ${authConfig.credentials.token}`; break; case 'api_key': headers[authConfig.credentials.header || 'X-API-Key'] = authConfig.credentials.key; break; case 'basic': const credentials = btoa(`${authConfig.credentials.username}:${authConfig.credentials.password}`); headers['Authorization'] = `Basic ${credentials}`; break; case 'custom': Object.assign(headers, authConfig.credentials); break; } } // Rate Limiting checkRateLimit(webhook) { if (!webhook.rateLimit) return true; const now = Date.now(); const tracking = this.rateLimitTracking.get(webhook.id); if (!tracking || now >= tracking.resetTime) { // Reset or initialize rate limit window this.rateLimitTracking.set(webhook.id, { requests: 1, resetTime: now + webhook.rateLimit.windowMs }); return true; } if (tracking.requests >= webhook.rateLimit.maxRequests) { return false; // Rate limit exceeded } tracking.requests++; return true; } // Statistics and Monitoring initializeStats(webhookId) { this.stats.set(webhookId, { webhookId, totalAttempts: 0, successfulDeliveries: 0, failedDeliveries: 0, averageResponseTime: 0, errorRate: 0 }); } updateStats(webhookId, success, responseTime) { const stats = this.stats.get(webhookId); if (!stats) return; stats.totalAttempts++; if (success) { stats.successfulDeliveries++; stats.lastSuccessAt = Date.now(); } else { stats.failedDeliveries++; stats.lastFailureAt = Date.now(); } stats.lastDeliveryAt = Date.now(); stats.averageResponseTime = (stats.averageResponseTime * (stats.totalAttempts - 1) + responseTime) / stats.totalAttempts; stats.errorRate = stats.failedDeliveries / stats.totalAttempts; } getWebhookStats(webhookId) { return this.stats.get(webhookId); } getAllWebhookStats() { return Array.from(this.stats.values()); } // Retry Logic calculateNextRetryTime(retryConfig, attempt) { let delay; if (retryConfig.backoffStrategy === 'linear') { delay = retryConfig.baseDelay * attempt; } else { delay = retryConfig.baseDelay * Math.pow(2, attempt - 1); } delay = Math.min(delay, retryConfig.maxDelay); // Add some jitter to prevent thundering herd delay += Math.random() * 1000; return Date.now() + delay; } // Security generateSignature(data) { const payload = JSON.stringify(data); return this.hmacSha256(payload, this.secretKey); } verifySignature(payload, signature) { const expectedSignature = this.hmacSha256(payload, this.secretKey); return signature === expectedSignature; } hmacSha256(data, key) { // Simple hash implementation for demo - in production use proper crypto return btoa(data + key).slice(0, 32); } // Processing Control startProcessing() { // Process queue every 5 seconds this.processingInterval = setInterval(() => { this.processDeliveryQueue().catch(error => { console.error('Error processing webhook delivery queue:', error); }); }, 5000); } stopProcessing() { if (this.processingInterval) { clearInterval(this.processingInterval); this.processingInterval = undefined; } } // Manual Operations async retryFailedDeliveries(webhookId) { const failedDeliveries = this.deliveryQueue.filter(d => d.status === 'failed' && (!webhookId || d.webhookId === webhookId)); failedDeliveries.forEach(delivery => { delivery.status = 'pending'; delivery.attempt = 1; delivery.nextRetryAt = undefined; }); return failedDeliveries.length; } getPendingDeliveries(webhookId) { return this.deliveryQueue.filter(d => (d.status === 'pending' || d.status === 'retrying') && (!webhookId || d.webhookId === webhookId)); } getDeliveryHistory(webhookId, limit = 100) { // In practice, this would query a persistent storage return this.deliveryQueue .filter(d => d.webhookId === webhookId) .sort((a, b) => b.timestamp - a.timestamp) .slice(0, limit); } // Testing and Validation async testWebhook(webhookId) { const webhook = this.webhooks.get(webhookId); if (!webhook) { throw new Error('Webhook not found'); } const testPayload = { event: 'system.status', timestamp: Date.now(), webhookId, data: { test: true, message: 'Webhook test payload' }, signature: this.generateSignature({ test: true }) }; const startTime = Date.now(); try { await this.makeHttpRequest(webhook, testPayload); return { success: true, responseTime: Date.now() - startTime }; } catch (error) { return { success: false, responseTime: Date.now() - startTime, error: error.message }; } } // Utility Methods generateId() { return Date.now().toString(36) + Math.random().toString(36).substr(2); } generateSecretKey() { // Simple random key generation for demo return Math.random().toString(36).repeat(4).slice(0, 32); } // Cleanup cleanup() { this.stopProcessing(); this.webhooks.clear(); this.deliveryQueue.length = 0; this.stats.clear(); this.rateLimitTracking.clear(); } } exports.WebhookManager = WebhookManager;