declare global { interface Window { __APP_CONFIG__?: { API_BASE_URL?: string; THALOS_AUTH_BASE_URL?: string; THALOS_DEFAULT_RETURN_URL?: string; THALOS_DEFAULT_TENANT_ID?: string; }; } } export type ApiErrorBody = { code?: string; message?: string; correlationId?: string; }; export class ApiError extends Error { readonly status: number; readonly code?: string; readonly correlationId?: string; constructor(status: number, message: string, code?: string, correlationId?: string) { super(message); this.name = 'ApiError'; this.status = status; this.code = code; this.correlationId = correlationId; } } const CorrelationHeaderName = 'x-correlation-id'; export function getApiBaseUrl(): string { const runtimeValue = window.__APP_CONFIG__?.API_BASE_URL; if (runtimeValue && runtimeValue.length > 0) { return runtimeValue; } return import.meta.env.VITE_API_BASE_URL ?? 'http://localhost:8080'; } export function getThalosAuthBaseUrl(): string { const runtimeValue = window.__APP_CONFIG__?.THALOS_AUTH_BASE_URL; if (runtimeValue && runtimeValue.length > 0) { return runtimeValue; } return import.meta.env.VITE_THALOS_AUTH_BASE_URL ?? getApiBaseUrl(); } export function getThalosDefaultReturnUrl(): string { const runtimeValue = window.__APP_CONFIG__?.THALOS_DEFAULT_RETURN_URL; if (runtimeValue && runtimeValue.length > 0) { return runtimeValue; } return import.meta.env.VITE_THALOS_DEFAULT_RETURN_URL ?? `${window.location.origin}/config`; } export function getThalosDefaultTenantId(): string { const runtimeValue = window.__APP_CONFIG__?.THALOS_DEFAULT_TENANT_ID; if (runtimeValue && runtimeValue.length > 0) { return runtimeValue; } return import.meta.env.VITE_THALOS_DEFAULT_TENANT_ID ?? 'demo-tenant'; } export async function getJson(path: string, baseUrl = getApiBaseUrl()): Promise { return requestJson(baseUrl, path, { method: 'GET' }); } export async function postJson( path: string, body: unknown, baseUrl = getApiBaseUrl() ): Promise { return requestJson(baseUrl, path, { method: 'POST', body: JSON.stringify(body) }); } export async function postNoContent( path: string, body: unknown, baseUrl = getApiBaseUrl() ): Promise { await request(baseUrl, path, { method: 'POST', body: JSON.stringify(body) }); } async function requestJson(baseUrl: string, path: string, init: RequestInit): Promise { const response = await request(baseUrl, path, init); if (response.status === 204) { return {} as T; } return (await response.json()) as T; } async function request(baseUrl: string, path: string, init: RequestInit): Promise { const correlationId = createCorrelationId(); const headers = new Headers(init.headers ?? {}); headers.set('Accept', 'application/json'); if (init.body) { headers.set('Content-Type', 'application/json'); } headers.set(CorrelationHeaderName, correlationId); const response = await fetch(`${baseUrl}${path}`, { ...init, credentials: 'include', headers }); if (!response.ok) { const apiError = await parseApiError(response); const message = apiError.message ?? `${init.method ?? 'GET'} ${path} failed with status ${response.status}`; throw new ApiError(response.status, message, apiError.code, apiError.correlationId ?? correlationId); } return response; } async function parseApiError(response: Response): Promise { try { return (await response.json()) as ApiErrorBody; } catch { return {}; } } function createCorrelationId(): string { if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') { return crypto.randomUUID(); } return `corr-${Date.now()}-${Math.random().toString(16).slice(2)}`; }