Files
Ekklesia/surfaces/shell/tests/modularity.test.ts
T
ekklesiaandClaude Opus 5 80189c8ae2
CI / verify (pull_request) Successful in 40s
CI / infrastructure-smoke (pull_request) Successful in 21s
CI / migration-smoke (pull_request) Successful in 1m22s
feat: give display devices their own paired identity
ADR 0033 gave Ekklesia a display host and ADR 0034 gave it a login page.
Together, the only way to light a television in a foyer was to sign a
human being into it and leave that session running on a screen anyone can
walk up to: a person's credentials on a wall, a person's session as the
thing signage depends on, and the display going dark when they are
revoked or their session simply expires.

A screen is not a person. Ekklesia now recognizes three principal classes
— anonymous, authenticated human, and managed device — and a device is a
first-class principal with its own credential and its own explicit
permissions. It has no email, no password, and no row in the identity
tables, and it satisfies the `authenticated` baseline never, so every
route that settles for "somebody is signed in" stays closed to it.

Device permissions live in their own table because
ekklesia_core_grant.user_id is a non-null foreign key into the user table
and making it polymorphic would rewrite the platform's central
authorization record to serve one narrow case. That is a schema decision,
not a second decision point: the evaluator gained one branch, so a device
is allowed or denied by the same function, with the same deny-by-default,
as anyone else — with no owner path it could reach and no baseline that
stands in for a grant.

The six-digit code is not the credential. It is printed across a screen in
a public hallway, so it is treated as public: it identifies a request to
an administrator and is worth nothing alone. Collecting the credential
also requires a 256-bit claim secret held only in the requesting browser's
HttpOnly cookie, and approving requires the restricted core.devices.manage
permission. Without that split, reading a code off a wall would hand over
the screen's identity.

The permission a pairing asks for is read from the module's validated
display declaration, never from the request, so a browser cannot ask to be
granted core.devices.manage or another module's permission. Approval
grants exactly that one permission.

Credentials are stored only as SHA-256; the raw value exists in the
Set-Cookie that issues it and the device's own cookie jar, and is never
logged, returned in JSON, or placed in a URL. The cookie is HttpOnly, so
the module display code running in that same page cannot read it.
Revocation is a server-side column checked on every principal resolution,
and re-pairing mints a new identity so an old credential can never come
back. State transitions are conditional updates, so one code cannot be
approved twice or claimed into two credentials under a race.

/api/v1/principal still reports a device as anonymous: it is what the
Dashboard uses to decide whether a person is signed in. Displays read
their viewer from the descriptor instead, which already performed the
authorization — so displays are descriptor-first while the Dashboard stays
principal-first, and neither infers an identity by elimination.

Proven end to end against real PostgreSQL and real HTTP: an unpaired
screen shows a code, an owner approves it, the device opens the display
descriptor and a realtime stream, and revocation closes both while the
device still holds its cookie. An already-open SSE stream is bounded by
realtime's existing five-minute reconnect rather than closed instantly,
and that bound is documented rather than called immediate.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-23 23:41:03 -05:00

244 lines
10 KiB
TypeScript

import { readdir, readFile } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { build } from "esbuild";
import { beforeAll, describe, expect, it } from "vitest";
/**
* The proof that this shell is a *generic* host rather than an application that happens to render
* some known modules.
*
* Two levels, because they fail differently. At source level, a static import of a module would be
* an architectural mistake a reviewer should catch. At bundle level, a dynamic import that a bundler
* managed to resolve statically would be an invisible one: the code would look correct, the tests
* would pass, and installing a new module would silently do nothing until the shell was rebuilt.
*
* `happy-dom` replaces the global `URL` with the DOM implementation, which `node:fs` rejects, so
* everything here works in plain filesystem paths.
*/
const packageDirectory = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
const sourceDirectory = path.join(packageDirectory, "src");
/** The complete set of packages this shell may reach for. `@ekklesia/module-ui-host` is the generic
* loader and `@ekklesia/module-ui-sdk` is the browser-side contract; no other `@ekklesia/*` package
* — and no module — may appear. */
const ALLOWED_EKKLESIA_IMPORTS = new Set(["@ekklesia/module-ui-host", "@ekklesia/module-ui-sdk"]);
async function sourceFiles(directory: string): Promise<string[]> {
const entries = await readdir(directory, { withFileTypes: true });
const files: string[] = [];
for (const entry of entries) {
const entryPath = path.join(directory, entry.name);
if (entry.isDirectory()) {
files.push(...(await sourceFiles(entryPath)));
} else if (/\.tsx?$/u.test(entry.name)) {
files.push(entryPath);
}
}
return files.sort();
}
function importSpecifiers(source: string): string[] {
const specifiers = new Set<string>();
const patterns = [
/\b(?:import|export)\s+(?:type\s+)?[^;]*?\bfrom\s*["']([^"'\n]+)["']/gu,
/\bimport\s*["']([^"'\n]+)["']/gu,
/\bimport\s*\(\s*["']([^"'\n]+)["']\s*\)/gu,
];
for (const pattern of patterns) {
for (const match of source.matchAll(pattern)) {
if (match[1]) {
specifiers.add(match[1]);
}
}
}
return [...specifiers];
}
let files: string[];
let sources: Map<string, string>;
beforeAll(async () => {
files = await sourceFiles(sourceDirectory);
sources = new Map(
await Promise.all(files.map(async (file) => [file, await readFile(file, "utf8")] as const)),
);
});
describe("the shell is coupled to no module", () => {
it("has source files to check", () => {
expect(files.length).toBeGreaterThan(3);
});
it("imports no module, template, or workspace package beyond the two browser contracts", () => {
for (const [file, source] of sources) {
for (const specifier of importSpecifiers(source)) {
const label = `${path.relative(packageDirectory, file)} -> ${specifier}`;
expect(specifier.includes("modules/"), label).toBe(false);
expect(specifier.includes("templates/"), label).toBe(false);
// Nothing may climb out of this package at all.
expect(specifier.startsWith("../../"), label).toBe(false);
if (specifier.startsWith("@ekklesia/")) {
expect(ALLOWED_EKKLESIA_IMPORTS.has(specifier), label).toBe(true);
}
}
}
});
it("performs no dynamic import of its own: all module loading goes through the generic loader", () => {
for (const [file, source] of sources) {
// Comment lines are dropped so a doc comment may still *describe* the loader's own
// `(url) => import(url)`. The authoritative check is the bundle-level one below, which counts
// dynamic imports in the emitted output where no comment survives.
const code = source
.split("\n")
.filter((line) => !/^\s*(\*|\/\/)/u.test(line))
.join("\n");
const dynamicImports = [...code.matchAll(/\bimport\s*\(/gu)];
expect(dynamicImports, path.relative(packageDirectory, file)).toEqual([]);
}
});
});
describe("the shell builds no module knowledge into its bundle", () => {
let bundle: string;
beforeAll(async () => {
// The real production bundle configuration, produced in memory so this test never depends on a
// previous `pnpm build` having run.
const result = await build({
absWorkingDir: packageDirectory,
bundle: true,
define: {
__EKKLESIA_API_BASE_URL__: '""',
"process.env.NODE_ENV": '"production"',
},
entryPoints: [path.join(sourceDirectory, "browser", "main.tsx")],
format: "esm",
jsx: "automatic",
logLevel: "silent",
minify: false,
platform: "browser",
target: "es2022",
write: false,
});
bundle = result.outputFiles.map((file) => file.text).join("\n");
}, 60_000);
it("leaves exactly one dynamic import — the loader's — unresolved for run time", () => {
const dynamicImports = [...bundle.matchAll(/\bimport\s*\(/gu)];
// Exactly one: `@ekklesia/module-ui-host`'s `(url) => import(url)`. More would mean the shell
// acquired its own loader; fewer would mean the bundler resolved it away and installed modules
// could never be fetched at run time.
expect(dynamicImports).toHaveLength(1);
expect(bundle).toContain("(url) => import(url)");
});
it("contains no module identity and no module API prefix", () => {
// The shell learns every module id, artifact URL, and API root from the inventory at run time.
// Any of these appearing in the bundle would mean it had been baked in instead.
expect(bundle).not.toContain("/api/v1/modules");
expect(bundle).not.toContain("/api/v1/module-ui/");
expect(bundle).not.toContain("ekklesia.module.json");
expect(bundle).not.toContain("entrypoints");
});
it("spells only the platform paths it genuinely owns", () => {
// The platform's own endpoints — never a module's. Everything module-specific arrives at run
// time from the navigation the server resolved.
expect(bundle).toContain('"/api/v1/principal"');
expect(bundle).toContain('"/api/v1/dashboard/navigation"');
expect(bundle).toContain('"/api/auth/sign-in/email"');
expect(bundle).toContain('"/api/auth/sign-out"');
// The shell reads the navigation view, which already contains the inventory; it never calls
// the host-neutral inventory endpoint itself.
expect(bundle).not.toContain('"/api/v1/module-ui"');
});
it("contains only the browser HTTP auth adapter, never Better Auth or server identity internals", () => {
expect(bundle.toLowerCase()).not.toContain("better-auth");
expect(bundle.toLowerCase()).not.toContain("drizzle");
expect(bundle).not.toContain("EKKLESIA_IDENTITY_SECRET");
expect(bundle).not.toContain("bootstrapPlatformOwner");
expect(bundle).not.toContain("session_token");
});
it("bakes in no group, label, or placement", () => {
// Grouping is server-resolved configuration. A group id or label appearing in the bundle would
// mean the shell had an opinion about an arrangement it must only render.
// Deliberately distinctive terms: a generic word like "media" appears inside React's own
// bundled source and would make this assertion meaningless rather than strict.
for (const term of ["planning", "sunday-production", "livestream", "calendar", "ungrouped-"]) {
expect(bundle.toLowerCase(), term).not.toContain(term);
}
});
it("uses neither eval nor a blob URL to load module code", () => {
expect(bundle).not.toContain("new Function(");
expect(bundle).not.toMatch(/\beval\s*\(/u);
expect(bundle).not.toContain("createObjectURL");
});
});
describe("the display host is a separate generic browser bundle", () => {
let bundle: string;
beforeAll(async () => {
const result = await build({
absWorkingDir: packageDirectory,
bundle: true,
define: {
__EKKLESIA_API_BASE_URL__: '""',
"process.env.NODE_ENV": '"production"',
},
entryPoints: [path.join(sourceDirectory, "display", "main.tsx")],
format: "esm",
jsx: "automatic",
logLevel: "silent",
minify: false,
platform: "browser",
target: "es2022",
write: false,
});
bundle = result.outputFiles.map((file) => file.text).join("\n");
}, 60_000);
it("contains the generic loader's one dynamic import and no statically known module", () => {
expect([...bundle.matchAll(/\bimport\s*\(/gu)]).toHaveLength(1);
expect(bundle).toContain("(url) => import(url)");
expect(bundle).not.toContain("ekklesia.module.json");
expect(bundle).not.toContain("/api/v1/modules");
for (const moduleId of ["announcements", "calendar", "livestream", "signage"]) {
expect(bundle.toLowerCase()).not.toContain(moduleId);
}
});
it("knows only its host route resources, with no Dashboard application or transport coupling", () => {
expect(bundle).toContain('"/api/v1/module-displays"');
// Pairing is a display concern and lives here; the Dashboard bundle has its own management UI.
expect(bundle).toContain('"/api/v1/device-pairing/requests"');
expect(bundle).toContain('"/api/v1/device-pairing/claims"');
// And the display deliberately never asks the *human* principal endpoint: a paired device is
// anonymous to it, so consulting it would report every display in the building as signed out
// (ADR 0036). The descriptor reports the viewer instead.
expect(bundle).not.toContain('"/api/v1/principal"');
// A display never approves, lists, or revokes a device — that is the administrator's surface.
expect(bundle).not.toContain('"/api/v1/device-pairing/approvals"');
expect(bundle).not.toContain('"/api/v1/devices"');
expect(bundle).not.toContain('"/api/v1/dashboard/navigation"');
expect(bundle).not.toContain('"/api/auth/sign-in/email"');
expect(bundle).not.toContain('"/api/auth/sign-out"');
expect(bundle).not.toContain("Loading Ekklesia");
expect(bundle).not.toContain("Sign in to Ekklesia");
expect(bundle.toLowerCase()).not.toContain("socket.io");
expect(bundle.toLowerCase()).not.toContain("mqtt");
});
it("uses neither eval nor a blob URL to load display code", () => {
expect(bundle).not.toContain("new Function(");
expect(bundle).not.toMatch(/\beval\s*\(/u);
expect(bundle).not.toContain("createObjectURL");
});
});