/** * api.ts * Browser-Side API Client. * Communicates ONLY with same-origin Next.js Server Route Handlers (/api/...). * The browser never connects directly to Hugging Face Spaces. */ import { AnalyzeTransactionResponse, BackendHealthStatus, MerchantStateQueryResponse, ResponseMetadata, ScenarioReplayResult, TransactionApiInput, } from "./types"; function generateRequestId(): string { return `REQ_${Math.random().toString(36).substring(2, 8).toUpperCase()}`; } function extractErrorMessage(json: any, fallback: string): string { if (!json) return fallback; if (typeof json.error === "string") return json.error; if (json.error?.message) return json.error.message; if (json.error?.details) return json.error.details; if (json.message) return json.message; return fallback; } /** * Health check probe via /api/health */ export async function checkBackendHealth(): Promise { const start = performance.now(); const sentAt = new Date().toISOString(); try { const res = await fetch("/api/health", { cache: "no-store" }); const latency = Math.round(performance.now() - start); if (res.ok) { const json = await res.json(); if (json.success) { return { status: "CONNECTED", endpoint: "/api/health (Vercel Server Proxy)", last_sync_at: new Date().toISOString(), roundtrip_latency_ms: latency, }; } } } catch (err: any) { console.warn("Health check error via /api/health:", err); } return { status: "OFFLINE", endpoint: "/api/health", last_sync_at: sentAt, roundtrip_latency_ms: null, error: "Server-side RazorShield proxy unreachable", }; } /** * Analyzes transaction via POST /api/transaction */ export async function analyzeTransaction( input: TransactionApiInput ): Promise { const reqId = generateRequestId(); const sentAt = new Date().toISOString(); const start = performance.now(); const res = await fetch("/api/transaction", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(input), }); const roundtrip = Math.round(performance.now() - start); const json = await res.json(); if (!res.ok || !json.success || !json.data) { throw new Error(extractErrorMessage(json, `Transaction analysis failed with HTTP ${res.status}`)); } const parsed: AnalyzeTransactionResponse = json.data; parsed.meta = { request_id: reqId, request_sent_at: sentAt, response_received_at: new Date().toISOString(), roundtrip_latency_ms: roundtrip, data_source: "LIVE HUGGING FACE BACKEND", }; return parsed; } /** * Replays scenario via POST /api/scenario */ export async function runScenarioReplay( scenarioName: string, policyMode: string = "BALANCED" ): Promise { const reqId = generateRequestId(); const sentAt = new Date().toISOString(); const start = performance.now(); const res = await fetch("/api/scenario", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ scenario_name: scenarioName, policy_mode: policyMode }), }); const roundtrip = Math.round(performance.now() - start); const json = await res.json(); if (!res.ok || !json.success || !json.data) { throw new Error(extractErrorMessage(json, `Scenario replay failed with HTTP ${res.status}`)); } const parsed: ScenarioReplayResult = json.data; parsed.meta = { request_id: reqId, request_sent_at: sentAt, response_received_at: new Date().toISOString(), roundtrip_latency_ms: roundtrip, data_source: "LIVE HUGGING FACE BACKEND", }; return parsed; } /** * Queries merchant state via POST /api/merchant */ export async function queryMerchantState(merchantId: string): Promise { const reqId = generateRequestId(); const sentAt = new Date().toISOString(); const start = performance.now(); const res = await fetch("/api/merchant", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ merchant_id: merchantId }), }); const roundtrip = Math.round(performance.now() - start); const json = await res.json(); if (!res.ok || !json.success || !json.data) { throw new Error(extractErrorMessage(json, `Merchant query failed with HTTP ${res.status}`)); } const parsed: MerchantStateQueryResponse = json.data; parsed.meta = { request_id: reqId, request_sent_at: sentAt, response_received_at: new Date().toISOString(), roundtrip_latency_ms: roundtrip, data_source: "LIVE HUGGING FACE BACKEND", }; return parsed; } /** * Explains evidence via POST /api/explain */ export async function explainEvidencePayload(evidenceJson: string): Promise { const res = await fetch("/api/explain", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ evidence_json: evidenceJson }), }); const json = await res.json(); if (!res.ok || !json.success) { throw new Error(extractErrorMessage(json, `Explanation failed with HTTP ${res.status}`)); } return json.data; } /** * Resets demo state via POST /api/reset */ export async function resetDemoState(): Promise<{ status: string; message: string; meta?: ResponseMetadata }> { const reqId = generateRequestId(); const sentAt = new Date().toISOString(); const start = performance.now(); const res = await fetch("/api/reset", { method: "POST", }); const roundtrip = Math.round(performance.now() - start); const json = await res.json(); if (!res.ok || !json.success || !json.data) { throw new Error(extractErrorMessage(json, `Reset demo state failed with HTTP ${res.status}`)); } const parsed = json.data; parsed.meta = { request_id: reqId, request_sent_at: sentAt, response_received_at: new Date().toISOString(), roundtrip_latency_ms: roundtrip, data_source: "LIVE HUGGING FACE BACKEND", }; return parsed; }