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);
}