const crypto = require('crypto'); const { audit } = require('../db'); const { getConfig } = require('./notifications'); const TIMEOUT_MS = 10000; // The JSON object delivered for each alert. Stable, flat shape for easy consumption. function buildPayload(alert) { return { event: 'alert', id: alert.id, type: alert.type, severity: alert.severity, title: alert.title, message: alert.message || null, due_date: alert.due_date || null, status: alert.status, reference_type: alert.reference_type, reference_id: alert.reference_id, asset_id: alert.asset_id || null, asset_code: alert.asset_code || null, created_at: alert.created_at, sent_at: new Date().toISOString() }; } // POST a single JSON object. When a secret is set, sign the raw body with HMAC-SHA256 // so the receiver can verify authenticity via the X-MixedAssets-Signature header. async function postJson(url, secret, payload) { const body = JSON.stringify(payload); const headers = { 'Content-Type': 'application/json', 'User-Agent': 'MixedAssets-Webhook/1' }; if (secret) { const signature = crypto.createHmac('sha256', secret).update(body).digest('hex'); headers['X-MixedAssets-Signature'] = `sha256=${signature}`; } const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), TIMEOUT_MS); try { const response = await fetch(url, { method: 'POST', headers, body, signal: controller.signal }); return { ok: response.ok, status: response.status }; } finally { clearTimeout(timer); } } // Deliver each alert as its own JSON POST. Best-effort: a failed delivery is counted // but never throws, so one bad endpoint can't break the alert cycle. async function deliverAlerts(alerts) { const c = getConfig(); if (!c.webhookEnabled || !c.webhookUrl || !alerts.length) { return { attempted: false, delivered: 0, failed: 0 }; } let delivered = 0; let failed = 0; for (const alert of alerts) { try { const result = await postJson(c.webhookUrl, c.webhookSecret, buildPayload(alert)); if (result.ok) delivered += 1; else failed += 1; } catch { failed += 1; } } return { attempted: true, delivered, failed }; } async function sendTestWebhook(userId) { const c = getConfig(); if (!c.webhookUrl) throw Object.assign(new Error('A webhook URL is not configured'), { status: 400 }); const payload = { event: 'test', severity: 'info', title: 'MixedAssets webhook test', message: 'If you received this, your webhook destination is configured correctly.', sent_at: new Date().toISOString() }; let result; try { result = await postJson(c.webhookUrl, c.webhookSecret, payload); } catch (error) { throw Object.assign(new Error(`Webhook request failed: ${error.message}`), { status: 400 }); } audit(userId, 'send', 'test_webhook', null, null, { url: c.webhookUrl, status: result.status }); if (!result.ok) throw Object.assign(new Error(`Webhook endpoint returned HTTP ${result.status}`), { status: 400 }); return { ok: true, status: result.status }; } module.exports = { buildPayload, deliverAlerts, sendTestWebhook };