feat: add display kiosk host
CI / verify (pull_request) Successful in 38s
CI / infrastructure-smoke (pull_request) Successful in 20s
CI / migration-smoke (pull_request) Successful in 59s

This commit is contained in:
ekklesia
2026-08-23 20:50:56 -05:00
parent be5ed7853d
commit 2e0fe059c3
80 changed files with 4868 additions and 397 deletions
+80 -12
View File
@@ -5,6 +5,7 @@ import {
isVersionCompatible,
type ModuleCapabilityDeclaration,
type ModuleManifest,
type PermissionId,
safeParseModuleManifest,
semanticVersionSchema,
} from "@ekklesia/contracts";
@@ -61,10 +62,16 @@ export interface ResolvedModuleEntrypoints {
readonly schema?: string;
readonly migrations?: string;
readonly jobs?: string;
/** A module's declared presentation artifact (ADR 0021), resolved and validated exactly like the
* other entrypoints. Discovery only proves the file exists, is compiled ESM, and stays inside the
* module package — nothing in this repository imports, evaluates, mounts, or renders it yet. */
/** A module's primary presentation artifact (ADR 0021), resolved and validated exactly like the
* other executable entrypoints. Discovery never imports or evaluates browser artifacts. */
readonly ui?: string;
/** The independently mountable full-viewport artifact and its statically declared access policy.
* `artifactPath` is canonical and package-contained; `permission` already matched one of this
* module's own manifest declarations during manifest parsing. */
readonly display?: {
readonly artifactPath: string;
readonly permission: PermissionId;
};
}
// Branded primitives (`PermissionId`, `ModuleId`, ...) are `string & {brand}` intersection types,
@@ -258,11 +265,49 @@ async function resolveEntrypoints(
): Promise<ResolvedModuleEntrypoints | undefined> {
const resolved: Partial<Record<keyof ModuleManifest["entrypoints"], string>> = {};
const canonicalModuleDirectory = await realpath(moduleDirectory);
const { display, ...ordinaryEntrypoints } = manifest.entrypoints;
const ordinaryConfiguredEntrypoints = Object.entries(ordinaryEntrypoints)
.filter((entry): entry is [string, string] => entry[1] !== undefined)
.map(([entrypointName, relativeEntrypoint]) => ({
entrypointName,
issuePath: `entrypoints.${entrypointName}`,
relativeEntrypoint,
}));
const configuredEntrypoints = [
...ordinaryConfiguredEntrypoints,
...(display === undefined
? []
: [
{
entrypointName: "display",
issuePath: "entrypoints.display.artifact",
relativeEntrypoint: display.artifact,
},
]),
];
let entrypointCollision = false;
for (const [entrypointName, relativeEntrypoint] of Object.entries(manifest.entrypoints)) {
if (relativeEntrypoint === undefined) {
continue;
if (display !== undefined) {
const configuredDisplayPath = path.resolve(moduleDirectory, display.artifact);
for (const ordinary of ordinaryConfiguredEntrypoints) {
if (path.resolve(moduleDirectory, ordinary.relativeEntrypoint) === configuredDisplayPath) {
entrypointCollision = true;
issues.push({
code: "MODULE_ENTRYPOINT_INVALID",
message: `display must not resolve to the same configured file as ${ordinary.entrypointName}.`,
moduleDirectory: directoryName,
moduleId: manifest.id,
path: "entrypoints.display.artifact",
});
}
}
}
for (const {
entrypointName,
issuePath: entrypointIssuePath,
relativeEntrypoint,
} of configuredEntrypoints) {
if (
isCompiledEntrypoint(entrypointName) &&
![".js", ".mjs"].includes(path.extname(relativeEntrypoint).toLowerCase())
@@ -272,7 +317,7 @@ async function resolveEntrypoints(
message: `${entrypointName} must reference compiled ESM JavaScript with a .js or .mjs extension.`,
moduleDirectory: directoryName,
moduleId: manifest.id,
path: `entrypoints.${entrypointName}`,
path: entrypointIssuePath,
});
continue;
}
@@ -283,7 +328,7 @@ async function resolveEntrypoints(
message: `${entrypointName} resolves outside its module directory.`,
moduleDirectory: directoryName,
moduleId: manifest.id,
path: `entrypoints.${entrypointName}`,
path: entrypointIssuePath,
});
continue;
}
@@ -296,7 +341,7 @@ async function resolveEntrypoints(
message: `${entrypointName} resolves through a link outside its module directory.`,
moduleDirectory: directoryName,
moduleId: manifest.id,
path: `entrypoints.${entrypointName}`,
path: entrypointIssuePath,
});
continue;
}
@@ -312,7 +357,7 @@ async function resolveEntrypoints(
: `${entrypointName} must resolve to a file.`,
moduleDirectory: directoryName,
moduleId: manifest.id,
path: `entrypoints.${entrypointName}`,
path: entrypointIssuePath,
});
continue;
}
@@ -329,12 +374,27 @@ async function resolveEntrypoints(
}.`,
moduleDirectory: directoryName,
moduleId: manifest.id,
path: `entrypoints.${entrypointName}`,
path: entrypointIssuePath,
});
}
}
if (!resolved.server) {
if (resolved.display !== undefined) {
for (const [entrypointName, canonicalEntrypoint] of Object.entries(resolved)) {
if (entrypointName !== "display" && canonicalEntrypoint === resolved.display) {
entrypointCollision = true;
issues.push({
code: "MODULE_ENTRYPOINT_INVALID",
message: `display must not resolve through a link to the same file as ${entrypointName}.`,
moduleDirectory: directoryName,
moduleId: manifest.id,
path: "entrypoints.display.artifact",
});
}
}
}
if (!resolved.server || entrypointCollision) {
return undefined;
}
@@ -344,6 +404,14 @@ async function resolveEntrypoints(
...(resolved.migrations ? { migrations: resolved.migrations } : {}),
...(resolved.jobs ? { jobs: resolved.jobs } : {}),
...(resolved.ui ? { ui: resolved.ui } : {}),
...(resolved.display && display
? {
display: {
artifactPath: resolved.display,
permission: display.permission,
},
}
: {}),
};
}