import { createHash } from "node:crypto"; import { readFile } from "node:fs/promises"; import { dashboardHostAssetUrls } from "@ekklesia/shell/dashboard-assets"; /** * The two immutable, platform-owned files that make up the built Ekklesia Dashboard (ADR 0044). * * Exactly the model `./display-host-assets.js` uses: read once, before the HTTP listener is bound, * from the exact URLs the surface package exports. Request input is never involved in this read, the * package is never exposed as a static directory, and the source map beside the script is never * read. Text rather than a mutable `Buffer` keeps the frozen snapshot a genuinely immutable value * every response can reuse. */ export interface DashboardHostAssets { readonly html: string; readonly htmlEtag: string; readonly script: string; readonly scriptEtag: string; } /** A startup failure with a fixed, non-secret shape. The originating filesystem exception, package * location, and machine path are intentionally not retained or surfaced. */ export class DashboardHostAssetError extends Error { readonly code = "DASHBOARD_HOST_ASSET_UNAVAILABLE" as const; constructor() { super("The Dashboard host browser assets are unavailable."); this.name = "DashboardHostAssetError"; this.stack = `${this.name}: ${this.message}`; Object.freeze(this); } } function strongEtag(text: string): string { return `"${createHash("sha256").update(text).digest("base64url")}"`; } /** * Captures one immutable, internally consistent Dashboard asset snapshot. * * Each strong validator is derived from the exact text in the same snapshot, so bytes from one build * can never be paired with an ETag from another. Empty output is a failed shell build rather than a * valid page that can never render anything. */ export function createDashboardHostAssets(html: string, script: string): DashboardHostAssets { if (html.trim().length === 0 || script.trim().length === 0) { throw new DashboardHostAssetError(); } return Object.freeze({ html, htmlEtag: strongEtag(html), script, scriptEtag: strongEtag(script), }); } /** Loads only the two surface-declared files. A missing or unreadable build fails startup rather * than leaving an API whose own origin answers `/` with a 404. */ export async function loadDashboardHostAssets(): Promise { try { // Sequential deliberately, as for the display host: diagnostics stay deterministic and no // rejected sibling read is left running after this function has already failed. const html = await readFile(new URL(dashboardHostAssetUrls.html), "utf8"); const script = await readFile(new URL(dashboardHostAssetUrls.script), "utf8"); return createDashboardHostAssets(html, script); } catch { throw new DashboardHostAssetError(); } }