feat: expose canonical module display URLs

This commit is contained in:
ekklesia
2026-09-30 18:20:40 -05:00
parent f445a83ea5
commit 3ca6b2cb32
21 changed files with 123 additions and 16 deletions
+16
View File
@@ -0,0 +1,16 @@
/**
* Canonical browser routes owned by the platform's display host.
*
* Kept separate from the route registration module so both the display host and module-UI
* inventory can publish the same path without introducing a dependency cycle.
*/
export const MODULE_DISPLAY_DESCRIPTOR_PREFIX = "/api/v1/module-displays";
export const MODULE_DISPLAY_PAGE_PREFIX = "/display";
export function moduleDisplayPageUrl(moduleId: string): string {
return `${MODULE_DISPLAY_PAGE_PREFIX}/${encodeURIComponent(moduleId)}`;
}
export function moduleDisplayDescriptorUrl(moduleId: string): string {
return `${MODULE_DISPLAY_DESCRIPTOR_PREFIX}/${encodeURIComponent(moduleId)}`;
}
+11 -10
View File
@@ -7,6 +7,13 @@ import {
requirePermission,
} from "./authorization-middleware.js";
import { createDisplayHostAssets, type DisplayHostAssets } from "./display-host-assets.js";
export {
MODULE_DISPLAY_DESCRIPTOR_PREFIX,
MODULE_DISPLAY_PAGE_PREFIX,
moduleDisplayDescriptorUrl,
moduleDisplayPageUrl,
} from "./module-display-path.js";
import { moduleDisplayDescriptorUrl, moduleDisplayPageUrl } from "./module-display-path.js";
import { moduleApiBaseUrl } from "./module-http-routes.js";
import { moduleRealtimeBaseUrl } from "./module-realtime-routes.js";
import {
@@ -18,8 +25,6 @@ import { moduleDisplayUiArtifactUrl } from "./module-ui-routes.js";
/** Host-owned namespaces. Neither can collide with module HTTP routes, all of which live under
* `/api/v1/modules/<module-id>`, and neither string enters the module-facing UI ABI. */
export const MODULE_DISPLAY_DESCRIPTOR_PREFIX = "/api/v1/module-displays";
export const MODULE_DISPLAY_PAGE_PREFIX = "/display";
export const DISPLAY_HOST_SCRIPT_PATH = "/assets/ekklesia-display/app.js";
/**
* Where the display's offline service worker is served (ADR 0043).
@@ -34,14 +39,6 @@ export const DISPLAY_HOST_SCRIPT_PATH = "/assets/ekklesia-display/app.js";
*/
export const DISPLAY_SERVICE_WORKER_PATH = "/display/sw.js";
export function moduleDisplayPageUrl(moduleId: string): string {
return `${MODULE_DISPLAY_PAGE_PREFIX}/${encodeURIComponent(moduleId)}`;
}
export function moduleDisplayDescriptorUrl(moduleId: string): string {
return `${MODULE_DISPLAY_DESCRIPTOR_PREFIX}/${encodeURIComponent(moduleId)}`;
}
/** The portable facts a browser host needs to mount this module's one display artifact. It mirrors
* the primary UI descriptor while naming the role explicitly, so a host cannot accidentally load a
* management artifact into a signage route. No outer Ekklesia route or filesystem path crosses the
@@ -50,6 +47,9 @@ export interface HostModuleDisplayDescriptor {
readonly id: string;
readonly version: string;
readonly display: { readonly url: string };
/** The public page a screen opens to mount this display. This is distinct from `display.url`,
* which is the compiled artifact imported by the platform host. */
readonly page: { readonly url: string };
readonly api: { readonly baseUrl: string };
readonly realtime: { readonly baseUrl: string };
readonly permissions: readonly string[];
@@ -157,6 +157,7 @@ async function describeDisplay(
contributions: describeModuleContributions(options.moduleUi, display.moduleId),
display: { url: moduleDisplayUiArtifactUrl(display) },
id: display.moduleId,
page: { url: moduleDisplayPageUrl(display.moduleId) },
permissions: await grantedModulePermissionIds(
options.evaluator,
principal,
+7
View File
@@ -13,6 +13,7 @@ import {
requirePermission,
} from "./authorization-middleware.js";
import { moduleApiBaseUrl } from "./module-http-routes.js";
import { moduleDisplayPageUrl } from "./module-display-path.js";
import { moduleRealtimeBaseUrl } from "./module-realtime-routes.js";
import {
MODULE_UI_ASSET_PREFIX,
@@ -48,6 +49,10 @@ export interface HostModuleUiDescriptor {
* canonical helper (`moduleUiArtifactUrl`), never re-spelled in the browser, so publication
* routing can change without touching a single line of host or module code. */
readonly ui: { readonly url: string };
/** Canonical page for this module's separately declared display artifact, or `null` when the
* module publishes no display. Server-produced so neither shell nor module spells platform
* routing or infers deployment topology. */
readonly display: { readonly url: string } | null;
/** The root this module's own HTTP API is mounted at, so the host can resolve the relative paths
* a module UI asks for (`context.api.request("records")`). Server-produced for the same reason
* `ui.url` is: `/api/v1/modules/<id>` is Ekklesia's private deployment detail, and neither the
@@ -170,9 +175,11 @@ async function describeModule(
published: PublishableModuleUi,
registry: ModuleUiPublicationRegistry,
): Promise<HostModuleUiDescriptor> {
const display = registry.resolveDisplay(published.moduleId, published.version);
return {
api: { baseUrl: moduleApiBaseUrl(published.moduleId) },
contributions: describeModuleContributions(registry, published.moduleId),
display: display === undefined ? null : { url: moduleDisplayPageUrl(published.moduleId) },
id: published.moduleId,
permissions: await grantedModulePermissionIds(evaluator, principal, published.permissions),
realtime: { baseUrl: moduleRealtimeBaseUrl(published.moduleId) },