import { createHash } from "node:crypto"; import { ANONYMOUS_PRINCIPAL, type ModuleRouteHandler, parseModuleManifest, parsePermissionId, type RegisteredModuleHttpRoute, } from "@ekklesia/contracts"; import { type AuthorizationEvaluator, type AuthorizationStore, createPermissionRegistry, } from "@ekklesia/core/authorization"; import type { EkklesiaIdentityRuntime } from "@ekklesia/core/identity"; import { createModuleUiPublicationRegistry, type DiscoveredModule, EMPTY_MODULE_REALTIME_REGISTRY, isReservedModuleId, type LegacyModuleRuntimeServices, type ModuleDatabase, type ModuleLifecycleConfiguration, RESERVED_MODULE_IDS, } from "@ekklesia/core/modules"; import { describe, expect, it, vi } from "vitest"; // Every byte these routes serve must come from the injected startup snapshot. Any filesystem read // while answering a request — the hallmark of a static directory — fails these tests. const readFileMock = vi.hoisted(() => vi.fn()); vi.mock("node:fs/promises", () => ({ readFile: readFileMock })); import { createApiApp } from "../src/app.js"; import { createDashboardHostAssets, type DashboardHostAssets, } from "../src/dashboard-host-assets.js"; import { DASHBOARD_HOST_PAGE_PATH, DASHBOARD_HOST_SCRIPT_PATH, DashboardHostConfigurationError, createDashboardHostRoutes, } from "../src/dashboard-host-routes.js"; import { DASHBOARD_PLATFORM_PAGES } from "@ekklesia/shell/dashboard-routes"; import { MODULE_DISPLAY_DESCRIPTOR_PREFIX, MODULE_DISPLAY_PAGE_PREFIX, } from "../src/module-display-path.js"; import { DISPLAY_HOST_SCRIPT_PATH, DISPLAY_SERVICE_WORKER_PATH, } from "../src/module-display-routes.js"; import { createInMemoryDashboardStore } from "./support/in-memory-dashboard-store.js"; import { emptyModuleConfiguration } from "./support/in-memory-module-configuration.js"; import { createInMemoryModuleLifecycleStore, moduleLifecycleSnapshotFor, } from "./support/in-memory-module-lifecycle.js"; import { emptyModuleStorage } from "./support/in-memory-module-storage.js"; import { readyHealth } from "./support/ready-health.js"; const trustedOrigin = "https://dashboard.example.test"; const modulePermission = parsePermissionId("dashboard.page.read"); const PAGE_HTML = '
\n'; const SCRIPT = "export const dashboard = 'built shell';\n"; function sha256Etag(text: string): string { return `"${createHash("sha256").update(text).digest("base64url")}"`; } const PAGE_ETAG = sha256Etag(PAGE_HTML); const SCRIPT_ETAG = sha256Etag(SCRIPT); function fakeRuntime(): EkklesiaIdentityRuntime { return { handleAuthRequest: vi.fn(), resolvePrincipal: vi.fn().mockResolvedValue(ANONYMOUS_PRINCIPAL), }; } function fakeStore(): AuthorizationStore { return { createGrant: vi.fn(), grantPlatformOwner: vi.fn(), hasActiveGrant: vi.fn().mockResolvedValue(false), isPlatformOwner: vi.fn().mockResolvedValue(false), listGrantsForUser: vi.fn().mockResolvedValue({ grants: [] }), revokeGrant: vi.fn(), revokePlatformOwner: vi.fn(), }; } function fakeModuleServices(): LegacyModuleRuntimeServices { return Object.freeze({ database: Object.freeze({}) as unknown as ModuleDatabase }); } /** A discovered module as the boot snapshot records it, from a real validated manifest. Only its id * and whether it declares a Dashboard UI matter to these routes; nothing is ever loaded. */ function discoveredModule(id: string, options: { readonly ui: boolean }): DiscoveredModule { const manifest = parseModuleManifest({ capabilities: [], compatibility: { moduleSdk: "^0.2.0", platform: "^0.2.0" }, entrypoints: { server: "./dist/server/index.js", ...(options.ui ? { ui: "./dist/ui/index.js" } : {}), }, id, manifestVersion: 8, name: `${id} module`, permissions: [], version: "0.1.0", }); return { contributions: [], directory: `/modules/${id}`, entrypoints: { server: `/modules/${id}/dist/server/index.js`, ...(options.ui ? { ui: `/modules/${id}/dist/ui/index.js` } : {}), }, manifest, manifestPath: `/modules/${id}/ekklesia.module.json`, packageName: `@test/${id}`, }; } function buildApp( options: { readonly assets?: DashboardHostAssets; readonly configuration?: ModuleLifecycleConfiguration; readonly discovered?: readonly DiscoveredModule[]; readonly moduleRoutes?: readonly RegisteredModuleHttpRoute[]; } = {}, ) { const evaluator: AuthorizationEvaluator = { authorize: vi.fn().mockResolvedValue({ allowed: true }), }; const runtime = fakeRuntime(); const app = createApiApp({ authorization: { evaluator, registry: createPermissionRegistry([ { baseline: "public", description: "Test permission.", id: modulePermission }, ]), store: fakeStore(), }, moduleRoutes: options.moduleRoutes ?? Object.freeze([]), moduleRealtime: EMPTY_MODULE_REALTIME_REGISTRY, moduleServices: fakeModuleServices(), moduleUi: createModuleUiPublicationRegistry([]), moduleLifecycle: { snapshot: moduleLifecycleSnapshotFor(options.discovered ?? [], options.configuration), store: createInMemoryModuleLifecycleStore(), }, moduleConfiguration: emptyModuleConfiguration(), moduleStorage: emptyModuleStorage(), dashboard: createInMemoryDashboardStore(), dashboardHostAssets: options.assets ?? createDashboardHostAssets(PAGE_HTML, SCRIPT), health: readyHealth(), runtime, trustedOrigins: [trustedOrigin], }); return { app, evaluator, runtime }; } function expectPageSecurityHeaders(response: Response): void { expect(response.headers.get("x-frame-options")).toBe("DENY"); expect(response.headers.get("content-security-policy")).toBe("frame-ancestors 'none'"); expect(response.headers.get("referrer-policy")).toBe("no-referrer"); expect(response.headers.get("x-content-type-options")).toBe("nosniff"); } async function expectSanitizedNotFound(response: Response, label: string): Promise { expect(response.status, label).toBe(404); expect(response.headers.get("content-type"), label).toMatch(/^application\/json/u); expect(await response.json(), label).toEqual({ code: "NOT_FOUND" }); } describe("GET / (the Dashboard page)", () => { it("serves the exact startup HTML bytes with revalidation, a strong ETag, and the page security headers", async () => { const { app } = buildApp(); const response = await app.request(DASHBOARD_HOST_PAGE_PATH); expect(response.status).toBe(200); expect(await response.text()).toBe(PAGE_HTML); expect(response.headers.get("content-type")).toBe("text/html; charset=utf-8"); expect(response.headers.get("cache-control")).toBe("no-cache"); expect(response.headers.get("etag")).toBe(PAGE_ETAG); expectPageSecurityHeaders(response); // The one CSP directive is the only one: the full policy is a separate decision. expect(response.headers.get("content-security-policy")).not.toContain(";"); }); it("serves the same page for an emailed-link landing, since the query never selects a file", async () => { const { app } = buildApp(); const response = await app.request("/?ekklesia_auth=reset-password&token=abc123"); expect(response.status).toBe(200); expect(await response.text()).toBe(PAGE_HTML); expect(response.headers.get("etag")).toBe(PAGE_ETAG); }); it("answers a matching If-None-Match with an empty 304 that keeps the validator and every page security header", async () => { const { app } = buildApp(); for (const ifNoneMatch of [PAGE_ETAG, `W/${PAGE_ETAG}`, `"other", ${PAGE_ETAG}`, "*"]) { const response = await app.request(DASHBOARD_HOST_PAGE_PATH, { headers: { "If-None-Match": ifNoneMatch }, }); expect(response.status, ifNoneMatch).toBe(304); expect(await response.text(), ifNoneMatch).toBe(""); expect(response.headers.get("etag")).toBe(PAGE_ETAG); expect(response.headers.get("cache-control")).toBe("no-cache"); expectPageSecurityHeaders(response); } }); it("serves the full page again when the browser holds a different build's validator", async () => { const { app } = buildApp(); const response = await app.request(DASHBOARD_HOST_PAGE_PATH, { headers: { "If-None-Match": sha256Etag("an older build") }, }); expect(response.status).toBe(200); expect(await response.text()).toBe(PAGE_HTML); }); it("is unauthenticated and outside the API's CORS policy: it resolves no principal and reflects no origin", async () => { const { app, evaluator, runtime } = buildApp(); const response = await app.request(DASHBOARD_HOST_PAGE_PATH, { headers: { Cookie: "better-auth.session_token=anything", Origin: trustedOrigin }, }); expect(response.status).toBe(200); expect(runtime.resolvePrincipal).not.toHaveBeenCalled(); expect(evaluator.authorize).not.toHaveBeenCalled(); expect(response.headers.get("access-control-allow-origin")).toBeNull(); expect(response.headers.get("set-cookie")).toBeNull(); }); }); describe("Dashboard deep links (ADR 0057)", () => { const installed = () => [ discoveredModule("signage", { ui: true }), discoveredModule("calendar", { ui: true }), discoveredModule("retired", { ui: true }), // A display-only module has no Dashboard page (ADR 0033). discoveredModule("lobby-screen", { ui: false }), ]; // Installed but disabled: its address still opens the Dashboard, which says it is unavailable. const configuration = { modules: [{ enabled: false, moduleId: "retired" }] }; const deepLinks = [ "/settings", "/settings/", "/settings/displays/add", "/account", "/account/appearance", "/signage", "/signage/", // Reserved for the module's own pages, which the shell will interpret (ADR 0057). "/signage/timelines", "/signage/timelines/a1b2", "/calendar", "/retired", ]; it("serves the shell's own platform pages, from the shell's own list", () => { expect(DASHBOARD_PLATFORM_PAGES).toEqual(["settings", "account"]); }); it("serves the identical page, validator, and security headers at every Dashboard page, so a refresh stays put", async () => { const { app } = buildApp({ configuration, discovered: installed() }); for (const path of deepLinks) { const response = await app.request(path); expect(response.status, path).toBe(200); expect(await response.text(), path).toBe(PAGE_HTML); expect(response.headers.get("content-type"), path).toBe("text/html; charset=utf-8"); expect(response.headers.get("cache-control"), path).toBe("no-cache"); expect(response.headers.get("etag"), path).toBe(PAGE_ETAG); expectPageSecurityHeaders(response); } expect(readFileMock).not.toHaveBeenCalled(); }); it("revalidates a deep link with the page's one validator", async () => { const { app } = buildApp({ discovered: installed() }); for (const path of ["/settings/displays", "/signage"]) { const response = await app.request(path, { headers: { "If-None-Match": PAGE_ETAG } }); expect(response.status, path).toBe(304); expect(await response.text(), path).toBe(""); expectPageSecurityHeaders(response); } }); it("never turns an arbitrary top-level path into a Dashboard page", async () => { const { app } = buildApp({ configuration, discovered: installed() }); for (const path of [ "/definitely-not-a-real-module", "/definitely-not-a-real-module/timelines", // Installed, but with no Dashboard UI. "/lobby-screen", // Look-alikes of real pages. "/signages", "/signage-old", "/Signage", "/settings-old", "/accounts", // The former implementation prefix is not a namespace at all. "/modules", "/modules/signage", ]) { await expectSanitizedNotFound(await app.request(path), path); } }); it("serves a module's page only while that module is installed", async () => { const { app } = buildApp({ discovered: [] }); await expectSanitizedNotFound(await app.request("/signage"), "/signage"); expect((await app.request("/settings")).status).toBe(200); }); it("serves the page and never the script, whatever a deep link's last segment looks like", async () => { const { app } = buildApp({ discovered: installed() }); for (const path of [ "/signage/app.js", "/settings/assets/ekklesia-dashboard/app.js", "/account/index.html", ]) { expect(await (await app.request(path)).text(), path).toBe(PAGE_HTML); } }); it("answers no other method at a Dashboard page", async () => { const { app } = buildApp({ discovered: installed() }); for (const path of ["/settings/displays", "/signage"]) { for (const method of ["POST", "PUT", "PATCH", "DELETE"]) { await expectSanitizedNotFound(await app.request(path, { method }), `${method} ${path}`); } } }); it("refuses at composition a module page that would shadow a platform namespace", () => { for (const modulePages of [["api"], ["settings"], ["display"], ["Not-An-Id"], ["a", "a"]]) { expect( () => createDashboardHostRoutes(createDashboardHostAssets(PAGE_HTML, SCRIPT), { modulePages }), JSON.stringify(modulePages), ).toThrow(DashboardHostConfigurationError); } }); }); describe("the platform's reserved top-level names (ADR 0057)", () => { it("cover every top-level namespace the API registers, so a module page can never shadow one", () => { const { app } = buildApp({ discovered: [discoveredModule("signage", { ui: true })] }); const topLevel = new Set( app.routes .map((route) => route.path.split("/")[1] ?? "") .filter((segment) => segment !== "" && segment !== "*"), ); topLevel.delete("signage"); // This composition publishes no display, so the display host's routes are absent here; they // are checked through their own path constants below. expect([...topLevel].sort()).toEqual(["account", "api", "assets", "settings"]); for (const segment of topLevel) { expect(isReservedModuleId(segment), segment).toBe(true); } }); it("cover the display host's pages, service worker, and script, and the Dashboard's script", () => { for (const path of [ MODULE_DISPLAY_PAGE_PREFIX, DISPLAY_SERVICE_WORKER_PATH, DISPLAY_HOST_SCRIPT_PATH, DASHBOARD_HOST_SCRIPT_PATH, MODULE_DISPLAY_DESCRIPTOR_PREFIX, ]) { expect(isReservedModuleId(path.split("/")[1] ?? ""), path).toBe(true); } expect([...RESERVED_MODULE_IDS].sort()).toEqual([ "account", "api", "assets", "display", "settings", ]); }); it("include every Dashboard platform page the shell interprets", () => { for (const page of DASHBOARD_PLATFORM_PAGES) { expect(RESERVED_MODULE_IDS, page).toContain(page); } }); }); describe("GET /assets/ekklesia-dashboard/app.js (the Dashboard script)", () => { it("serves the exact startup script bytes as JavaScript with revalidation and a strong ETag", async () => { const { app } = buildApp(); const response = await app.request(DASHBOARD_HOST_SCRIPT_PATH); expect(response.status).toBe(200); expect(await response.text()).toBe(SCRIPT); expect(response.headers.get("content-type")).toBe("text/javascript; charset=utf-8"); expect(response.headers.get("cache-control")).toBe("no-cache"); expect(response.headers.get("etag")).toBe(SCRIPT_ETAG); expect(response.headers.get("x-content-type-options")).toBe("nosniff"); }); it("answers a matching If-None-Match with an empty 304 carrying the script's own validator", async () => { const { app } = buildApp(); const response = await app.request(DASHBOARD_HOST_SCRIPT_PATH, { headers: { "If-None-Match": SCRIPT_ETAG }, }); expect(response.status).toBe(304); expect(await response.text()).toBe(""); expect(response.headers.get("etag")).toBe(SCRIPT_ETAG); expect(response.headers.get("cache-control")).toBe("no-cache"); }); it("does not accept the page's validator as the script's", async () => { const { app } = buildApp(); const response = await app.request(DASHBOARD_HOST_SCRIPT_PATH, { headers: { "If-None-Match": PAGE_ETAG }, }); expect(response.status).toBe(200); }); }); describe("Dashboard host boundaries", () => { it("serves nothing but its two exact paths, answering every other spelling with the sanitized 404", async () => { const { app } = buildApp(); for (const path of [ "/index.html", "/app.js", "/app.js.map", "/assets/ekklesia-dashboard/app.js.map", "/assets/ekklesia-dashboard/", "/assets/ekklesia-dashboard", "/assets/ekklesia-dashboard/app.js/", "/assets/ekklesia-dashboard/index.html", "/assets/", "/assets/ekklesia-dashboard/APP.JS", "/dashboard", "/login", ]) { await expectSanitizedNotFound(await app.request(path), path); } }); it("cannot be walked out of: encoded traversal, separators, and double slashes reach no file", async () => { const { app } = buildApp(); for (const path of [ "/assets/ekklesia-dashboard/%2e%2e/%2e%2e/package.json", "/assets/ekklesia-dashboard/..%2fapp.js.map", "/assets/ekklesia-dashboard/%2e%2e%2fapp.js.map", "/assets/ekklesia-dashboard/app.js%2f..%2f..%2fpackage.json", "/assets/ekklesia-dashboard/..%5c..%5cpackage.json", "/%2e%2e/%2e%2e/etc/passwd", "/../../etc/passwd", "//assets/ekklesia-dashboard/app.js", "/assets//ekklesia-dashboard/app.js", "/assets/ekklesia-dashboard/app.js%00.map", ]) { const response = await app.request(path); expect(await response.text(), path).not.toContain(SCRIPT); expect(response.status, path).toBe(404); } expect(readFileMock).not.toHaveBeenCalled(); }); it("never touches the filesystem while answering, even across every served path", async () => { const { app } = buildApp(); await app.request(DASHBOARD_HOST_PAGE_PATH); await app.request(DASHBOARD_HOST_SCRIPT_PATH); await app.request("/assets/ekklesia-dashboard/app.js.map"); expect(readFileMock).not.toHaveBeenCalled(); }); it("serves GET (and Hono's implicit HEAD) only; every other method is the ordinary 404", async () => { const { app } = buildApp(); const head = await app.request(DASHBOARD_HOST_PAGE_PATH, { method: "HEAD" }); expect(head.status).toBe(200); expect(head.headers.get("etag")).toBe(PAGE_ETAG); expect(await head.text()).toBe(""); for (const path of [DASHBOARD_HOST_PAGE_PATH, DASHBOARD_HOST_SCRIPT_PATH]) { for (const method of ["POST", "PUT", "PATCH", "DELETE"]) { await expectSanitizedNotFound(await app.request(path, { method }), `${method} ${path}`); } } }); it("leaves the API, the display host's namespace, and module routes exactly where they were", async () => { const moduleHandler = vi.fn( async () => new Response("module", { status: 200 }), ); const { app } = buildApp({ moduleRoutes: [ Object.freeze({ bodyMode: "buffered", handler: moduleHandler, maxBodyBytes: 1024, method: "GET", moduleId: "dashboard", path: "/", permission: modulePermission, }) as RegisteredModuleHttpRoute, ], }); expect(await (await app.request("/api/v1/principal")).json()).toEqual({ kind: "anonymous" }); expect(await (await app.request("/api/v1/health/live")).json()).toEqual({ status: "live" }); expect(await (await app.request("/api/v1/modules/dashboard")).text()).toBe("module"); await expectSanitizedNotFound(await app.request("/api/v1/unknown"), "/api/v1/unknown"); // No display is published here, so the display host's script is absent rather than answered // by the Dashboard's. await expectSanitizedNotFound( await app.request(DISPLAY_HOST_SCRIPT_PATH), DISPLAY_HOST_SCRIPT_PATH, ); expect(DASHBOARD_HOST_SCRIPT_PATH.startsWith("/assets/ekklesia-display/")).toBe(false); expect(await (await app.request(DASHBOARD_HOST_PAGE_PATH)).text()).toBe(PAGE_HTML); expect(moduleHandler).toHaveBeenCalledTimes(1); }); it("refuses at composition a snapshot whose validator does not match its bytes, or empty bytes", () => { const consistent = createDashboardHostAssets(PAGE_HTML, SCRIPT); for (const assets of [ { ...consistent, html: `${PAGE_HTML}` }, { ...consistent, scriptEtag: PAGE_ETAG }, { html: "", htmlEtag: sha256Etag(""), script: SCRIPT, scriptEtag: SCRIPT_ETAG }, ]) { expect(() => buildApp({ assets })).toThrow(DashboardHostConfigurationError); } }); });