/** * Server-side client for the real IIOS service (see D:\iios\docs\IIOS_API_AND_SDK_GUIDE.md). * Runs only in BFF route handlers — never shipped to the browser. Auth uses a dev token * minted from /v1/dev/token (requires the IIOS service running with IIOS_DEV_TOKENS=1); * in production swap mintToken() for a real Session-Broker exchange. */ const IIOS_URL = process.env.IIOS_URL ?? "http://localhost:3200"; const APP_ID = process.env.IIOS_APP_ID ?? "portal-demo"; const USER_ID = process.env.IIOS_USER_ID ?? "james"; const ORG_ID = process.env.IIOS_ORG_ID ?? "org_A"; let cachedToken: { token: string; exp: number } | null = null; async function mintToken(): Promise { // reuse for ~100 min (IIOS dev tokens live 2h) if (cachedToken && cachedToken.exp > Date.now()) return cachedToken.token; const res = await fetch(`${IIOS_URL}/v1/dev/token`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ appId: APP_ID, userId: USER_ID, orgId: ORG_ID }), cache: "no-store", }); if (!res.ok) throw new Error(`IIOS token mint failed: ${res.status} ${await res.text().catch(() => "")}`); const { token } = (await res.json()) as { token: string }; cachedToken = { token, exp: Date.now() + 100 * 60_000 }; return token; } async function req(path: string, init?: RequestInit & { idempotencyKey?: string }): Promise { const token = await mintToken(); const headers: Record = { "content-type": "application/json", authorization: `Bearer ${token}`, ...(init?.headers as Record | undefined), }; if (init?.idempotencyKey) headers["idempotency-key"] = init.idempotencyKey; const res = await fetch(`${IIOS_URL}${path}`, { ...init, headers, cache: "no-store" }); if (!res.ok) throw new Error(`IIOS ${res.status} ${path}: ${await res.text().catch(() => "")}`); if (res.status === 204) return undefined as T; return (await res.json()) as T; } export const iios = { info: { url: IIOS_URL, appId: APP_ID, userId: USER_ID, orgId: ORG_ID }, health: () => req<{ status: string; db: boolean }>("/health"), getThreadMessages: (threadId: string) => req(`/v1/threads/${threadId}/messages`), sendMessage: (threadId: string, content: string, idem?: string) => req(`/v1/threads/${threadId}/messages`, { method: "POST", body: JSON.stringify({ content }), idempotencyKey: idem }), listInbox: (state?: string) => req(`/v1/inbox/items${state ? `?state=${state}` : ""}`), patchInbox: (id: string, state: string, reason?: string) => req(`/v1/inbox/items/${id}`, { method: "PATCH", body: JSON.stringify({ state, reason }) }), listTickets: (scope?: string) => req(`/v1/support/tickets${scope ? `?scope=${scope}` : ""}`), patchTicket: (id: string, state: string, reason?: string) => req(`/v1/support/tickets/${id}`, { method: "PATCH", body: JSON.stringify({ state, reason }) }), createTicket: (subject: string, priority?: string, threadId?: string) => req(`/v1/support/tickets`, { method: "POST", body: JSON.stringify({ subject, priority, threadId }) }), escalate: (threadId: string, subject?: string) => req(`/v1/support/escalate`, { method: "POST", body: JSON.stringify({ threadId, subject }) }), }; export async function iiosReachable(): Promise { try { const h = await iios.health(); return !!h?.status; } catch { return false; } }