import { isVersionCompatible, type PermissionDeclaration, type PermissionId, } from "@ekklesia/contracts"; import type { DiscoveredModule } from "./discovery.js"; /** * One installed module whose UI artifact the platform is willing to publish at a browser URL. * * `artifactPath` is the canonical absolute path discovery already resolved and validated for * `entrypoints.ui` — realpath-resolved, proven to be a compiled `.js`/`.mjs` file, and proven to * stay inside the module package both before and after symlink resolution * (`resolveEntrypoints`, `./discovery.js`). A publisher serves **exactly this path** and never * composes a path of its own from request input; that is the whole reason this record exists * rather than a module id plus a path-building convention. */ export interface PublishableModuleUi { readonly moduleId: string; readonly version: string; /** * The module's own `manifest.name` — its product identity, required by every manifest version and * therefore available without any manifest change (ADR 0027). * * Carried here so a host can render something a person recognizes instead of a module id. It is * presentation *identity*, not presentation *placement*: a module says what it is called, while * where it appears in a Dashboard is configuration an administrator owns. Nothing about grouping, * ordering, or navigation belongs on this record. */ readonly name: string; readonly artifactPath: string; /** * The module's own manifest-declared permissions, carried here so a publisher can compute the * presentation-only permission hints a host context needs (ADR 0022) **without** reaching back * into the manifest and re-deriving which modules are publishable — which would put the * publication policy in two places. * * Declarations only: this says what the module declares, never what any principal holds. * Evaluating a principal against them is the caller's job, through Core's one authorization * evaluator, and the answer is a hint rather than a grant. `artifactPath` and these declarations * are the complete set of things a publisher may need; nothing else from the manifest belongs * here. */ 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[]; } /** * One installed module's browser contribution that the platform is willing to publish (ADR 0041). * * Publishable means three things, and nothing beyond them: the contributing module survived * discovery and is in the eligible set, some **other** eligible module provides the targeted * capability, and that provider's declared capability version satisfies the contribution's declared * range. A contribution whose capability nobody provides, or whose provider moved outside its range, * is simply absent — never published-but-broken, and never mounted with a shrug. * * Note what is *not* here. There is no payload schema, no slot, no ordering, no rendering hint, and * no domain vocabulary of any kind: Core carries identity, version, compatibility, and one file * path. What a contributed renderer draws, and what shape its input takes, are the consumer module's * business and travel between the two modules in the browser without passing through the platform. */ export interface PublishableModuleContribution { /** The module that *contributes* the renderer. */ readonly moduleId: string; readonly moduleVersion: string; readonly contributionId: string; /** The contribution's own version, independent of its module's. */ readonly version: string; readonly capability: string; /** The version the providing module declares for that capability — the value the contribution's * declared range was actually checked against. */ readonly capabilityVersion: string; readonly artifactPath: string; } /** * The platform's answer to "may this module's UI artifact be served, and if so, which exact file?" * * Lookup is by module **identity**, never by path. There is deliberately no method that accepts a * filename, a relative path, a glob, or anything else that could turn an installed module * directory into a static file root. */ export interface ModuleUiPublicationRegistry { /** Every publishable module, in the discovery order it was built from. Stable and frozen. */ list(): readonly PublishableModuleUi[]; /** * The publishable record for an exact module id **and** exact installed version, or `undefined`. * * Takes `unknown` for the same reason `PermissionRegistry.get` does: the caller is usually * handing over decoded request input, and a lookup that fails closed on anything malformed is * safer than one whose type signature quietly assumes validation already happened. A module that * is not installed, one that declares no `entrypoints.ui`, and one installed at a different * 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; /** Every publishable browser contribution, in discovery order then manifest order. Frozen. */ listContributions(): readonly PublishableModuleContribution[]; /** * Every publishable contribution targeting one capability, in the same stable order. * * This is the lookup a *consumer* module's descriptor is built from, and the reason a consumer can * only ever learn about renderers aimed at a capability it actually provides: the caller passes * its own declared capability id and gets back exactly the contributions for it. */ contributionsForCapability(capabilityId: unknown): readonly PublishableModuleContribution[]; /** * Every publishable contribution aimed at **any** capability the named module provides. * * This is the one lookup a descriptor for a consuming module is built from. Framing it per module * rather than per capability is what keeps capability ownership inside Core: a caller hands over a * module id and gets that module's own extension points filled in, with no way to ask about * someone else's. */ contributionsForModule(moduleId: unknown): readonly PublishableModuleContribution[]; /** * The publishable record for an exact contributing module id, installed module version, and * contribution id, or `undefined`. * * All three must match. A contribution that is not installed, one whose module is at a different * version, one whose capability has no compatible provider, and an id that was never declared are * all indistinguishable here — each returns `undefined`. */ resolveContribution( moduleId: unknown, moduleVersion: unknown, contributionId: unknown, ): PublishableModuleContribution | undefined; } /** * Builds the platform's module UI publication policy from an already-discovered module list. * * The policy is deliberately small. "Publishable" means exactly: * * 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. 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 * artifact on disk must not be what makes it servable*. Since ADR 0028 the same is true of a module * an administrator disabled: it is discovered and validated, and it is still not publishable, * because this registry is built from the eligible list rather than the discovered one. That is why * the filtering lives at the *call site's choice of input* rather than as a `disabled` check * somewhere downstream — there is exactly one authoritative eligible set, and everything that * exposes a module consumes it. * * Pure and synchronous: no filesystem, environment, database, or network access, and it never * imports, evaluates, or otherwise executes a module's UI artifact. Production composition * (`apps/api/src/server.ts`) passes the exact list it just started, and only after * `startDiscoveredModuleRuntime` succeeded — startup is all-or-nothing across that set, so a module * that failed to load, register, or start takes API startup down with it rather than lingering as a * publishable-but-not-running module. */ export function createModuleUiPublicationRegistry( modules: readonly DiscoveredModule[], ): ModuleUiPublicationRegistry { const byModuleId = new Map(); const displaysByModuleId = new Map(); const contributions: PublishableModuleContribution[] = []; // Every capability the eligible set actually provides, at the version its provider declares. Built // from the same list everything else here is built from, so a contribution can never be checked // against a provider the platform is not running. const capabilityVersions = new Map(); for (const module of modules) { for (const capability of module.manifest.capabilities) { capabilityVersions.set(capability.id, capability.version); } } for (const module of modules) { for (const contribution of module.contributions) { const capabilityVersion = capabilityVersions.get(contribution.capability); // Fails closed on both halves: an absent provider and an incompatible one are equally // unpublishable, so a range mismatch can never be papered over by mounting anyway. if ( capabilityVersion === undefined || !isVersionCompatible(capabilityVersion, contribution.capabilityRange) ) { continue; } contributions.push( Object.freeze({ artifactPath: contribution.artifactPath, capability: contribution.capability, capabilityVersion, contributionId: contribution.id, moduleId: module.manifest.id, moduleVersion: module.manifest.version, version: contribution.version, }), ); } } for (const module of modules) { const artifactPath = module.entrypoints.ui; 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, }), ); } } const published = Object.freeze([...byModuleId.values()]); const publishedDisplays = Object.freeze([...displaysByModuleId.values()]); const publishedContributions = Object.freeze([...contributions]); const contributionsByCapability = new Map(); const contributionsByKey = new Map(); for (const contribution of publishedContributions) { const forCapability = contributionsByCapability.get(contribution.capability) ?? []; forCapability.push(contribution); contributionsByCapability.set(contribution.capability, forCapability); // Module id and contribution id are both namespaced and unique, so this key cannot collide. contributionsByKey.set( `${contribution.moduleId}\u0000${contribution.contributionId}`, contribution, ); } const frozenByCapability = new Map( [...contributionsByCapability].map(([capability, list]) => [capability, Object.freeze(list)]), ); const noContributions: readonly PublishableModuleContribution[] = Object.freeze([]); const byConsumingModule = new Map( modules.map((module) => [ module.manifest.id, Object.freeze( module.manifest.capabilities.flatMap( (capability) => contributionsByCapability.get(capability.id) ?? [], ), ), ]), ); return Object.freeze({ contributionsForCapability: ( capabilityId: unknown, ): readonly PublishableModuleContribution[] => typeof capabilityId === "string" ? (frozenByCapability.get(capabilityId) ?? noContributions) : noContributions, contributionsForModule: (moduleId: unknown): readonly PublishableModuleContribution[] => typeof moduleId === "string" ? (byConsumingModule.get(moduleId) ?? noContributions) : noContributions, listContributions: (): readonly PublishableModuleContribution[] => publishedContributions, resolveContribution: ( moduleId: unknown, moduleVersion: unknown, contributionId: unknown, ): PublishableModuleContribution | undefined => { if ( typeof moduleId !== "string" || typeof moduleVersion !== "string" || typeof contributionId !== "string" ) { return undefined; } const candidate = contributionsByKey.get(`${moduleId}\u0000${contributionId}`); // Exact installed-version equality, exactly as the UI and display lookups require: a request // naming a version the platform does not have installed resolves to nothing at all. return candidate?.moduleVersion === moduleVersion ? candidate : undefined; }, list: (): readonly PublishableModuleUi[] => published, listDisplays: (): readonly PublishableModuleDisplay[] => publishedDisplays, resolve: (moduleId: unknown, version: unknown): PublishableModuleUi | undefined => { if (typeof moduleId !== "string" || typeof version !== "string") { return undefined; } const candidate = byModuleId.get(moduleId); // Exact version equality, never a range, a "latest" fallback, or a nearest match. A request // naming a version the platform does not have installed resolves to nothing at all rather // 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; }, }); }