// API client for Mossad backend. // Falls back gracefully to static demo data if backend is unreachable or unauthenticated. const API_BASE = "/api/v1"; let _token = localStorage.getItem("mossad_token"); let _tenantId = localStorage.getItem("mossad_tenant"); function headers() { const h = { "Content-Type": "application/json" }; if (_token) h["Authorization"] = `Bearer ${_token}`; if (_tenantId) h["X-Tenant-ID"] = _tenantId; return h; } async function api(path, options = {}) { const url = `${API_BASE}${path}`; const res = await fetch(url, { ...options, headers: { ...headers(), ...(options.headers || {}) }, }); if (res.status === 401) { clearAuth(); throw new Error("UNAUTHORIZED"); } if (!res.ok) { const text = await res.text().catch(() => ""); throw new Error(`HTTP ${res.status}: ${text}`); } return res.status === 204 ? null : res.json(); } function setAuth(token, tenantId) { _token = token; _tenantId = tenantId; localStorage.setItem("mossad_token", token); localStorage.setItem("mossad_tenant", tenantId); } function clearAuth() { _token = null; _tenantId = null; localStorage.removeItem("mossad_token"); localStorage.removeItem("mossad_tenant"); } function isAuthenticated() { return !!(_token && _tenantId); } async function login(email, password) { const data = await api("/auth/login", { method: "POST", body: JSON.stringify({ email, password }), }); // Temporarily set the token so /auth/me can authenticate and return tenant_id. _token = data.access_token; const me = await api("/auth/me"); setAuth(data.access_token, me.tenant_id); return { ...data, tenant_id: me.tenant_id }; } async function fetchDashboard() { return api("/analytics/dashboard"); } // Pages through the full alert set — the backend paginates via limit/offset and has no // server-side cap, so a single fixed-limit request silently drops alerts once the tenant // has more open alerts than that limit (seen live: 15,937 open alerts vs. a hardcoded 10,000). async function fetchAlerts(params = "") { const pageSize = 5000; const safetyCap = 100000; // stop paging if something is very wrong, rather than looping forever let offset = 0; let all = []; while (true) { const page = await api(`/alerts?limit=${pageSize}&offset=${offset}${params ? "&" + params : ""}`); all = all.concat(page); if (page.length < pageSize || all.length >= safetyCap) break; offset += pageSize; } return all; } async function fetchCases(params = "") { return api(`/cases?limit=50${params ? "&" + params : ""}`); } async function fetchRules() { return api("/rules"); } async function fetchRiskProfiles() { return api("/analytics/risk-profiles?limit=76"); } async function fetchCustomers(limit = 500, q = "") { const params = new URLSearchParams({ limit: String(limit) }); if (q) params.set("q", q); return api(`/customers?${params.toString()}`); } async function fetchCustomer(customerId) { return api(`/customers/${customerId}`); } async function fetchCustomerTransactions(customerId, limit = 100) { return api(`/customers/${customerId}/transactions?limit=${limit}`); } async function fetchCustomerAlerts(customerId, limit = 100) { return api(`/customers/${customerId}/alerts?limit=${limit}`); } async function fetchCustomerCases(customerId, limit = 100) { return api(`/customers/${customerId}/cases?limit=${limit}`); } async function fetchTenants() { return api("/tenants"); } async function fetchTransactions(limit = 1000) { return api(`/transactions?limit=${limit}`); } async function updateAlert(id, body) { return api(`/alerts/${id}`, { method: "PUT", body: JSON.stringify(body) }); } async function convertAlertToCase(id) { return api(`/alerts/${id}/convert-to-case`, { method: "POST" }); } async function addAlertToCase(caseId, alertId) { return api(`/cases/${caseId}/alerts/${alertId}`, { method: "POST" }); } async function fetchCaseAlerts(caseId) { return api(`/cases/${caseId}/alerts`); } async function fetchAlertNetwork(alertId) { return api(`/alerts/${alertId}/network`); } async function fetchAlertTransactions(alertId) { return api(`/alerts/${alertId}/transactions`); } async function addCaseComment(caseId, content) { return api(`/cases/${caseId}/comments`, { method: "POST", body: JSON.stringify({ content, is_internal: true }) }); } async function addAlertNote(id, content) { return api(`/alerts/${id}/notes`, { method: "POST", body: JSON.stringify({ content }) }); } async function fetchSARs(params = "") { return api(`/sars?limit=1000${params ? "&" + params : ""}`); } async function createSAR(body) { return api("/sars", { method: "POST", body: JSON.stringify(body) }); } async function fetchSAR(id) { return api(`/sars/${id}`); } async function updateSAR(id, body) { return api(`/sars/${id}`, { method: "PUT", body: JSON.stringify(body) }); } async function fileSAR(id) { return api(`/sars/${id}/file`, { method: "POST" }); } async function addSARNote(id, content) { return api(`/sars/${id}/notes`, { method: "POST", body: JSON.stringify({ content }) }); } async function createRule(body) { return api("/rules", { method: "POST", body: JSON.stringify(body) }); } async function updateRule(id, body) { return api(`/rules/${id}`, { method: "PUT", body: JSON.stringify(body) }); } async function activateRule(id) { return api(`/rules/${id}/activate`, { method: "POST" }); } async function deactivateRule(id) { return api(`/rules/${id}/deactivate`, { method: "POST" }); } async function deleteRule(id) { return api(`/rules/${id}`, { method: "DELETE" }); } async function simulateRules(body) { return api("/rules/simulate", { method: "POST", body: JSON.stringify(body) }); } async function createManualAlerts(body) { return api("/alerts/manual", { method: "POST", body: JSON.stringify(body) }); } // Map backend alert schema to the UI's expected shape. function mapAlert(a) { const typology = a.matched_patterns?.[0]?.rule_type || "Unknown"; const rule = a.matched_patterns?.[0]?.rule_name || "R-000 · Unknown rule"; return { id: a.id, external_id: a.external_id, case: a.case_id ? `CSE-${a.external_id.split("-")[1]}` : "—", risk: Math.round(Number(a.risk_score || 0)), severity: a.severity || "low", typology: typologyToName(typology), rule: rule, subject: a.primary_entity_id || "Unknown", subjectType: a.primary_entity_type === "business" ? "business" : "person", subjectCountry: a.primary_entity_country || "US", counterparty: a.counterparty_name || "—", cpCountry: a.counterparty_country || "—", amount: Number(a.amount || 0), currency: a.currency || "USD", customerId: a.customer_id || a.primary_entity_id || "—", ruleIds: a.rule_ids || [], txnCount: a.txn_count || 1, opened: a.created_at, sla: "—", slaWarn: false, assignee: a.assignee_name || "Unassigned", status: a.status || "open", channel: a.channel || "—", notes: a.notes || [], score: { volume: Math.min(99, Math.round(Number(a.risk_score || 0) * 0.9)), velocity: Math.min(99, Math.round(Number(a.risk_score || 0) * 0.8)), geo: Math.min(99, Math.round(Number(a.risk_score || 0) * 0.5)), peer: Math.min(99, Math.round(Number(a.risk_score || 0) * 0.7)), }, flagged: a.severity === "high" || a.severity === "critical", _raw: a, }; } function typologyToName(ruleType) { const map = { threshold: "Structuring", velocity: "Velocity anomaly", anomaly: "Mule activity", list_match: "PEP/Sanctions", composite: "Round-tripping", }; return map[ruleType] || "Other"; } // Map backend case schema to UI shape. function mapCase(c) { return { id: c.external_id, title: c.title, status: c.status, priority: c.priority, entity: c.primary_entity_id, risk: Math.round(Number(c.risk_score || 0)), alerts: c.alert_count, amount: Number(c.total_amount || 0), customerId: c.customer_id || c.primary_entity_id || "—", assignee: c.assignee_name || "Unassigned", opened: c.created_at, _raw: c, }; } Object.assign(window, { mossadApi: { login, logout: clearAuth, isAuthenticated, fetchDashboard, fetchAlerts, fetchCases, fetchRules, fetchRiskProfiles, fetchCustomers, fetchCustomer, fetchCustomerTransactions, fetchCustomerAlerts, fetchCustomerCases, fetchTenants, fetchTransactions, updateAlert, convertAlertToCase, addAlertToCase, fetchCaseAlerts, fetchAlertNetwork, fetchAlertTransactions, addCaseComment, addAlertNote, fetchSARs, createSAR, fetchSAR, updateSAR, fileSAR, addSARNote, createRule, updateRule, activateRule, deactivateRule, deleteRule, simulateRules, createManualAlerts, mapAlert, mapCase, }, });