55 lines
2.3 KiB
JavaScript
55 lines
2.3 KiB
JavaScript
|
|
// Outbound webhook delivery. emit(event, tenantId, payload) fans the event out to every
|
||
|
|
// active per-tenant subscription registered for that event, plus the legacy global
|
||
|
|
// BIZGAZE_WEBHOOK_URL (back-compat). Each delivery is HMAC-signed and retried on failure.
|
||
|
|
//
|
||
|
|
// NOTE (roadmap): retries are in-memory/best-effort. For guaranteed delivery this should
|
||
|
|
// move to a persistent queue when the app scales to multiple instances (see ARCHITECTURE.md).
|
||
|
|
const R = require('./repos');
|
||
|
|
const crypto = require('crypto');
|
||
|
|
|
||
|
|
const EVENTS = ['session.started', 'session.ended'];
|
||
|
|
|
||
|
|
function sign(secret, body) {
|
||
|
|
return crypto.createHmac('sha256', secret || '').update(body).digest('base64url');
|
||
|
|
}
|
||
|
|
|
||
|
|
const RETRY_DELAYS = [2000, 10000, 30000]; // after the first attempt
|
||
|
|
function deliver(url, secret, body, onDone) {
|
||
|
|
let attempt = 0;
|
||
|
|
const go = async () => {
|
||
|
|
attempt++;
|
||
|
|
let ok = false, status = 0, err = null;
|
||
|
|
try {
|
||
|
|
const res = await fetch(url, {
|
||
|
|
method: 'POST',
|
||
|
|
headers: { 'Content-Type': 'application/json', 'X-BizGaze-Signature': sign(secret, body), 'X-BizGaze-Event': (() => { try { return JSON.parse(body).event; } catch { return ''; } })() },
|
||
|
|
body,
|
||
|
|
signal: AbortSignal.timeout(10000),
|
||
|
|
});
|
||
|
|
status = res.status; ok = res.ok;
|
||
|
|
} catch (e) { err = (e && e.message) || 'delivery failed'; }
|
||
|
|
if (ok || attempt > RETRY_DELAYS.length) { if (onDone) onDone({ ok, status, err }); return; }
|
||
|
|
setTimeout(go, RETRY_DELAYS[attempt - 1]);
|
||
|
|
};
|
||
|
|
go();
|
||
|
|
}
|
||
|
|
|
||
|
|
function emit(event, tenantId, payload) {
|
||
|
|
const body = JSON.stringify({ event, ...payload });
|
||
|
|
// Per-tenant subscriptions
|
||
|
|
try {
|
||
|
|
for (const h of R.webhooks.activeForTenant(tenantId)) {
|
||
|
|
const subs = String(h.events || '').split(',').map((s) => s.trim());
|
||
|
|
if (subs.includes('*') || subs.includes(event)) {
|
||
|
|
deliver(h.url, h.secret, body, (r) => { try { R.webhooks.setStatus(h.id, r.ok ? 1 : 0, r.err || ('HTTP ' + r.status)); } catch (_) {} });
|
||
|
|
}
|
||
|
|
}
|
||
|
|
} catch (_) {}
|
||
|
|
// Legacy global webhook (back-compat): session.ended → BIZGAZE_WEBHOOK_URL, signed with SSO_SECRET.
|
||
|
|
if (event === 'session.ended' && process.env.BIZGAZE_WEBHOOK_URL) {
|
||
|
|
deliver(process.env.BIZGAZE_WEBHOOK_URL, process.env.SSO_SECRET || '', body);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
module.exports = { emit, sign, EVENTS };
|