feat(api): add unauthenticated health endpoints
CI / verify (pull_request) Successful in 45s
CI / infrastructure-smoke (pull_request) Successful in 12s
CI / migration-smoke (pull_request) Successful in 1m42s

Add GET /api/v1/health/live and GET /api/v1/health/ready per ADR 0044.

- live returns a fixed 200 {"status":"live"} whenever the process serves.
- ready returns 200 {"status":"ready"} only when module startup has
  completed (and shutdown has not begun) and a database ping succeeds
  within 2s; otherwise a fixed 503 {"code":"NOT_READY"}.
- The ping runs on the shared pool as one simple-protocol message with
  a transaction-local statement_timeout, so PostgreSQL cancels a stalled
  statement; the pool discards the connection on failure.
- At most one ping is ever unsettled: probes share its outcome and, once
  its deadline passes, answer 503 immediately until it settles.
- Both routes set Cache-Control: no-store, accept GET only (405 with
  Allow: GET otherwise, HEAD included), never resolve a principal, and
  are mounted in app.ts beside, never inside, /api/v1/modules/.
- createApiApp gains a required `health` option; startApi supplies it
  and clears module readiness when shutdown begins.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
ekklesia
2026-10-01 16:47:01 -05:00
co-authored by Claude Opus 5.5
parent d593c10f1d
commit aaec9789c4
18 changed files with 727 additions and 0 deletions
+13
View File
@@ -31,6 +31,7 @@ import { createDashboardRoutes } from "./dashboard-routes.js";
import { createDeviceRoutes } from "./device-routes.js";
import type { DisplayHostAssets } from "./display-host-assets.js";
import { createGrantsRoutes } from "./grants-routes.js";
import { type ApiHealthOptions, createHealthRoutes } from "./health-routes.js";
import { createModuleConfigurationRoutes } from "./module-configuration-routes.js";
import { mountModuleDisplayRoutes } from "./module-display-routes.js";
import { canonicalizeVaryHeader, mountModuleHttpRoutes } from "./module-http-routes.js";
@@ -137,6 +138,13 @@ export interface CreateApiAppOptions {
* presentation only — nothing reachable through this store influences discovery, dependency
* ordering, module startup, or any authorization decision. */
readonly dashboard: DashboardStore;
/**
* The readiness inputs behind the unauthenticated health routes (ADR 0044). Required rather than
* defaulted, like every other dependency here: a composition that cannot reach a database says so
* with a check that rejects, never falls through to an implicitly healthy one. `checkDatabase` is
* the bare query; the routes themselves bound and serialize it. See `./health-routes.js`.
*/
readonly health: ApiHealthOptions;
readonly runtime: EkklesiaIdentityRuntime;
/** Exact allowed browser origins. Never a wildcard; combined with `credentials: true`, only an
* exact, reflected match is ever returned in `Access-Control-Allow-Origin`. Also the trusted set
@@ -341,6 +349,11 @@ export function createApiApp(options: CreateApiAppOptions): Hono {
}
});
// Unauthenticated liveness/readiness under a platform-owned sibling prefix: like every route below,
// it is mounted before module routes and never inside `/api/v1/modules/`, which installed modules
// own entirely. See `./health-routes.js`.
app.route("/", createHealthRoutes(options.health));
app.route(
"/",
createGrantsRoutes({