feat: add display kiosk host
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import type { PermissionDeclaration } from "@ekklesia/contracts";
|
||||
import type { PermissionDeclaration, PermissionId } from "@ekklesia/contracts";
|
||||
import type { DiscoveredModule } from "./discovery.js";
|
||||
|
||||
/**
|
||||
@@ -40,6 +40,26 @@ export interface PublishableModuleUi {
|
||||
readonly permissions: readonly PermissionDeclaration[];
|
||||
}
|
||||
|
||||
/**
|
||||
* One installed module whose dedicated full-viewport display artifact may be published.
|
||||
*
|
||||
* This is deliberately a separate record from {@link PublishableModuleUi}. A primary UI and a
|
||||
* display UI are two independently declared artifacts with different host roles, and a module may
|
||||
* publish either one without publishing the other. `displayPermission` is the module-owned
|
||||
* permission the manifest places beside `entrypoints.display`; it travels with the exact artifact
|
||||
* identity so a display descriptor cannot accidentally authorize one artifact with another
|
||||
* module's policy. The complete declaration list supports the same browser-safe permission hints
|
||||
* as the primary UI while conferring no authority by itself.
|
||||
*/
|
||||
export interface PublishableModuleDisplay {
|
||||
readonly moduleId: string;
|
||||
readonly version: string;
|
||||
readonly name: string;
|
||||
readonly artifactPath: string;
|
||||
readonly displayPermission: PermissionId;
|
||||
readonly permissions: readonly PermissionDeclaration[];
|
||||
}
|
||||
|
||||
/**
|
||||
* The platform's answer to "may this module's UI artifact be served, and if so, which exact file?"
|
||||
*
|
||||
@@ -60,6 +80,15 @@ export interface ModuleUiPublicationRegistry {
|
||||
* version are all indistinguishable here — each returns `undefined`.
|
||||
*/
|
||||
resolve(moduleId: unknown, version: unknown): PublishableModuleUi | undefined;
|
||||
/** Every dedicated display artifact, in discovery order. Stable and frozen. */
|
||||
listDisplays(): readonly PublishableModuleDisplay[];
|
||||
/**
|
||||
* The dedicated display record for an exact module id and installed version, or `undefined`.
|
||||
*
|
||||
* This lookup never falls back to `entrypoints.ui`: a primary/dashboard artifact cannot become
|
||||
* a display merely because a caller appends `/display` to its publication URL.
|
||||
*/
|
||||
resolveDisplay(moduleId: unknown, version: unknown): PublishableModuleDisplay | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -70,8 +99,9 @@ export interface ModuleUiPublicationRegistry {
|
||||
* 1. the module is in the **eligible** set the platform started — it survived discovery *and*
|
||||
* the operational lifecycle permits it to run (`resolveModuleEligibility`, `./lifecycle.js`) —
|
||||
* and
|
||||
* 2. its manifest declares `entrypoints.ui`, which discovery resolved to a real compiled file
|
||||
* inside the module package.
|
||||
* 2. for each role independently, its manifest declares `entrypoints.ui` or
|
||||
* `entrypoints.display`, which discovery resolved to a real compiled file inside the module
|
||||
* package.
|
||||
*
|
||||
* A module directory sitting on disk satisfies neither on its own — it has to have survived
|
||||
* discovery — which is the property `docs/development/security.md` requires: *the existence of an
|
||||
@@ -93,30 +123,47 @@ export function createModuleUiPublicationRegistry(
|
||||
modules: readonly DiscoveredModule[],
|
||||
): ModuleUiPublicationRegistry {
|
||||
const byModuleId = new Map<string, PublishableModuleUi>();
|
||||
const displaysByModuleId = new Map<string, PublishableModuleDisplay>();
|
||||
|
||||
for (const module of modules) {
|
||||
const artifactPath = module.entrypoints.ui;
|
||||
if (artifactPath === undefined) {
|
||||
continue;
|
||||
if (artifactPath !== undefined) {
|
||||
byModuleId.set(
|
||||
module.manifest.id,
|
||||
Object.freeze({
|
||||
artifactPath,
|
||||
moduleId: module.manifest.id,
|
||||
name: module.manifest.name,
|
||||
// Already deeply frozen by discovery; copied into a fresh frozen array so a caller holding
|
||||
// this record can never reach the manifest object graph itself.
|
||||
permissions: Object.freeze([...module.manifest.permissions]),
|
||||
version: module.manifest.version,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const display = module.entrypoints.display;
|
||||
if (display !== undefined) {
|
||||
displaysByModuleId.set(
|
||||
module.manifest.id,
|
||||
Object.freeze({
|
||||
artifactPath: display.artifactPath,
|
||||
displayPermission: display.permission,
|
||||
moduleId: module.manifest.id,
|
||||
name: module.manifest.name,
|
||||
permissions: Object.freeze([...module.manifest.permissions]),
|
||||
version: module.manifest.version,
|
||||
}),
|
||||
);
|
||||
}
|
||||
byModuleId.set(
|
||||
module.manifest.id,
|
||||
Object.freeze({
|
||||
artifactPath,
|
||||
moduleId: module.manifest.id,
|
||||
name: module.manifest.name,
|
||||
// Already deeply frozen by discovery; copied into a fresh frozen array so a caller holding
|
||||
// this record can never reach the manifest object graph itself.
|
||||
permissions: Object.freeze([...module.manifest.permissions]),
|
||||
version: module.manifest.version,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const published = Object.freeze([...byModuleId.values()]);
|
||||
const publishedDisplays = Object.freeze([...displaysByModuleId.values()]);
|
||||
|
||||
return Object.freeze({
|
||||
list: (): readonly PublishableModuleUi[] => published,
|
||||
listDisplays: (): readonly PublishableModuleDisplay[] => publishedDisplays,
|
||||
resolve: (moduleId: unknown, version: unknown): PublishableModuleUi | undefined => {
|
||||
if (typeof moduleId !== "string" || typeof version !== "string") {
|
||||
return undefined;
|
||||
@@ -127,5 +174,12 @@ export function createModuleUiPublicationRegistry(
|
||||
// than to a different artifact that happens to exist.
|
||||
return candidate?.version === version ? candidate : undefined;
|
||||
},
|
||||
resolveDisplay: (moduleId: unknown, version: unknown): PublishableModuleDisplay | undefined => {
|
||||
if (typeof moduleId !== "string" || typeof version !== "string") {
|
||||
return undefined;
|
||||
}
|
||||
const candidate = displaysByModuleId.get(moduleId);
|
||||
return candidate?.version === version ? candidate : undefined;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user