Files
Ekklesia/apps/api/tests/module-ui-routes.test.ts
T
ekklesiaandClaude Opus 5.5 a12cbe9600
CI / verify (pull_request) Successful in 46s
CI / infrastructure-smoke (pull_request) Successful in 12s
CI / migration-smoke (pull_request) Successful in 1m44s
feat(api): serve the built Dashboard from the API origin
Serve surfaces/shell's built Dashboard from apps/api on the same origin
as /api (ADR 0044), exactly as the display host is served: a closed set
of files read once at startup, never a static directory, never a path
built from request input.

- The shell exports @ekklesia/shell/dashboard-assets (the exact built
  index.html and script URLs), emits the script at
  assets/ekklesia-dashboard/app.js, and index.html names that absolute
  path so dev and production serve the same URLs.
- GET / and GET /assets/ekklesia-dashboard/app.js answer from a frozen
  snapshot: no-cache with a strong ETag and 304 on match. Source maps
  are never served; every other path is the ordinary sanitized 404.
- The page sets nosniff, X-Frame-Options: DENY, Content-Security-Policy:
  frame-ancestors 'none' (that directive only), and Referrer-Policy:
  no-referrer, on 200 and 304 alike.
- A missing or empty shell build fails startup with a fixed
  DashboardHostAssetError before any module code is imported.
- The If-None-Match comparison moves to conditional-request.ts, shared
  with the display host routes.
- The emailed-link token stripping moves from browser/main.tsx into
  consumeAuthLanding (location and history injected) so it is tested;
  behavior is unchanged.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-01 17:54:55 -05:00

1101 lines
44 KiB
TypeScript

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 { testDashboardHostAssets } from "./support/dashboard-host-assets.js";
import { readyHealth } from "./support/ready-health.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<string> {
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<Record<string, string>>;
/** 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<void> {
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<Installation> {
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(
'<!doctype html><div id="display-root"></div>',
"export {};\n",
"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(),
dashboardHostAssets: testDashboardHostAssets(),
health: readyHealth(),
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<readonly [string, number]> = [
[`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<void> {
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<Installation> {
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<string> {
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<typeof loadModuleUi>[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,
display: undefined,
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);
}
});
});