// Runtime configuration for the SPA — server URL + auth token. Every fetch // call goes through fetchWithAuth/apiBase (used by api.ts) so the SPA works // identically whether it's served same-origin (browser prod, Vite dev proxy) // or cross-origin (Wails webview, remote access). See // plans/2026-07-12-wails-desktop-app.md 0.2. import { getToken, isOIDCConfigured } from './oidc' export interface OikosConfig { apiUrl: string // e.g. "https://oikos.hubris.network", or "" for same-origin token?: string // bearer token for auth } declare global { interface Window { __OIKOS_CONFIG__?: OikosConfig } } let cfg: OikosConfig | undefined export function initConfig(override?: OikosConfig) { cfg = override ?? window.__OIKOS_CONFIG__ if (cfg?.token) { localStorage.setItem('oikos_token', cfg.token) if (cfg.apiUrl) localStorage.setItem('oikos_api_url', cfg.apiUrl) } } export function getConfig(): OikosConfig { if (!cfg) { const token = localStorage.getItem('oikos_token') const apiUrl = localStorage.getItem('oikos_api_url') if (token || apiUrl) { cfg = { apiUrl: apiUrl ?? '', token: token ?? undefined } } } return cfg ?? { apiUrl: '' } } export function setConfig(next: OikosConfig) { cfg = next if (next.token) localStorage.setItem('oikos_token', next.token) else localStorage.removeItem('oikos_token') if (next.apiUrl) localStorage.setItem('oikos_api_url', next.apiUrl) else localStorage.removeItem('oikos_api_url') } export function clearConfig() { cfg = { apiUrl: '' } localStorage.removeItem('oikos_token') localStorage.removeItem('oikos_api_url') } export function isConfigured(): boolean { return !!getConfig().token || isOIDCConfigured() } // Relative paths are used in dev (Vite proxy) and when the SPA shares an // origin with the API server (Caddy reverse proxy). Absolute paths are used // when the API server is on a different origin (Wails webview, remote access). export function apiBase(path: string): string { const c = getConfig() if (!c.apiUrl) return path // relative — relies on same-origin or Vite proxy return `${c.apiUrl}${path}` } // Resolves the auth token for a request: OIDC takes precedence, then static. function resolveAuthHeader(): string | null { const oidcToken = getToken() if (oidcToken) return `Bearer ${oidcToken}` const c = getConfig() if (c.token) return `Bearer ${c.token}` return null } // ---- Auth fetch wrapper ---- // Prepends the API base URL (absolute when configured, relative when unset // for the Vite dev proxy / same-origin prod) and adds the Authorization // header. Used by every fetch call in api.ts. export async function fetchWithAuth(path: string, opts?: RequestInit): Promise { const headers: Record = { 'Content-Type': 'application/json', ...(opts?.headers as Record ?? {}) } const authH = resolveAuthHeader() if (authH) { headers['Authorization'] = authH } return fetch(apiBase(path), { ...opts, headers }) } // SSE path builder — EventSource doesn't take headers, so pass the token as // a query parameter (the SSE handler's combinedAuth checks it alongside the // Authorization header, only for this route). export function sseUrl(path: string): string { const url = apiBase(path) const c = getConfig() const oidcToken = getToken() const token = oidcToken ?? c.token if (!token) return url const sep = url.includes('?') ? '&' : '?' return `${url}${sep}token=${encodeURIComponent(token)}` }