import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; import { ANONYMOUS_PRINCIPAL, type ModuleDatabase, type ModuleRuntimeServices, } from "@ekklesia/contracts"; import type { AuthorizationEvaluator, AuthorizationStore } from "@ekklesia/core/authorization"; import type { EkklesiaIdentityRuntime } from "@ekklesia/core/identity"; import { composePlatformPermissionRegistry, createModuleUiPublicationRegistry, type DiscoveredModule, discoverModules, EMPTY_MODULE_REALTIME_REGISTRY, } from "@ekklesia/core/modules"; import { loadModuleUi } from "@ekklesia/module-ui-host"; import { MODULE_UI_ABI_VERSION, NO_MODULE_UI_CONTRIBUTIONS } from "@ekklesia/module-ui-sdk"; import { Window } from "happy-dom"; import type { Hono } from "hono"; import { afterEach, describe, expect, it, vi } from "vitest"; import { createInMemoryDashboardStore } from "./support/in-memory-dashboard-store.js"; import { createApiApp } from "../src/app.js"; import { createDisplayHostAssets } from "../src/display-host-assets.js"; import { MODULE_UI_ASSET_PREFIX, MODULE_UI_CACHE_CONTROL, MODULE_UI_CONTENT_TYPE, moduleContributionArtifactUrl, moduleDisplayUiArtifactUrl, moduleUiArtifactUrl, } from "../src/module-ui-routes.js"; import { createInMemoryModuleLifecycleStore, moduleLifecycleSnapshotFor, } from "./support/in-memory-module-lifecycle.js"; import { moduleConfigurationOptionFor } from "./support/in-memory-module-configuration.js"; import { moduleStorageOptionFor } from "./support/in-memory-module-storage.js"; const trustedOrigin = "https://dashboard.example.test"; const temporaryDirectories: string[] = []; afterEach(async () => { await Promise.all( temporaryDirectories .splice(0) .map((directory) => rm(directory, { force: true, recursive: true })), ); }); async function createModulesDirectory(): Promise { const directory = await mkdtemp(path.join(tmpdir(), "ekklesia-api-ui-publication-")); temporaryDirectories.push(directory); return directory; } /** A real, loadable module UI artifact: a valid `ModuleUiDefinition` default export, written as * plain ESM. `@ekklesia/module-ui-build` already proves the esbuild pipeline emits this shape * (`packages/module-ui-host/tests/real-artifact.test.ts`); what these tests need is a byte * sequence whose exact publication and loader-compatibility can be asserted. */ function artifactSource(id: string, version: string): string { return `export default { abi: ${MODULE_UI_ABI_VERSION}, id: ${JSON.stringify(id)}, version: ${JSON.stringify(version)}, mount(container, context) { container.textContent = context.moduleId + ":mounted"; return () => { container.textContent = ""; }; }, }; `; } interface ModuleFixture { readonly id: string; readonly version?: string; /** Written to `dist/ui/index.js` and declared as `entrypoints.ui`. Omit for a backend-only * module, whose manifest then declares no UI entrypoint at all. */ readonly ui?: string; /** Written to a distinct artifact and declared as the protected display entrypoint. */ readonly display?: string; /** Extra package-relative files written alongside the artifact — a source map, a sibling bundle * chunk, an operator secret — so "only the declared artifact is reachable" is asserted against * files that genuinely exist on disk rather than against hypothetical ones. */ readonly extraFiles?: Readonly>; /** Capabilities this module provides — what another module's contributions target. */ readonly capabilities?: readonly { readonly id: string; readonly version: string }[]; /** Browser renderers this module contributes to another module's capability. */ readonly contributions?: readonly { readonly localName: string; readonly capability: string; readonly capabilityRange: string; readonly source: string; }[]; } async function createModule(modulesDirectory: string, fixture: ModuleFixture): Promise { const version = fixture.version ?? "0.1.0"; const displayPermission = `${fixture.id}.display.view`; const moduleDirectory = path.join(modulesDirectory, fixture.id); await mkdir(path.join(moduleDirectory, "dist/server"), { recursive: true }); await writeFile( path.join(moduleDirectory, "package.json"), JSON.stringify({ ekklesia: { layer: "module" }, name: `@test/module-${fixture.id}`, type: "module", version, }), ); await writeFile( path.join(moduleDirectory, "ekklesia.module.json"), JSON.stringify({ compatibility: { moduleSdk: "^0.1.0", platform: "^0.1.0" }, entrypoints: { server: "./dist/server/index.js", ...(fixture.ui === undefined ? {} : { ui: "./dist/ui/index.js" }), ...(fixture.display === undefined ? {} : { display: { artifact: "./dist/display/index.js", permission: displayPermission, }, }), }, capabilities: fixture.capabilities ?? [], contributions: (fixture.contributions ?? []).map((contribution) => ({ artifact: `./dist/contributions/${contribution.localName}/index.js`, capability: contribution.capability, capabilityRange: contribution.capabilityRange, id: `${fixture.id}.${contribution.localName}`, version: "1.0.0", })), id: fixture.id, manifestVersion: 7, name: `${fixture.id} module`, permissions: fixture.display === undefined ? [] : [ { baseline: "restricted", description: `View ${fixture.id}'s display`, id: displayPermission, }, ], version, }), ); await writeFile( path.join(moduleDirectory, "dist/server/index.js"), `export default { id: ${JSON.stringify(fixture.id)}, version: ${JSON.stringify(version)} };`, ); if (fixture.ui !== undefined) { await mkdir(path.join(moduleDirectory, "dist/ui"), { recursive: true }); await writeFile(path.join(moduleDirectory, "dist/ui/index.js"), fixture.ui); } for (const contribution of fixture.contributions ?? []) { const contributionDirectory = path.join( moduleDirectory, "dist/contributions", contribution.localName, ); await mkdir(contributionDirectory, { recursive: true }); await writeFile(path.join(contributionDirectory, "index.js"), contribution.source); await writeFile(path.join(contributionDirectory, "index.js.map"), '{"sources":["secret"]}'); } if (fixture.display !== undefined) { await mkdir(path.join(moduleDirectory, "dist/display"), { recursive: true }); await writeFile(path.join(moduleDirectory, "dist/display/index.js"), fixture.display); } for (const [relativePath, contents] of Object.entries(fixture.extraFiles ?? {})) { const target = path.join(moduleDirectory, relativePath); await mkdir(path.dirname(target), { recursive: true }); await writeFile(target, contents); } } interface Installation { readonly app: Hono; readonly modules: readonly DiscoveredModule[]; readonly modulesDirectory: string; } /** * A real installation: fixtures on disk, Core's real `discoverModules` (manifest validation, * compiled-artifact validation, package containment, symlink canonicalization), Core's real * publication registry, and the real `createApiApp`. Nothing about the publication policy or the * artifact path is hand-built, so these tests exercise the actual security boundary rather than a * reconstruction of it. */ async function install( fixtures: readonly ModuleFixture[], publicationModuleIds?: readonly string[], ): Promise { const modulesDirectory = await createModulesDirectory(); for (const fixture of fixtures) { await createModule(modulesDirectory, fixture); } const modules = await discoverModules({ moduleSdkVersion: "0.1.0", modulesDirectory, platformVersion: "0.1.0", }); const publicationModules = publicationModuleIds === undefined ? modules : modules.filter((module) => publicationModuleIds.includes(module.manifest.id)); const database = Object.freeze({}) as unknown as ModuleDatabase; const services: ModuleRuntimeServices = Object.freeze({ database }); const runtime: EkklesiaIdentityRuntime = { handleAuthRequest: vi.fn().mockResolvedValue(new Response(null, { status: 200 })), // Anonymous, and every authorization decision denied: whatever this route does, it does it // without a session and without a single permission. resolvePrincipal: vi.fn().mockResolvedValue(ANONYMOUS_PRINCIPAL), }; const evaluator: AuthorizationEvaluator = { authorize: vi.fn().mockResolvedValue({ allowed: false }), }; const store = { 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(), } as unknown as AuthorizationStore; const app = createApiApp({ authorization: { evaluator, registry: composePlatformPermissionRegistry(modules), store }, ...(publicationModules.some((module) => module.entrypoints.display !== undefined) ? { displayHostAssets: createDisplayHostAssets( '
', "export {};\n", ), } : {}), moduleRoutes: Object.freeze([]), moduleRealtime: EMPTY_MODULE_REALTIME_REGISTRY, moduleServices: services, moduleUi: createModuleUiPublicationRegistry(publicationModules), moduleLifecycle: { snapshot: moduleLifecycleSnapshotFor(modules), store: createInMemoryModuleLifecycleStore(), }, moduleConfiguration: moduleConfigurationOptionFor(modules), moduleStorage: moduleStorageOptionFor(modules), dashboard: createInMemoryDashboardStore(), runtime, trustedOrigins: [trustedOrigin], }); return { app, modules, modulesDirectory }; } describe("module UI artifact publication — happy path", () => { it("serves the declared artifact byte-for-byte at the canonical URL", async () => { const source = artifactSource("announcements", "1.4.2"); const { app } = await install([{ id: "announcements", ui: source, version: "1.4.2" }]); const response = await app.request( moduleUiArtifactUrl({ moduleId: "announcements", version: "1.4.2" }), ); expect(response.status).toBe(200); expect(await response.text()).toBe(source); }); it("serves it as ES module JavaScript, with sniffing disabled", async () => { const { app } = await install([ { id: "announcements", ui: artifactSource("announcements", "0.1.0") }, ]); const response = await app.request( moduleUiArtifactUrl({ moduleId: "announcements", version: "0.1.0" }), ); expect(response.headers.get("content-type")).toBe(MODULE_UI_CONTENT_TYPE); expect(response.headers.get("x-content-type-options")).toBe("nosniff"); }); it("revalidates rather than promising immutability the platform cannot enforce", async () => { const { app } = await install([ { id: "announcements", ui: artifactSource("announcements", "0.1.0") }, ]); const response = await app.request( moduleUiArtifactUrl({ moduleId: "announcements", version: "0.1.0" }), ); expect(response.headers.get("cache-control")).toBe(MODULE_UI_CACHE_CONTROL); // A reused version can be rebuilt with different bytes, so neither of these may ever appear. expect(response.headers.get("cache-control")).not.toContain("immutable"); expect(response.headers.get("cache-control")).not.toContain("max-age"); // A strong validator (no `W/` prefix), so the revalidation above is actually decidable. expect(response.headers.get("etag")).toMatch(/^"[A-Za-z0-9_-]+"$/u); }); it("varies on Origin, so a shared cache cannot reuse one origin's CORS response for another", async () => { const { app } = await install([ { id: "announcements", ui: artifactSource("announcements", "0.1.0") }, ]); const response = await app.request( moduleUiArtifactUrl({ moduleId: "announcements", version: "0.1.0" }), { headers: { origin: trustedOrigin } }, ); expect(response.headers.get("access-control-allow-origin")).toBe(trustedOrigin); expect(response.headers.get("vary")?.toLowerCase()).toContain("origin"); }); it("answers a matching conditional request with 304 and no body", async () => { const { app } = await install([ { id: "announcements", ui: artifactSource("announcements", "0.1.0") }, ]); const url = moduleUiArtifactUrl({ moduleId: "announcements", version: "0.1.0" }); const etag = (await app.request(url)).headers.get("etag") as string; const revalidated = await app.request(url, { headers: { "if-none-match": etag } }); expect(revalidated.status).toBe(304); expect(await revalidated.text()).toBe(""); expect(revalidated.headers.get("etag")).toBe(etag); expect(revalidated.headers.get("cache-control")).toBe(MODULE_UI_CACHE_CONTROL); }); it("applies RFC 9110 weak comparison and the wildcard, and refuses a stale validator", async () => { const { app } = await install([ { id: "announcements", ui: artifactSource("announcements", "0.1.0") }, ]); const url = moduleUiArtifactUrl({ moduleId: "announcements", version: "0.1.0" }); const etag = (await app.request(url)).headers.get("etag") as string; const cases: ReadonlyArray = [ [`W/${etag}`, 304], ["*", 304], [`"someone-elses-tag", ${etag}`, 304], ['"stale-tag"', 200], ["not a valid header at all", 200], ]; for (const [header, expected] of cases) { const response = await app.request(url, { headers: { "if-none-match": header } }); expect(response.status, header).toBe(expected); } }); it("recomputes the validator from the bytes actually on disk after a same-version rebuild", async () => { const { app, modulesDirectory } = await install([ { id: "announcements", ui: artifactSource("announcements", "0.1.0") }, ]); const url = moduleUiArtifactUrl({ moduleId: "announcements", version: "0.1.0" }); const first = await app.request(url); const firstEtag = first.headers.get("etag"); // Exactly the develop-in-place case an `immutable` policy would have made unrecoverable. const rebuilt = `${artifactSource("announcements", "0.1.0")}// rebuilt\n`; await writeFile(path.join(modulesDirectory, "announcements/dist/ui/index.js"), rebuilt); const second = await app.request(url, { headers: { "if-none-match": firstEtag as string } }); expect(second.status).toBe(200); expect(await second.text()).toBe(rebuilt); expect(second.headers.get("etag")).not.toBe(firstEtag); }); it("serves the artifact without executing it, even when its top level throws", async () => { const exploding = 'throw new Error("module UI must never be evaluated on the server");\n'; const { app } = await install([{ id: "announcements", ui: exploding }]); const response = await app.request( moduleUiArtifactUrl({ moduleId: "announcements", version: "0.1.0" }), ); expect(response.status).toBe(200); expect(await response.text()).toBe(exploding); }); it("requires no authentication, by deliberate decision", async () => { // The installation's identity runtime resolves an anonymous principal and its evaluator denies // every permission. If this route ever grows an authentication or authorization requirement, // this test is the one that must be consciously changed. const { app } = await install([ { id: "announcements", ui: artifactSource("announcements", "0.1.0") }, ]); const response = await app.request( moduleUiArtifactUrl({ moduleId: "announcements", version: "0.1.0" }), ); expect(response.status).toBe(200); }); it("still refuses a protected platform API to the same anonymous caller", async () => { // Publication grants no authority: the very same caller that just downloaded the bundle is // still denied every permission-protected route. const { app } = await install([ { id: "announcements", ui: artifactSource("announcements", "0.1.0") }, ]); expect( (await app.request(moduleUiArtifactUrl({ moduleId: "announcements", version: "0.1.0" }))) .status, ).toBe(200); const grants = await app.request("/api/v1/grants?userId=someone"); expect(grants.status).toBe(403); }); }); describe("module display UI artifact publication", () => { it("serves the declared display bytes anonymously at the exact role-specific URL", async () => { const source = `${artifactSource("announcements", "1.4.2")}// display\n`; const { app } = await install([{ display: source, id: "announcements", version: "1.4.2" }]); const url = moduleDisplayUiArtifactUrl({ moduleId: "announcements", version: "1.4.2" }); expect(url).toBe(`${MODULE_UI_ASSET_PREFIX}/announcements/1.4.2/display`); const response = await app.request(url); expect(response.status).toBe(200); expect(await response.text()).toBe(source); expect(response.headers.get("content-type")).toBe(MODULE_UI_CONTENT_TYPE); expect(response.headers.get("cache-control")).toBe(MODULE_UI_CACHE_CONTROL); expect(response.headers.get("x-content-type-options")).toBe("nosniff"); const etag = response.headers.get("etag") as string; expect(etag).toMatch(/^"[A-Za-z0-9_-]+"$/u); const revalidated = await app.request(url, { headers: { "if-none-match": etag } }); expect(revalidated.status).toBe(304); expect(await revalidated.text()).toBe(""); expect(revalidated.headers.get("cache-control")).toBe(MODULE_UI_CACHE_CONTROL); expect(revalidated.headers.get("etag")).toBe(etag); }); it("keeps display-only, primary-only, both, and neither roles strictly separate", async () => { const primary = `${artifactSource("signage", "0.1.0")}// primary\n`; const display = `${artifactSource("signage", "0.1.0")}// display\n`; const { app } = await install([ { display: `${display}// announcements\n`, id: "announcements" }, { id: "calendar", ui: `${primary}// calendar\n` }, { display, id: "signage", ui: primary }, { id: "backend" }, ]); const primaryUrl = (moduleId: string) => moduleUiArtifactUrl({ moduleId, version: "0.1.0" }); const displayUrl = (moduleId: string) => moduleDisplayUiArtifactUrl({ moduleId, version: "0.1.0" }); expect((await app.request(primaryUrl("announcements"))).status).toBe(404); expect((await app.request(displayUrl("announcements"))).status).toBe(200); expect((await app.request(primaryUrl("calendar"))).status).toBe(200); expect((await app.request(displayUrl("calendar"))).status).toBe(404); expect(await (await app.request(primaryUrl("signage"))).text()).toBe(primary); expect(await (await app.request(displayUrl("signage"))).text()).toBe(display); expect((await app.request(primaryUrl("backend"))).status).toBe(404); expect((await app.request(displayUrl("backend"))).status).toBe(404); }); it("refuses wrong versions with the same sanitized response as an undeclared display", async () => { const { app } = await install([ { display: `${artifactSource("announcements", "1.4.2")}// display\n`, id: "announcements", version: "1.4.2", }, { id: "calendar", ui: artifactSource("calendar", "0.1.0") }, ]); const responses = await Promise.all( [ moduleDisplayUiArtifactUrl({ moduleId: "announcements", version: "latest" }), moduleDisplayUiArtifactUrl({ moduleId: "calendar", version: "0.1.0" }), moduleDisplayUiArtifactUrl({ moduleId: "missing", version: "1.4.2" }), ].map(async (url) => { const response = await app.request(url); return { body: await response.text(), status: response.status }; }), ); expect(responses).toEqual([ { body: '{"code":"MODULE_UI_NOT_PUBLISHED"}', status: 404 }, { body: '{"code":"MODULE_UI_NOT_PUBLISHED"}', status: 404 }, { body: '{"code":"MODULE_UI_NOT_PUBLISHED"}', status: 404 }, ]); }); it("never turns the display URL into a package directory or generic role selector", async () => { const display = `${artifactSource("announcements", "0.1.0")}// display\n`; const { app } = await install([ { display, extraFiles: { ".env": "DISPLAY_SECRET=do-not-serve\n", "dist/display/index.js.map": "display source map secret", "dist/display/internal-chunk.js": "export const internal = 'secret';\n", }, id: "announcements", }, ]); const exact = moduleDisplayUiArtifactUrl({ moduleId: "announcements", version: "0.1.0" }); for (const url of [ `${exact}.map`, `${exact}/index.js.map`, `${exact}/internal-chunk.js`, `${exact}/../../.env`, `${MODULE_UI_ASSET_PREFIX}/announcements/0.1.0/dashboard`, `${MODULE_UI_ASSET_PREFIX}/announcements/0.1.0/arbitrary-role`, ]) { const response = await app.request(url); expect(response.status, url).toBe(404); const body = await response.text(); expect(body, url).not.toContain("secret"); expect(body, url).not.toContain("mount"); } }); it("cannot publish a blocked module omitted from Core's eligible registry input", async () => { const display = `${artifactSource("announcements", "0.1.0")}// display\n`; const { app } = await install( [ { display, id: "announcements" }, { display: `${display}// signage\n`, id: "signage" }, ], ["signage"], ); expect( ( await app.request( moduleDisplayUiArtifactUrl({ moduleId: "announcements", version: "0.1.0" }), ) ).status, ).toBe(404); expect( (await app.request(moduleDisplayUiArtifactUrl({ moduleId: "signage", version: "0.1.0" }))) .status, ).toBe(200); }); it("reports the same fixed 503 when the validated display artifact disappears", async () => { const { app, modulesDirectory } = await install([ { display: `${artifactSource("announcements", "0.1.0")}// display\n`, id: "announcements", }, ]); await rm(path.join(modulesDirectory, "announcements/dist/display/index.js")); const response = await app.request( moduleDisplayUiArtifactUrl({ moduleId: "announcements", version: "0.1.0" }), ); expect(response.status).toBe(503); expect(await response.json()).toEqual({ code: "MODULE_UI_ARTIFACT_UNAVAILABLE" }); expect(response.headers.get("cache-control")).toBe("no-store"); }); }); describe("module UI artifact publication — module identity", () => { async function expectNotPublished(app: Hono, url: string): Promise { const response = await app.request(url); expect(response.status).toBe(404); expect(await response.json()).toEqual({ code: "MODULE_UI_NOT_PUBLISHED" }); } it("refuses a module that is not installed", async () => { const { app } = await install([ { id: "announcements", ui: artifactSource("announcements", "0.1.0") }, ]); await expectNotPublished(app, moduleUiArtifactUrl({ moduleId: "calendar", version: "0.1.0" })); }); it("refuses an installed module that declares no UI entrypoint", async () => { const { app } = await install([{ id: "calendar" }]); await expectNotPublished(app, moduleUiArtifactUrl({ moduleId: "calendar", version: "0.1.0" })); }); it("refuses a version other than the installed one rather than serving what exists", async () => { const { app } = await install([ { id: "announcements", ui: artifactSource("announcements", "1.4.2"), version: "1.4.2" }, ]); for (const version of ["1.4.1", "1.4.3", "1.4", "latest", "^1.4.2"]) { await expectNotPublished(app, moduleUiArtifactUrl({ moduleId: "announcements", version })); } expect( (await app.request(moduleUiArtifactUrl({ moduleId: "announcements", version: "1.4.2" }))) .status, ).toBe(200); }); it("makes unknown module, no UI, and wrong version indistinguishable", async () => { const { app } = await install([ { id: "announcements", ui: artifactSource("announcements", "1.4.2"), version: "1.4.2" }, { id: "calendar" }, ]); const responses = await Promise.all( [ moduleUiArtifactUrl({ moduleId: "signage", version: "1.4.2" }), moduleUiArtifactUrl({ moduleId: "calendar", version: "0.1.0" }), moduleUiArtifactUrl({ moduleId: "announcements", version: "9.9.9" }), ].map(async (url) => { const response = await app.request(url); return { body: await response.text(), status: response.status }; }), ); // An anonymous caller must not be able to use the distinction to enumerate an installation. expect(new Set(responses.map((entry) => `${entry.status}:${entry.body}`)).size).toBe(1); }); it("never serves one module's artifact under another module's identity", async () => { const { app } = await install([ { id: "announcements", ui: artifactSource("announcements", "0.1.0") }, { id: "signage", ui: artifactSource("signage", "0.1.0") }, ]); const announcements = await app.request( moduleUiArtifactUrl({ moduleId: "announcements", version: "0.1.0" }), ); const signage = await app.request( moduleUiArtifactUrl({ moduleId: "signage", version: "0.1.0" }), ); expect(await announcements.text()).toContain('"announcements"'); expect(await signage.text()).toContain('"signage"'); }); }); describe("module UI artifact publication — filesystem exposure", () => { const sourceMap = '{"version":3,"sources":["../../src/ui/index.tsx"],"sourcesContent":["secret"]}'; /** One installation carrying every kind of file a wildcard route would have leaked. */ async function installWithNeighbours(): Promise { return install([ { extraFiles: { ".env": "EKKLESIA_IDENTITY_SECRET=super-secret\n", "dist/server/index.js.map": sourceMap, "dist/ui/index.js.map": sourceMap, "dist/ui/internal-chunk.js": "export const internal = 1;\n", }, id: "announcements", ui: artifactSource("announcements", "0.1.0"), }, ]); } it("exposes no path shape beyond the two identity segments", async () => { const { app } = await installWithNeighbours(); const base = `${MODULE_UI_ASSET_PREFIX}/announcements`; const unreachable = [ // Source maps: real files, sitting right next to the published artifact. `${base}/0.1.0.map`, `${base}/0.1.0/index.js.map`, `${base}/0.1.0/dist/ui/index.js.map`, // Sibling bundle output and package-root files. `${base}/0.1.0/internal-chunk.js`, `${base}/0.1.0/package.json`, `${base}/0.1.0/ekklesia.module.json`, `${base}/0.1.0/.env`, // A trailing segment of any kind, including an empty one. `${base}/0.1.0/`, `${base}/0.1.0/anything`, // Fewer segments than the route declares. The collection root itself is deliberately absent // from this list: it is the authenticated inventory (ADR 0026) rather than a 404, and is // asserted separately below. base, ]; for (const url of unreachable) { const response = await app.request(url); expect(response.status, url).toBe(404); // Nothing leaks: not the artifact, not a neighbour, not a path. const body = await response.text(); expect(body, url).not.toContain("secret"); expect(body, url).not.toContain("internal"); expect(body, url).not.toContain("mount"); } }); it("serves no artifact bytes from the collection root, which is the authenticated inventory", async () => { const { app } = await installWithNeighbours(); // `/api/v1/module-ui` lists what a host may mount (ADR 0026) and requires // `core.modules.ui.read`; this installation's caller is anonymous, so it is refused. Either way // it never yields a byte of any artifact or neighbouring file. const response = await app.request(MODULE_UI_ASSET_PREFIX); const body = await response.text(); expect(response.status).toBe(403); expect(body).not.toContain("mount"); expect(body).not.toContain("secret"); expect(body).not.toContain("internal"); }); it("treats a traversal attempt as an ordinary failed lookup", async () => { const { app } = await installWithNeighbours(); const attempts = [ `${MODULE_UI_ASSET_PREFIX}/..%2F..%2F..%2Fetc%2Fpasswd/0.1.0`, `${MODULE_UI_ASSET_PREFIX}/announcements/..%2F..%2Fpackage.json`, `${MODULE_UI_ASSET_PREFIX}/announcements%2Fdist%2Fui%2Findex.js/0.1.0`, `${MODULE_UI_ASSET_PREFIX}/${encodeURIComponent("../announcements")}/0.1.0`, `${MODULE_UI_ASSET_PREFIX}/announcements/${encodeURIComponent("0.1.0/../../.env")}`, `${MODULE_UI_ASSET_PREFIX}/announcements/${encodeURIComponent("0.1.0 .map")}`, ]; for (const url of attempts) { const response = await app.request(url); expect(response.status, url).toBe(404); expect(await response.text(), url).not.toContain("secret"); } }); it("keeps an absolute filesystem path from ever becoming a request parameter that works", async () => { const { app, modules } = await install([ { id: "announcements", ui: artifactSource("announcements", "0.1.0") }, ]); const artifactPath = (modules[0] as DiscoveredModule).entrypoints.ui as string; const response = await app.request( `${MODULE_UI_ASSET_PREFIX}/${encodeURIComponent(artifactPath)}/0.1.0`, ); expect(response.status).toBe(404); }); }); describe("module UI artifact publication — request surface", () => { it("is read-only: no other method is routed at all", async () => { const { app } = await install([ { id: "announcements", ui: artifactSource("announcements", "0.1.0") }, ]); const url = moduleUiArtifactUrl({ moduleId: "announcements", version: "0.1.0" }); for (const method of ["POST", "PUT", "PATCH", "DELETE"]) { const response = await app.request(url, { method }); expect(response.status, method).toBe(404); expect(await response.json(), method).toEqual({ code: "NOT_FOUND" }); } }); it("answers HEAD with the full header contract and no body", async () => { const { app } = await install([ { id: "announcements", ui: artifactSource("announcements", "0.1.0") }, ]); const url = moduleUiArtifactUrl({ moduleId: "announcements", version: "0.1.0" }); const response = await app.request(url, { method: "HEAD" }); expect(response.status).toBe(200); expect(response.headers.get("content-type")).toBe(MODULE_UI_CONTENT_TYPE); expect(response.headers.get("etag")).toBe((await app.request(url)).headers.get("etag")); expect(await response.text()).toBe(""); }); it("fails closed on malformed percent-encoding instead of throwing", async () => { const { app } = await install([ { id: "announcements", ui: artifactSource("announcements", "0.1.0") }, ]); for (const url of [ `${MODULE_UI_ASSET_PREFIX}/%zz/0.1.0`, `${MODULE_UI_ASSET_PREFIX}/announcements/%E0%A4%A`, `${MODULE_UI_ASSET_PREFIX}/a%00b/0.1.0`, ]) { const response = await app.request(url); // An ordinary refused lookup, never a 500 that would reveal a decoding failure. expect(response.status, url).toBe(404); expect(await response.json(), url).toEqual({ code: "MODULE_UI_NOT_PUBLISHED" }); } }); }); describe("module UI artifact publication — artifact changed after discovery", () => { it("reports a sanitized 503 when the validated artifact has been deleted", async () => { const { app, modulesDirectory } = await install([ { id: "announcements", ui: artifactSource("announcements", "0.1.0") }, ]); const url = moduleUiArtifactUrl({ moduleId: "announcements", version: "0.1.0" }); expect((await app.request(url)).status).toBe(200); await rm(path.join(modulesDirectory, "announcements/dist/ui/index.js")); const response = await app.request(url); expect(response.status).toBe(503); expect(await response.json()).toEqual({ code: "MODULE_UI_ARTIFACT_UNAVAILABLE" }); }); it("leaks no filesystem detail when the artifact is unreadable", async () => { const { app, modulesDirectory } = await install([ { id: "announcements", ui: artifactSource("announcements", "0.1.0") }, ]); const artifact = path.join(modulesDirectory, "announcements/dist/ui/index.js"); await rm(artifact); // Replaced by a directory: a different `errno` (EISDIR) on the same validated path. await mkdir(artifact); const response = await app.request( moduleUiArtifactUrl({ moduleId: "announcements", version: "0.1.0" }), ); const body = await response.text(); expect(response.status).toBe(503); expect(body).not.toContain(modulesDirectory); expect(body).not.toContain(tmpdir()); expect(body).not.toContain("dist"); expect(body).not.toContain("EISDIR"); expect(response.headers.get("cache-control")).toBe("no-store"); }); it("never falls back to a reconstructed path when the exact artifact is gone", async () => { const { app, modulesDirectory } = await install([ { id: "announcements", ui: artifactSource("announcements", "0.1.0") }, ]); const moduleDirectory = path.join(modulesDirectory, "announcements"); await rm(path.join(moduleDirectory, "dist/ui/index.js")); // Plausible neighbours a path-guessing implementation might land on instead. await writeFile(path.join(moduleDirectory, "dist/ui/index.mjs"), "export default {};\n"); await writeFile(path.join(moduleDirectory, "dist/index.js"), "export default {};\n"); const response = await app.request( moduleUiArtifactUrl({ moduleId: "announcements", version: "0.1.0" }), ); expect(response.status).toBe(503); }); }); describe("module UI artifact URL generation", () => { it("agrees with the route it is generated for", async () => { const { app } = await install([ { id: "announcements", ui: artifactSource("announcements", "1.4.2"), version: "1.4.2" }, ]); const url = moduleUiArtifactUrl({ moduleId: "announcements", version: "1.4.2" }); expect(url).toBe(`${MODULE_UI_ASSET_PREFIX}/announcements/1.4.2`); expect((await app.request(url)).status).toBe(200); }); it("is root-relative, so a host resolves it against whichever API origin it is configured for", () => { const url = moduleUiArtifactUrl({ moduleId: "announcements", version: "1.4.2" }); expect(url.startsWith("/")).toBe(true); expect(new URL(url, "https://ekklesia.example.test").href).toBe( `https://ekklesia.example.test${MODULE_UI_ASSET_PREFIX}/announcements/1.4.2`, ); // A split-origin host resolves the identical value against its own API base. expect(new URL(url, "http://127.0.0.1:3000").href).toBe( `http://127.0.0.1:3000${MODULE_UI_ASSET_PREFIX}/announcements/1.4.2`, ); }); it("encodes hostile input instead of letting it restructure the URL", () => { const hostile = [ "../../etc/passwd", "announcements/dist/ui/index.js", "announcements?x=1", "announcements#fragment", "announcements ", "a b", ]; for (const value of hostile) { // Still exactly the prefix's own segments plus one for the module and one for the version. const expectedSegments = MODULE_UI_ASSET_PREFIX.split("/").length + 2; const moduleUrl = moduleUiArtifactUrl({ moduleId: value, version: "0.1.0" }); expect(moduleUrl.split("/"), value).toHaveLength(expectedSegments); expect(moduleUrl.includes("?"), value).toBe(false); expect(moduleUrl.includes("#"), value).toBe(false); const versionUrl = moduleUiArtifactUrl({ moduleId: "announcements", version: value }); expect(versionUrl.split("/"), value).toHaveLength(expectedSegments); expect(versionUrl.includes("?"), value).toBe(false); expect(versionUrl.includes("#"), value).toBe(false); } }); }); interface HostContainer { readonly children: { readonly length: number }; querySelector(selectors: string): HostContainer | null; readonly shadowRoot: { querySelector(selectors: string): { textContent: string } | null } | null; } describe("module UI artifact publication — the generic loader consumes what is published", () => { /** * Node cannot `import()` an `http:` URL, so the two halves of the chain are proven separately and * honestly: the tests above prove the route publishes the exact artifact bytes with ES module * JavaScript semantics over real HTTP routing, and this one takes **those very bytes, as returned * by the route**, and puts them through the real `@ekklesia/module-ui-host` loader as a `data:` * URL — a genuine module URL, the same technique ADR 0024's own loader tests use. Nothing here * reads the artifact from the filesystem. */ async function publishedModuleUrl(app: Hono, moduleId: string, version: string): Promise { const response = await app.request(moduleUiArtifactUrl({ moduleId, version })); expect(response.status).toBe(200); expect(response.headers.get("content-type")).toBe(MODULE_UI_CONTENT_TYPE); const bytes = Buffer.from(await response.arrayBuffer()); return `data:text/javascript;base64,${bytes.toString("base64")}`; } /** * A real host-owned container, because since ADR 0040 the loader builds an isolated Shadow DOM * root inside whatever it is given. The container's type is read back off `loadModuleUi` itself * rather than written as `Element`, because `apps/api` compiles against Node libs only — naming a * DOM type here would be the same browser/server confusion this route exists to keep apart. */ type LoaderContainer = Parameters[0]["container"]; /** * `happy-dom` used as a library rather than as this suite's environment: the loader now builds a * real isolated root inside the container, and installing a DOM environment here would replace * the module runner's resolution for a server-side suite that dynamically imports real module * entrypoints. */ function containerSink(): HostContainer { return new Window().document.createElement("div") as unknown as HostContainer; } /** What the mounted artifact rendered, read through its open isolated root. */ function mountedText(container: HostContainer): string { const shadowHost = container.querySelector("[data-ekklesia-module-ui]"); return ( shadowHost?.shadowRoot?.querySelector("[data-ekklesia-module-ui-root]")?.textContent ?? "" ); } function hostContext() { return { api: { request: async () => new Response(null, { status: 200 }) }, contributions: NO_MODULE_UI_CONTRIBUTIONS, moduleId: "announcements", permissions: { has: () => false }, principal: { kind: "anonymous" as const }, realtime: { subscribe: () => ({ close: () => undefined }) }, }; } it("mounts and unmounts an artifact obtained from the publication route", async () => { const { app } = await install([ { id: "announcements", ui: artifactSource("announcements", "1.4.2"), version: "1.4.2" }, ]); const url = await publishedModuleUrl(app, "announcements", "1.4.2"); const container = containerSink(); const mounted = await loadModuleUi({ container: container as unknown as LoaderContainer, context: hostContext(), source: { moduleId: "announcements", url, version: "1.4.2" }, }); expect(mountedText(container)).toBe("announcements:mounted"); await mounted.unmount(); expect(container.children).toHaveLength(0); }); it("still rejects the published artifact when the host expects a different identity", async () => { const { app } = await install([ { id: "announcements", ui: artifactSource("announcements", "1.4.2"), version: "1.4.2" }, ]); const url = await publishedModuleUrl(app, "announcements", "1.4.2"); await expect( loadModuleUi({ container: containerSink() as unknown as LoaderContainer, context: hostContext(), source: { moduleId: "announcements", url, version: "9.9.9" }, }), ).rejects.toMatchObject({ code: "MODULE_UI_IDENTITY_MISMATCH" }); }); }); /** * Contribution artifact publication (ADR 0041), on exactly the same terms as every other declared * artifact: three lookup keys, one exact registry match, one already-validated canonical path. */ describe("module contribution artifact publication", () => { const signage = { capabilities: [{ id: "signage.slide-renderer", version: "1.2.0" }], id: "signage", }; const contributionSource = "export default { contributed: true };\n"; const announcements = (capabilityRange = "^1.0.0") => ({ contributions: [ { capability: "signage.slide-renderer", capabilityRange, localName: "signage-slide", source: contributionSource, }, ], id: "announcements", }); const contributionUrl = moduleContributionArtifactUrl( { moduleId: "announcements", version: "0.1.0" }, "announcements.signage-slide", ); it("serves the declared contribution artifact byte for byte", async () => { const { app } = await install([signage, announcements()]); const response = await app.request(contributionUrl); expect(response.status).toBe(200); expect(response.headers.get("content-type")).toBe(MODULE_UI_CONTENT_TYPE); expect(response.headers.get("cache-control")).toBe(MODULE_UI_CACHE_CONTROL); expect(response.headers.get("x-content-type-options")).toBe("nosniff"); expect(await response.text()).toBe(contributionSource); }); it("builds a URL that cannot select a neighbouring file", () => { expect(contributionUrl).toBe( "/api/v1/module-ui/announcements/0.1.0/contributions/announcements.signage-slide", ); // The contribution id is one segment's worth of text, always — a value that needed encoding // simply fails to resolve. expect( moduleContributionArtifactUrl( { moduleId: "announcements", version: "0.1.0" }, "../../../etc/passwd", ), ).toBe("/api/v1/module-ui/announcements/0.1.0/contributions/..%2F..%2F..%2Fetc%2Fpasswd"); }); it.each([ ["an unknown contribution id", "announcements.nope"], ["another module's contribution id", "signage.slide-renderer"], ["a path traversal", "../../../etc/passwd"], ["the artifact's source map", "announcements.signage-slide.map"], ])("refuses %s with the same indistinguishable 404", async (_label, contributionId) => { const { app } = await install([signage, announcements()]); const response = await app.request( moduleContributionArtifactUrl( { moduleId: "announcements", version: "0.1.0" }, contributionId, ), ); expect(response.status).toBe(404); expect(await response.json()).toEqual({ code: "MODULE_UI_NOT_PUBLISHED" }); }); it("refuses a version the platform does not have installed", async () => { const { app } = await install([signage, announcements()]); const response = await app.request( moduleContributionArtifactUrl( { moduleId: "announcements", version: "9.9.9" }, "announcements.signage-slide", ), ); expect(response.status).toBe(404); }); it("publishes nothing when no installed module provides the target capability", async () => { const { app } = await install([announcements()]); expect((await app.request(contributionUrl)).status).toBe(404); }); it("publishes nothing when the provider is outside the declared compatible range", async () => { const { app } = await install([signage, announcements("^2.0.0")]); expect((await app.request(contributionUrl)).status).toBe(404); }); it("keeps the sibling source map and the module's own directory unreachable", async () => { const { app } = await install([signage, announcements()]); for (const suffix of [".map", "/index.js.map", "/../ui/index.js"]) { const response = await app.request(`${contributionUrl}${suffix}`); expect(response.status).toBe(404); } }); });