const crypto = require('crypto'); const db = require('../database'); /** * Trigger webhooks for a specific event type * @param {string} eventType - The event type (e.g., 'game.added') * @param {object} data - The payload data to send */ async function triggerWebhook(eventType, data) { try { // Get all enabled webhooks that are subscribed to this event const webhooks = db.prepare(` SELECT * FROM webhooks WHERE enabled = 1 `).all(); if (webhooks.length === 0) { return; // No webhooks configured } // Filter webhooks that are subscribed to this event const subscribedWebhooks = webhooks.filter(webhook => { try { const events = JSON.parse(webhook.events); return events.includes(eventType); } catch (err) { console.error(`Invalid events JSON for webhook ${webhook.id}:`, err); return false; } }); if (subscribedWebhooks.length === 0) { return; // No webhooks subscribed to this event } // Build the payload const payload = { event: eventType, timestamp: new Date().toISOString(), data: data }; // Send to each webhook asynchronously (non-blocking) subscribedWebhooks.forEach(webhook => { sendWebhook(webhook, payload, eventType).catch(err => { console.error(`Error sending webhook ${webhook.id}:`, err); }); }); } catch (err) { console.error('Error triggering webhooks:', err); } } /** * Send a webhook to a specific URL * @param {object} webhook - The webhook configuration * @param {object} payload - The payload to send * @param {string} eventType - The event type */ async function sendWebhook(webhook, payload, eventType) { const payloadString = JSON.stringify(payload); // Generate HMAC signature const signature = 'sha256=' + crypto .createHmac('sha256', webhook.secret) .update(payloadString) .digest('hex'); const startTime = Date.now(); let responseStatus = null; let errorMessage = null; try { // Send the webhook const response = await fetch(webhook.url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Webhook-Signature': signature, 'X-Webhook-Event': eventType, 'User-Agent': 'Jackbox-Game-Picker-Webhook/1.0' }, body: payloadString, // Set a timeout of 5 seconds signal: AbortSignal.timeout(5000) }); responseStatus = response.status; if (!response.ok) { errorMessage = `HTTP ${response.status}: ${response.statusText}`; } } catch (err) { errorMessage = err.message; responseStatus = 0; // Indicates connection/network error } // Log the webhook call try { db.prepare(` INSERT INTO webhook_logs (webhook_id, event_type, payload, response_status, error_message) VALUES (?, ?, ?, ?, ?) `).run(webhook.id, eventType, payloadString, responseStatus, errorMessage); } catch (logErr) { console.error('Error logging webhook call:', logErr); } const duration = Date.now() - startTime; if (errorMessage) { console.error(`Webhook ${webhook.id} (${webhook.name}) failed: ${errorMessage} (${duration}ms)`); } else { console.log(`Webhook ${webhook.id} (${webhook.name}) sent successfully: ${responseStatus} (${duration}ms)`); } } /** * Verify a webhook signature * @param {string} signature - The signature from the X-Webhook-Signature header * @param {string} payload - The raw request body as a string * @param {string} secret - The webhook secret * @returns {boolean} - True if signature is valid */ function verifyWebhookSignature(signature, payload, secret) { if (!signature || !signature.startsWith('sha256=')) { return false; } const expectedSignature = 'sha256=' + crypto .createHmac('sha256', secret) .update(payload) .digest('hex'); // Use timing-safe comparison to prevent timing attacks try { return crypto.timingSafeEqual( Buffer.from(signature), Buffer.from(expectedSignature) ); } catch (err) { return false; } } module.exports = { triggerWebhook, verifyWebhookSignature };