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
+24
View File
@@ -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,
+4
View File
@@ -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,