Spaces:
Running
Running
File size: 6,131 Bytes
f568a4f | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 | /**
* apiClient β the single typed entry point to the Aureon operator REST surface.
*
* Every dashboard reaches the backend over relative `/api/...` paths, which Vite
* proxies to the operator server in dev (see vite.config.ts) and are same-origin
* in production. This wrapper gives the whole UI ONE honest contract:
*
* - a uniform result: it returns data on success and throws a typed `ApiError`
* on any non-2xx or transport failure, so callers can distinguish "backend
* offline" from "backend said no" and degrade honestly (never fabricate).
* - a short timeout so a dead gateway surfaces as "offline" instead of hanging.
* - no embedded credentials, but an optional injected auth token: when an
* end-user session exists, its bearer is attached to every `/api/*` call so
* the backend can identify the tenant. With no session, no header is sent β
* the single-operator/same-origin path is unchanged.
*
* Prefer this over bare `fetch("/api/...")` in new code.
*/
/**
* An optional async provider of the current auth bearer token (e.g. the Supabase
* session access token). Injected once at app bootstrap via `setAuthTokenProvider`
* so this module stays decoupled from the auth library (and testable). When it
* returns null/throws, no `Authorization` header is attached β backward compatible.
*/
let authTokenProvider: (() => Promise<string | null>) | null = null;
export function setAuthTokenProvider(fn: (() => Promise<string | null>) | null): void {
authTokenProvider = fn;
}
/** Thrown for any failed backend call. `offline` marks transport failures
* (gateway down / timeout / network) as opposed to a real HTTP error status. */
export class ApiError extends Error {
readonly status: number;
readonly offline: boolean;
readonly path: string;
constructor(message: string, opts: { status?: number; offline?: boolean; path: string }) {
super(message);
this.name = "ApiError";
this.status = opts.status ?? 0;
this.offline = opts.offline ?? false;
this.path = opts.path;
}
}
const DEFAULT_TIMEOUT_MS = 8000;
interface RequestOptions {
/** Abort the request after this many ms (default 8000). */
timeoutMs?: number;
/** Extra headers merged over the defaults. */
headers?: Record<string, string>;
signal?: AbortSignal;
}
async function request<T>(
method: "GET" | "POST" | "DELETE",
path: string,
body?: unknown,
opts: RequestOptions = {},
): Promise<T> {
const controller = new AbortController();
const timeout = window.setTimeout(() => controller.abort(), opts.timeoutMs ?? DEFAULT_TIMEOUT_MS);
// If the caller passed their own signal, abort ours when theirs fires.
if (opts.signal) {
if (opts.signal.aborted) controller.abort();
else opts.signal.addEventListener("abort", () => controller.abort(), { once: true });
}
// Attach the end-user session bearer when one exists (tenant identity). A missing
// provider, a null token, or a throw all mean "no auth header" β unchanged behavior.
let authHeader: Record<string, string> = {};
try {
const token = authTokenProvider ? await authTokenProvider() : null;
if (token) authHeader = { Authorization: `Bearer ${token}` };
} catch {
/* token resolution failed β proceed unauthenticated */
}
let res: Response;
try {
res = await fetch(path, {
method,
cache: "no-store",
signal: controller.signal,
headers: {
Accept: "application/json",
...(body !== undefined ? { "Content-Type": "application/json" } : {}),
...authHeader,
...opts.headers,
},
...(body !== undefined ? { body: JSON.stringify(body) } : {}),
});
} catch (err) {
// fetch rejects only on transport failure (network down, CORS, abort) β the
// gateway is unreachable. This is the honest "offline" signal.
const aborted = err instanceof DOMException && err.name === "AbortError";
throw new ApiError(aborted ? `Request to ${path} timed out` : `Cannot reach ${path}`, {
offline: true,
path,
});
} finally {
window.clearTimeout(timeout);
}
if (!res.ok) {
let detail = "";
try {
const j = await res.json();
detail = typeof j?.error === "string" ? j.error : "";
} catch {
/* body was not JSON β keep the status-only message */
}
throw new ApiError(detail || `${method} ${path} -> ${res.status}`, {
status: res.status,
path,
});
}
// 204 / empty body -> return undefined as T
if (res.status === 204) return undefined as T;
return (await res.json()) as T;
}
export const api = {
get: <T>(path: string, opts?: RequestOptions) => request<T>("GET", path, undefined, opts),
post: <T>(path: string, body?: unknown, opts?: RequestOptions) =>
request<T>("POST", path, body, opts),
del: <T>(path: string, opts?: RequestOptions) => request<T>("DELETE", path, undefined, opts),
};
/**
* `/api/pulse` β the operator's one-call vitals (providers + platform status +
* organism + switchboard). The richest single read for a top-of-app health band.
* Shape is intentionally permissive: fields the backend omits stay optional.
*/
export interface OperatorPulse {
ok?: boolean;
status?: "healthy" | "degraded" | "critical" | string;
/** Configured LLM providers β a list; its length is the provider count. */
providers?: unknown[];
switchboard?: { total?: number; enabled?: number; armed?: number } & Record<string, unknown>;
organism?: {
connectome?: { coverage_pct?: number; nodes?: number; woven?: number } & Record<string, unknown>;
} & Record<string, unknown>;
service?: unknown;
[key: string]: unknown;
}
/** Fetch the operator pulse. Throws `ApiError` (with `.offline`) when the
* backend is unreachable, so callers show an honest offline state. */
export function fetchPulse(opts?: RequestOptions): Promise<OperatorPulse> {
return api.get<OperatorPulse>("/api/pulse", opts);
}
|