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}