diff --git a/apps/api/src/module-display-path.ts b/apps/api/src/module-display-path.ts new file mode 100644 index 0000000..b99b639 --- /dev/null +++ b/apps/api/src/module-display-path.ts @@ -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)}`; +} diff --git a/apps/api/src/module-display-routes.ts b/apps/api/src/module-display-routes.ts index de192c8..6a48f5c 100644 --- a/apps/api/src/module-display-routes.ts +++ b/apps/api/src/module-display-routes.ts @@ -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/`, 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, diff --git a/apps/api/src/module-ui-inventory.ts b/apps/api/src/module-ui-inventory.ts index bcb680c..43e83fa 100644 --- a/apps/api/src/module-ui-inventory.ts +++ b/apps/api/src/module-ui-inventory.ts @@ -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/` is Ekklesia's private deployment detail, and neither the @@ -170,9 +175,11 @@ async function describeModule( published: PublishableModuleUi, registry: ModuleUiPublicationRegistry, ): Promise { + 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) }, diff --git a/apps/api/tests/dashboard-routes.test.ts b/apps/api/tests/dashboard-routes.test.ts index 769cecc..1a71f98 100644 --- a/apps/api/tests/dashboard-routes.test.ts +++ b/apps/api/tests/dashboard-routes.test.ts @@ -271,6 +271,7 @@ describe("Dashboard navigation — reading", () => { expect(livestream?.module).toEqual({ contributions: [], api: { baseUrl: "/api/v1/modules/livestream" }, + display: null, id: "livestream", permissions: [], realtime: { baseUrl: "/api/v1/module-realtime/livestream" }, diff --git a/apps/api/tests/module-display-routes.test.ts b/apps/api/tests/module-display-routes.test.ts index 8ab3783..644c6a2 100644 --- a/apps/api/tests/module-display-routes.test.ts +++ b/apps/api/tests/module-display-routes.test.ts @@ -274,6 +274,7 @@ describe("module display routes — authorization and descriptor", () => { url: moduleDisplayUiArtifactUrl({ moduleId: "announcements", version: "1.4.2" }), }, id: "announcements", + page: { url: moduleDisplayPageUrl("announcements") }, permissions: [displayPermission], realtime: { baseUrl: moduleRealtimeBaseUrl("announcements") }, version: "1.4.2", @@ -351,9 +352,14 @@ describe("module display routes — role and lifecycle separation", () => { expect((await app.request(moduleDisplayPageUrl("announcements"))).status).toBe(200); expect((await app.request(MODULE_UI_INVENTORY_PATH)).status).toBe(200); expect( - ((await (await app.request(MODULE_UI_INVENTORY_PATH)).json()) as { modules: unknown[] }) - .modules, - ).toHaveLength(1); + ( + (await (await app.request(MODULE_UI_INVENTORY_PATH)).json()) as { + modules: Array<{ display: { url: string } | null }>; + } + ).modules, + ).toEqual([ + expect.objectContaining({ display: { url: moduleDisplayPageUrl("announcements") } }), + ]); }); it("has no display page, descriptor, or artifact when the module is outside the eligible set", async () => { diff --git a/apps/api/tests/module-ui-host-e2e.test.ts b/apps/api/tests/module-ui-host-e2e.test.ts index 69f7623..33c7216 100644 --- a/apps/api/tests/module-ui-host-e2e.test.ts +++ b/apps/api/tests/module-ui-host-e2e.test.ts @@ -439,6 +439,7 @@ describe("end to end: a discovered module's UI mounts in the Ekklesia host", () expect(bootstrap.descriptor).toEqual({ apiBaseUrl: "/api/v1/modules/example", contributions: [], + displayPageUrl: "/display/example", id: "example", permissions: ["example.records.read", "example.display.view"], realtimeBaseUrl: "/api/v1/module-realtime/example", @@ -454,8 +455,8 @@ describe("end to end: a discovered module's UI mounts in the Ekklesia host", () ); const artifactUrl = await publishedArtifactUrl(harness, sourceUrl); - // 4. The generic loader receives the unchanged five-member browser context and mounts the - // display artifact independently of the Dashboard artifact. + // 4. The generic loader receives the host-owned browser context, including the canonical + // display page, and mounts the display artifact independently of the Dashboard artifact. const context = createModuleUiContext({ config: harness.config, descriptor: bootstrap.descriptor, @@ -464,6 +465,7 @@ describe("end to end: a discovered module's UI mounts in the Ekklesia host", () expect(Object.keys(context).sort()).toEqual([ "api", "contributions", + "display", "moduleId", "permissions", "principal", diff --git a/apps/api/tests/module-ui-inventory.test.ts b/apps/api/tests/module-ui-inventory.test.ts index 6ab8d9d..43521d2 100644 --- a/apps/api/tests/module-ui-inventory.test.ts +++ b/apps/api/tests/module-ui-inventory.test.ts @@ -244,6 +244,7 @@ describe("module UI inventory — what it lists", () => { { contributions: [], api: { baseUrl: "/api/v1/modules/example" }, + display: null, id: "example", permissions: [], realtime: { baseUrl: "/api/v1/module-realtime/example" }, @@ -340,6 +341,7 @@ describe("module UI inventory — what it lists", () => { expect(Object.keys(modules[0] ?? {}).sort()).toEqual([ "api", "contributions", + "display", "id", "permissions", "realtime", diff --git a/apps/api/tests/module-ui-routes.test.ts b/apps/api/tests/module-ui-routes.test.ts index 67237bc..530433d 100644 --- a/apps/api/tests/module-ui-routes.test.ts +++ b/apps/api/tests/module-ui-routes.test.ts @@ -947,6 +947,7 @@ describe("module UI artifact publication — the generic loader consumes what is 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 }, diff --git a/packages/module-ui-host/tests/load-module-ui.test.ts b/packages/module-ui-host/tests/load-module-ui.test.ts index cb909d9..ef9535f 100644 --- a/packages/module-ui-host/tests/load-module-ui.test.ts +++ b/packages/module-ui-host/tests/load-module-ui.test.ts @@ -52,6 +52,7 @@ export default { function hostContext(requested: string[], moduleId = "announcements"): ModuleUiHostContext { return { contributions: NO_MODULE_UI_CONTRIBUTIONS, + display: undefined, moduleId, api: { async request(path) { diff --git a/packages/module-ui-host/tests/real-artifact.test.ts b/packages/module-ui-host/tests/real-artifact.test.ts index 5d5b87b..0b1e391 100644 --- a/packages/module-ui-host/tests/real-artifact.test.ts +++ b/packages/module-ui-host/tests/real-artifact.test.ts @@ -33,6 +33,7 @@ let artifactSource: string; function ekklesiaContext(requested: string[]): ModuleUiHostContext { return { contributions: NO_MODULE_UI_CONTRIBUTIONS, + display: undefined, moduleId: "example", api: { async request(pathname) { @@ -50,6 +51,7 @@ function ekklesiaContext(requested: string[]): ModuleUiHostContext { function standaloneContext(requested: string[]): ModuleUiHostContext { return { contributions: NO_MODULE_UI_CONTRIBUTIONS, + display: { url: "https://example.test/display" }, moduleId: "example", api: { async request(pathname) { diff --git a/packages/module-ui-sdk/src/host-context.ts b/packages/module-ui-sdk/src/host-context.ts index e7cb760..728e7ce 100644 --- a/packages/module-ui-sdk/src/host-context.ts +++ b/packages/module-ui-sdk/src/host-context.ts @@ -75,6 +75,14 @@ export interface ModuleUiHostContext { /** The module this UI belongs to, as the host knows it. Supplied so a module never has to hardcode * or re-derive its own identity from a URL. */ readonly moduleId: string; + /** + * The host-owned public page for this module's dedicated display, when one exists. + * + * The URL is already absolute and deployment-correct. A module may show, open, or copy it, but + * must never construct a platform route or combine its own id with a browser origin. A standalone + * host can provide its own display page here without imitating Ekklesia's routing. + */ + readonly display: ModuleUiDisplayLocation | undefined; /** The only supported way for module UI to reach its own backend. See {@link ModuleUiApiClient}. */ readonly api: ModuleUiApiClient; /** Who is viewing, for presentation decisions only. See {@link ModuleUiPrincipal} for why this is @@ -96,6 +104,10 @@ export interface ModuleUiHostContext { readonly contributions: ModuleUiContributions; } +export interface ModuleUiDisplayLocation { + readonly url: string; +} + /** * A module UI's access to its own module's HTTP API, addressed **relative to the module's own API * root** — never as an absolute path. diff --git a/packages/module-ui-sdk/tests/module-ui-contract.test.ts b/packages/module-ui-sdk/tests/module-ui-contract.test.ts index 133fc7e..525a646 100644 --- a/packages/module-ui-sdk/tests/module-ui-contract.test.ts +++ b/packages/module-ui-sdk/tests/module-ui-contract.test.ts @@ -186,6 +186,7 @@ describe("the same module UI artifact hosted by two different hosts", () => { requestedUrls, context: { contributions: NO_MODULE_UI_CONTRIBUTIONS, + display: undefined, moduleId, api, principal: { kind: "anonymous" }, @@ -211,6 +212,7 @@ describe("the same module UI artifact hosted by two different hosts", () => { requestedUrls, context: { contributions: NO_MODULE_UI_CONTRIBUTIONS, + display: { url: "https://announcements.example.org/display" }, moduleId, api, principal: { kind: "anonymous" }, diff --git a/surfaces/shell/src/host-protocol.ts b/surfaces/shell/src/host-protocol.ts index 2f9805b..5f6f53e 100644 --- a/surfaces/shell/src/host-protocol.ts +++ b/surfaces/shell/src/host-protocol.ts @@ -105,6 +105,8 @@ export interface HostModuleUiDescriptor { /** Server-produced. The shell resolves it against `apiBaseUrl` and hands it to the generic loader * unchanged; it never constructs, parses, or pattern-matches this URL. */ readonly uiUrl: string; + /** Server-produced page for this module's display, when it publishes one. */ + readonly displayPageUrl: string | undefined; /** Server-produced root of this module's own HTTP API. */ readonly apiBaseUrl: string; /** Server-produced root for this module's own live channels. */ @@ -214,8 +216,24 @@ function parseDescriptor(entry: unknown): HostModuleUiDescriptor { const uiUrl = isRecord(entry) ? rootRelativePath(entry.ui, "url") : undefined; const apiBaseUrl = isRecord(entry) ? rootRelativePath(entry.api, "baseUrl") : undefined; const realtimeBaseUrl = isRecord(entry) ? rootRelativePath(entry.realtime, "baseUrl") : undefined; + const displayPageUrl = + isRecord(entry) && entry.display !== null ? rootRelativePath(entry.display, "url") : undefined; const permissions = isRecord(entry) ? entry.permissions : undefined; if ( + !hasExactKeys(entry, [ + "api", + "contributions", + "display", + "id", + "permissions", + "realtime", + "ui", + "version", + ]) || + !hasExactKeys(entry.api, ["baseUrl"]) || + !hasExactKeys(entry.realtime, ["baseUrl"]) || + !hasExactKeys(entry.ui, ["url"]) || + (entry.display !== null && !hasExactKeys(entry.display, ["url"])) || id === undefined || version === undefined || uiUrl === undefined || @@ -229,6 +247,7 @@ function parseDescriptor(entry: unknown): HostModuleUiDescriptor { return Object.freeze({ apiBaseUrl, contributions: parseContributions(isRecord(entry) ? entry.contributions : undefined), + displayPageUrl, id, permissions: Object.freeze([...(permissions as string[])]), realtimeBaseUrl, @@ -323,12 +342,14 @@ export function parseHostModuleDisplay(payload: unknown): HostModuleDisplayRespo "contributions", "display", "id", + "page", "permissions", "realtime", "version", ]) || !hasExactKeys(entry.api, ["baseUrl"]) || !hasExactKeys(entry.display, ["url"]) || + !hasExactKeys(entry.page, ["url"]) || !hasExactKeys(entry.realtime, ["baseUrl"]) ) { throw new HostProtocolError("HOST_RESPONSE_INVALID"); @@ -338,6 +359,7 @@ export function parseHostModuleDisplay(payload: unknown): HostModuleDisplayRespo const version = nonEmptyString(entry.version); const apiBaseUrl = rootRelativePath(entry.api, "baseUrl"); const uiUrl = rootRelativePath(entry.display, "url"); + const displayPageUrl = rootRelativePath(entry.page, "url"); const realtimeBaseUrl = rootRelativePath(entry.realtime, "baseUrl"); const permissions = entry.permissions; if ( @@ -347,6 +369,7 @@ export function parseHostModuleDisplay(payload: unknown): HostModuleDisplayRespo !isSemanticVersion(version) || apiBaseUrl === undefined || uiUrl === undefined || + displayPageUrl === undefined || realtimeBaseUrl === undefined || !Array.isArray(permissions) || permissions.some((permission) => typeof permission !== "string" || permission.length === 0) || @@ -359,6 +382,7 @@ export function parseHostModuleDisplay(payload: unknown): HostModuleDisplayRespo module: Object.freeze({ apiBaseUrl, contributions: parseContributions(entry.contributions), + displayPageUrl, id, permissions: Object.freeze([...(permissions as string[])]), realtimeBaseUrl, diff --git a/surfaces/shell/src/module-context.ts b/surfaces/shell/src/module-context.ts index bd504f3..26c17fa 100644 --- a/surfaces/shell/src/module-context.ts +++ b/surfaces/shell/src/module-context.ts @@ -350,6 +350,10 @@ export function createModuleUiContext(options: CreateModuleUiContextOptions): Mo options.descriptor, options.importModule, ), + display: + options.descriptor.displayPageUrl === undefined + ? undefined + : Object.freeze({ url: resolveApiUrl(options.config, options.descriptor.displayPageUrl) }), moduleId: options.descriptor.id, permissions: Object.freeze({ has: (permissionId: string) => granted.has(permissionId) }), principal: options.principal, diff --git a/surfaces/shell/tests/display-app.test.tsx b/surfaces/shell/tests/display-app.test.tsx index 8e4882e..a5df29b 100644 --- a/surfaces/shell/tests/display-app.test.tsx +++ b/surfaces/shell/tests/display-app.test.tsx @@ -52,6 +52,7 @@ function descriptorPayload( api: { baseUrl: `/api/v1/modules/${id}` }, display: { url: `/api/v1/module-ui/${id}/${version}/display` }, id, + page: { url: `/display/${id}` }, permissions: [`${id}.slideshow.view`], realtime: { baseUrl: `/api/v1/module-realtime/${id}` }, version, @@ -240,6 +241,7 @@ describe("display host — full-viewport boundary", () => { expect(Object.keys(seenContext ?? {}).sort()).toEqual([ "api", "contributions", + "display", "moduleId", "permissions", "principal", @@ -334,6 +336,7 @@ describe("display host — full-viewport boundary", () => { module: { contributions: [], api: { baseUrl: "/api/v1/modules/announcements" }, + display: { url: "/display/announcements" }, id: "announcements", permissions: ["announcements.slideshow.view"], realtime: { baseUrl: "/api/v1/module-realtime/announcements" }, diff --git a/surfaces/shell/tests/display-host-protocol.test.ts b/surfaces/shell/tests/display-host-protocol.test.ts index 19f88e3..fe9caf2 100644 --- a/surfaces/shell/tests/display-host-protocol.test.ts +++ b/surfaces/shell/tests/display-host-protocol.test.ts @@ -22,6 +22,7 @@ function displayPayload(overrides: Record = {}): { api: { baseUrl: "/api/v1/modules/announcements" }, display: { url: "/api/v1/module-ui/announcements/1.2.0/display" }, id: "announcements", + page: { url: "/display/announcements" }, permissions: ["announcements.slideshow.view"], realtime: { baseUrl: "/api/v1/module-realtime/announcements" }, version: "1.2.0", @@ -45,6 +46,7 @@ describe("display host protocol — strict descriptor", () => { module: { apiBaseUrl: "/api/v1/modules/announcements", contributions: [], + displayPageUrl: "/display/announcements", id: "announcements", permissions: ["announcements.slideshow.view"], realtimeBaseUrl: "/api/v1/module-realtime/announcements", diff --git a/surfaces/shell/tests/host-app.test.tsx b/surfaces/shell/tests/host-app.test.tsx index 23c3f69..383a457 100644 --- a/surfaces/shell/tests/host-app.test.tsx +++ b/surfaces/shell/tests/host-app.test.tsx @@ -43,6 +43,7 @@ function entry(id: string, name: string, version = "0.1.0", permissions: readonl module: { contributions: [], api: { baseUrl: `/api/v1/modules/${id}` }, + display: null, id, permissions, realtime: { baseUrl: `/api/v1/module-realtime/${id}` }, diff --git a/surfaces/shell/tests/host-protocol.test.ts b/surfaces/shell/tests/host-protocol.test.ts index 7a4febd..3f3f81e 100644 --- a/surfaces/shell/tests/host-protocol.test.ts +++ b/surfaces/shell/tests/host-protocol.test.ts @@ -39,6 +39,7 @@ const inventoryPayload = { { contributions: [], api: { baseUrl: "/api/v1/modules/example" }, + display: { url: "/display/example" }, id: "example", permissions: ["example.records.read"], realtime: { baseUrl: "/api/v1/module-realtime/example" }, @@ -51,6 +52,7 @@ const inventoryPayload = { const calendarDescriptor: HostModuleUiDescriptor = { contributions: [], apiBaseUrl: "/api/v1/modules/calendar", + displayPageUrl: undefined, id: "calendar", permissions: [], realtimeBaseUrl: "/api/v1/module-realtime/calendar", @@ -72,6 +74,7 @@ const navigationPayload = { module: { contributions: [], api: { baseUrl: "/api/v1/modules/calendar" }, + display: null, id: "calendar", permissions: [], realtime: { baseUrl: "/api/v1/module-realtime/calendar" }, @@ -86,6 +89,7 @@ const navigationPayload = { const exampleDescriptor: HostModuleUiDescriptor = { contributions: [], apiBaseUrl: "/api/v1/modules/example", + displayPageUrl: "/display/example", id: "example", permissions: ["example.records.read"], realtimeBaseUrl: "/api/v1/module-realtime/example", @@ -409,6 +413,7 @@ describe("module UI host context", () => { const calendar: HostModuleUiDescriptor = { contributions: [], apiBaseUrl: "/api/v1/modules/calendar", + displayPageUrl: undefined, id: "calendar", permissions: [], realtimeBaseUrl: "/api/v1/module-realtime/calendar", @@ -438,7 +443,18 @@ describe("module UI host context", () => { expect(none.has("core.authorization.grants.manage")).toBe(false); }); - it("exposes exactly the six contract members and the browser principal", () => { + it("exposes the host-owned display page at the API origin in split-origin deployments", () => { + const { implementation } = recordingFetch(); + const context = createModuleUiContext({ + config: { apiBaseUrl: "https://api.example.test", fetch: implementation }, + descriptor: exampleDescriptor, + principal: { kind: "authenticated", userId: "user-1" }, + }); + + expect(context.display?.url).toBe("https://api.example.test/display/example"); + }); + + it("exposes exactly the seven contract members and the browser principal", () => { const { implementation } = recordingFetch(); const context = contextFor(implementation); @@ -446,6 +462,7 @@ describe("module UI host context", () => { expect(Object.keys(context).sort()).toEqual([ "api", "contributions", + "display", "moduleId", "permissions", "principal", diff --git a/surfaces/shell/tests/module-contributions.test.tsx b/surfaces/shell/tests/module-contributions.test.tsx index 1bd18e2..f1a2a35 100644 --- a/surfaces/shell/tests/module-contributions.test.tsx +++ b/surfaces/shell/tests/module-contributions.test.tsx @@ -139,6 +139,7 @@ function descriptor(overrides: Partial = {}): HostModule version: "1.0.0", }, ], + displayPageUrl: "/display/signage", id: "signage", permissions: [], realtimeBaseUrl: "/api/v1/module-realtime/signage", diff --git a/surfaces/shell/tests/module-realtime-context.test.ts b/surfaces/shell/tests/module-realtime-context.test.ts index 58b7ddc..916b868 100644 --- a/surfaces/shell/tests/module-realtime-context.test.ts +++ b/surfaces/shell/tests/module-realtime-context.test.ts @@ -12,6 +12,7 @@ const apiBaseUrl = "https://ekklesia.example.test"; const exampleDescriptor: HostModuleUiDescriptor = { contributions: [], apiBaseUrl: "/api/v1/modules/example", + displayPageUrl: undefined, id: "example", permissions: ["example.state.read"], realtimeBaseUrl: "/api/v1/module-realtime/example", diff --git a/templates/module/tests/module-ui.test.ts b/templates/module/tests/module-ui.test.ts index f1850ea..af954f5 100644 --- a/templates/module/tests/module-ui.test.ts +++ b/templates/module/tests/module-ui.test.ts @@ -56,6 +56,7 @@ function recordingHost(options: { }; return { contributions: NO_MODULE_UI_CONTRIBUTIONS, + display: undefined, moduleId: "example", api, principal: { kind: "anonymous" },