import { createHash } from "node:crypto"; import { ANONYMOUS_PRINCIPAL, type ModuleDatabase, type ModuleRouteHandler, type ModuleRuntimeServices, 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, EMPTY_MODULE_REALTIME_REGISTRY, } 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, } from "../src/dashboard-host-routes.js"; import { DISPLAY_HOST_SCRIPT_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 { emptyModuleLifecycle } 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(): ModuleRuntimeServices { return Object.freeze({ database: Object.freeze({}) as unknown as ModuleDatabase }); } function buildApp( options: { readonly assets?: DashboardHostAssets; 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: emptyModuleLifecycle(), 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("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); } }); });