baa-conductor


baa-conductor / apps / status-api / src
im_wower  ·  2026-03-22

host.ts

  1import { createStatusApiRuntime, describeStatusApiRuntimeSurface, type StatusApiRuntime, type StatusApiRuntimeOptions } from "./runtime.js";
  2import type { StatusApiEnvironment, StatusApiResponse } from "./contracts.js";
  3
  4const DEFAULT_STATUS_API_HOST = "127.0.0.1";
  5const DEFAULT_STATUS_API_PORT = 4318;
  6const NODE_HTTP_MODULE_SPECIFIER = "node:http";
  7
  8const ERROR_HEADERS = {
  9  "content-type": "application/json; charset=utf-8",
 10  "cache-control": "no-store"
 11} as const;
 12
 13export interface StatusApiListenOptions {
 14  host: string;
 15  port: number;
 16}
 17
 18export interface StatusApiServerOptions extends StatusApiRuntimeOptions {
 19  host?: string;
 20  port?: number;
 21}
 22
 23export interface StatusApiServerAddress {
 24  address: string;
 25  family: string;
 26  port: number;
 27}
 28
 29export interface StatusApiServer {
 30  readonly runtime: StatusApiRuntime;
 31  close(): Promise<void>;
 32  describeSurface(): string[];
 33  getAddress(): StatusApiServerAddress | string | null;
 34  getBaseUrl(): string | null;
 35}
 36
 37export interface StatusApiSmokeCheck {
 38  path: string;
 39  status: number;
 40  detail: string;
 41}
 42
 43export interface StatusApiSmokeCheckResult {
 44  baseUrl: string;
 45  checks: StatusApiSmokeCheck[];
 46}
 47
 48interface NodeHttpAddressInfo {
 49  address: string;
 50  family: string;
 51  port: number;
 52}
 53
 54interface NodeIncomingMessage {
 55  method?: string;
 56  url?: string;
 57}
 58
 59interface NodeServerResponse {
 60  statusCode: number;
 61  setHeader(name: string, value: string | readonly string[]): void;
 62  end(chunk?: string | Uint8Array): void;
 63}
 64
 65interface NodeHttpServer {
 66  address(): NodeHttpAddressInfo | string | null;
 67  close(callback?: (error?: Error) => void): void;
 68  listen(port: number, host: string, listeningListener?: () => void): void;
 69  once(event: string, listener: (...args: unknown[]) => void): void;
 70}
 71
 72interface NodeHttpModule {
 73  createServer(
 74    requestListener: (request: NodeIncomingMessage, response: NodeServerResponse) => void | Promise<void>
 75  ): NodeHttpServer;
 76}
 77
 78export function resolveStatusApiListenOptions(
 79  options: StatusApiServerOptions = {},
 80  env: StatusApiEnvironment = process?.env ?? {}
 81): StatusApiListenOptions {
 82  return {
 83    host: resolveStatusApiHost(options.host, env),
 84    port: resolveStatusApiPort(options.port, env)
 85  };
 86}
 87
 88export function createStatusApiNodeRequestListener(runtime: StatusApiRuntime) {
 89  return (request: NodeIncomingMessage, response: NodeServerResponse): void => {
 90    void handleStatusApiNodeRequest(runtime, request, response);
 91  };
 92}
 93
 94export async function startStatusApiServer(options: StatusApiServerOptions = {}): Promise<StatusApiServer> {
 95  const { createServer } = await importNodeHttp();
 96  const env = options.env ?? process?.env ?? {};
 97  const listenOptions = resolveStatusApiListenOptions(options, env);
 98  const runtime = createStatusApiRuntime({
 99    env,
100    snapshotLoader: options.snapshotLoader
101  });
102  const server = createServer(createStatusApiNodeRequestListener(runtime));
103
104  await listenOnServer(server, listenOptions);
105
106  return {
107    runtime,
108    close: () => closeServer(server),
109    describeSurface: () => describeStatusApiRuntimeSurface(runtime),
110    getAddress: () => toServerAddress(server.address()),
111    getBaseUrl: () => getStatusApiBaseUrl(server.address(), listenOptions.host)
112  };
113}
114
115export async function runStatusApiSmokeCheck(
116  options: StatusApiServerOptions = {}
117): Promise<StatusApiSmokeCheckResult> {
118  const server = await startStatusApiServer({
119    ...options,
120    host: options.host ?? DEFAULT_STATUS_API_HOST,
121    port: options.port ?? 0
122  });
123
124  try {
125    const baseUrl = server.getBaseUrl();
126
127    if (baseUrl == null) {
128      throw new Error("Status API smoke check could not resolve a listening address.");
129    }
130
131    const checks = [
132      await verifyHealthz(baseUrl),
133      await verifyStatus(baseUrl),
134      await verifyUi(baseUrl)
135    ];
136
137    return {
138      baseUrl,
139      checks
140    };
141  } finally {
142    await server.close();
143  }
144}
145
146export function getDefaultStatusApiHost(): string {
147  return DEFAULT_STATUS_API_HOST;
148}
149
150export function getDefaultStatusApiPort(): number {
151  return DEFAULT_STATUS_API_PORT;
152}
153
154async function handleStatusApiNodeRequest(
155  runtime: StatusApiRuntime,
156  request: NodeIncomingMessage,
157  response: NodeServerResponse
158): Promise<void> {
159  try {
160    const runtimeResponse = await runtime.handle({
161      method: request.method ?? "GET",
162      path: request.url ?? "/"
163    });
164
165    writeStatusApiResponse(response, runtimeResponse);
166  } catch (error) {
167    writeUnhandledErrorResponse(response, error);
168  }
169}
170
171function writeStatusApiResponse(response: NodeServerResponse, runtimeResponse: StatusApiResponse): void {
172  response.statusCode = runtimeResponse.status;
173
174  for (const [name, value] of Object.entries(runtimeResponse.headers)) {
175    response.setHeader(name, value);
176  }
177
178  response.end(runtimeResponse.body);
179}
180
181function writeUnhandledErrorResponse(response: NodeServerResponse, error: unknown): void {
182  const message = error instanceof Error ? error.message : String(error);
183
184  response.statusCode = 500;
185
186  for (const [name, value] of Object.entries(ERROR_HEADERS)) {
187    response.setHeader(name, value);
188  }
189
190  response.end(
191    `${JSON.stringify(
192      {
193        ok: false,
194        error: "internal_error",
195        message
196      },
197      null,
198      2
199    )}\n`
200  );
201}
202
203function resolveStatusApiHost(explicitHost: string | undefined, env: StatusApiEnvironment): string {
204  const host = explicitHost ?? env.BAA_STATUS_API_HOST ?? env.HOST ?? DEFAULT_STATUS_API_HOST;
205
206  return host.trim() === "" ? DEFAULT_STATUS_API_HOST : host;
207}
208
209function resolveStatusApiPort(explicitPort: number | undefined, env: StatusApiEnvironment): number {
210  if (explicitPort != null) {
211    return normalizePort(explicitPort);
212  }
213
214  const rawPort = env.BAA_STATUS_API_PORT ?? env.PORT;
215
216  if (rawPort == null || rawPort.trim() === "") {
217    return DEFAULT_STATUS_API_PORT;
218  }
219
220  return normalizePort(Number(rawPort));
221}
222
223function normalizePort(value: number): number {
224  if (!Number.isInteger(value) || value < 0 || value > 65_535) {
225    throw new TypeError(`Expected a valid TCP port, received "${String(value)}".`);
226  }
227
228  return value;
229}
230
231async function importNodeHttp(): Promise<NodeHttpModule> {
232  return import(NODE_HTTP_MODULE_SPECIFIER) as Promise<NodeHttpModule>;
233}
234
235function listenOnServer(server: NodeHttpServer, options: StatusApiListenOptions): Promise<void> {
236  return new Promise((resolve, reject) => {
237    let settled = false;
238
239    server.once("error", (error: unknown) => {
240      if (settled) {
241        return;
242      }
243
244      settled = true;
245      reject(error);
246    });
247
248    server.listen(options.port, options.host, () => {
249      if (settled) {
250        return;
251      }
252
253      settled = true;
254      resolve();
255    });
256  });
257}
258
259function closeServer(server: NodeHttpServer): Promise<void> {
260  return new Promise((resolve, reject) => {
261    server.close((error?: Error) => {
262      if (error != null) {
263        reject(error);
264        return;
265      }
266
267      resolve();
268    });
269  });
270}
271
272function toServerAddress(value: ReturnType<NodeHttpServer["address"]>): StatusApiServerAddress | string | null {
273  if (value == null || typeof value === "string") {
274    return value;
275  }
276
277  return {
278    address: value.address,
279    family: value.family,
280    port: value.port
281  };
282}
283
284function getStatusApiBaseUrl(
285  value: ReturnType<NodeHttpServer["address"]>,
286  fallbackHost: string
287): string | null {
288  if (value == null || typeof value === "string") {
289    return null;
290  }
291
292  return `http://${formatHostForUrl(selectReachableHost(value.address, fallbackHost))}:${value.port}`;
293}
294
295function selectReachableHost(address: string, fallbackHost: string): string {
296  if (address === "::" || address === "0.0.0.0") {
297    return fallbackHost === "::" || fallbackHost === "0.0.0.0" ? DEFAULT_STATUS_API_HOST : fallbackHost;
298  }
299
300  return address;
301}
302
303function formatHostForUrl(host: string): string {
304  return host.includes(":") ? `[${host}]` : host;
305}
306
307async function verifyHealthz(baseUrl: string): Promise<StatusApiSmokeCheck> {
308  const response = await fetch(new URL("/healthz", baseUrl));
309  const body = await response.text();
310
311  assertStatus(response, 200, "/healthz");
312
313  if (body.trim() !== "ok") {
314    throw new Error(`Expected /healthz to return "ok", received ${JSON.stringify(body)}.`);
315  }
316
317  return {
318    path: "/healthz",
319    status: response.status,
320    detail: 'body="ok"'
321  };
322}
323
324async function verifyStatus(baseUrl: string): Promise<StatusApiSmokeCheck> {
325  const response = await fetch(new URL("/v1/status", baseUrl));
326  const payload = (await response.json()) as {
327    ok?: boolean;
328    data?: {
329      leaderId?: string | null;
330      mode?: string;
331      source?: string;
332    };
333  };
334  const snapshot = payload.data;
335
336  assertStatus(response, 200, "/v1/status");
337
338  if (
339    payload.ok !== true ||
340    snapshot == null ||
341    snapshot.source === "empty" ||
342    !["running", "draining", "paused"].includes(snapshot.mode ?? "")
343  ) {
344    throw new Error(`Expected /v1/status to return a non-empty live snapshot, received ${JSON.stringify(payload)}.`);
345  }
346
347  return {
348    path: "/v1/status",
349    status: response.status,
350    detail: `source=${snapshot.source} mode=${snapshot.mode ?? "unknown"} leaderId=${snapshot.leaderId ?? "null"}`
351  };
352}
353
354async function verifyUi(baseUrl: string): Promise<StatusApiSmokeCheck> {
355  const response = await fetch(new URL("/v1/status/ui", baseUrl));
356  const html = await response.text();
357
358  assertStatus(response, 200, "/v1/status/ui");
359
360  if (!html.includes("<title>BAA Conductor Status</title>")) {
361    throw new Error("Expected /v1/status/ui to return the HTML status page shell.");
362  }
363
364  return {
365    path: "/v1/status/ui",
366    status: response.status,
367    detail: "html shell rendered"
368  };
369}
370
371function assertStatus(response: Response, expectedStatus: number, path: string): void {
372  if (response.status !== expectedStatus) {
373    throw new Error(`Expected ${path} to return ${expectedStatus}, received ${response.status}.`);
374  }
375}