feat: publish validated module UI artifacts at a platform-owned URL
Phase 6C pass 1. ADR 0024 named the one thing missing between the module UI build pipeline and the generic host loader: Core resolves entrypoints.ui to an absolute server filesystem path, and a browser needs a URL. This adds that publication layer. Core owns the policy. createModuleUiPublicationRegistry builds, from the discovered and started module inventory, the answer to "may this be published, and which exact file is it?" — publishable means the module survived discovery and declares entrypoints.ui, with no new lifecycle state and no installer. apps/api owns the transport: GET /api/v1/module-ui/<module-id>/<version>. Both segments are lookup keys, not path components. An exact match yields the canonical artifact path discovery already validated; no request value is ever composed into a filesystem path, and the route declares no wildcard segment, so traversal, encoded separators, sibling chunks, package-root files, and source maps are unreachable by construction. An installed module directory never becomes a static file root. The prefix is a sibling of /api/v1/modules/, not a child: `ui` is a legal module route segment, so a module registering GET /ui/:version would otherwise compete with the platform for one URL with routing order deciding the winner. The route requires no authentication, deliberately. Fetching the bundle confers no authority, and a cookie-gated route would break cross-origin import(), which sends none. The cost is stated rather than hidden: a caller who knows a module id and its exact version can download that bundle. The three failure modes return one identical 404 so the route is not an enumeration oracle. Caching is no-cache with a strong SHA-256 ETag and RFC 9110 If-None-Match handling, not immutable: without an installer nothing stops a version being rebuilt with different bytes. The server reads bytes and writes bytes — never imports, evaluates, or transforms the artifact. Nothing mounts a module UI. A host could now obtain a URL, which was previously impossible; there is still no Dashboard shell, navigation, or metadata endpoint. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
82cbeef437
commit
a02deb9433
@@ -216,10 +216,33 @@ host will use: dynamic import from a browser URL, identity validated against the
|
||||
mounting, a host-owned container the loader never mutates, and an idempotent unmount. Modules may not
|
||||
depend on it.
|
||||
|
||||
The distinction that shapes the next pass: Core resolves `entrypoints.ui` to a **server filesystem
|
||||
path**, and a browser needs a **URL**. Turning an installed artifact into a browser URL is asset
|
||||
publication — host-specific, deliberately outside the loader, and not implemented. Until it exists,
|
||||
no host can mount an installed module UI.
|
||||
**Phase 6C pass 1 publishes a validated module UI artifact at a browser URL.**
|
||||
[ADR 0025](decisions/0025-trusted-module-ui-asset-publication.md) closes the gap the previous pass
|
||||
named: Core resolves `entrypoints.ui` to a **server filesystem path**, and a browser needs a **URL**.
|
||||
|
||||
Core owns the policy and `apps/api` owns the transport. `createModuleUiPublicationRegistry`
|
||||
(`core/src/modules/ui-publication.ts`) answers "may this be published, and which exact file is it?"
|
||||
from the discovered, started module inventory — publishable means the module survived discovery
|
||||
*and* declares `entrypoints.ui`, no new lifecycle state and no installer. `apps/api` serves
|
||||
`GET /api/v1/module-ui/<module-id>/<version>`, where both segments are **lookup keys, not path
|
||||
components**: an exact match yields the canonical artifact path discovery already validated, and no
|
||||
request value is ever composed into a filesystem path. There is no wildcard segment, so traversal,
|
||||
sibling chunks, package-root files, and source maps are unreachable by construction — an installed
|
||||
module directory never becomes a static file root. The prefix is a sibling of `/api/v1/modules/`
|
||||
rather than a child because `ui` is a legal module route segment and a module could otherwise collide
|
||||
with the platform.
|
||||
|
||||
The route requires no authentication: the bundle confers no authority, every module API stays behind
|
||||
`requirePermission`, and a cookie-gated route would break cross-origin `import()`, which sends none.
|
||||
The disclosure that buys — a caller who knows a module id and its exact version can download that
|
||||
bundle — is stated rather than papered over, and the three failure modes are made indistinguishable
|
||||
so the route is not an enumeration oracle. Caching is `no-cache` with a strong `ETag`, not
|
||||
`immutable`, because without an installer a version can be rebuilt with different bytes. The server
|
||||
reads and writes bytes; it never imports, evaluates, or transforms the artifact.
|
||||
|
||||
**A host could now obtain a URL, and nothing uses one.** There is no Dashboard shell, no navigation,
|
||||
no module metadata endpoint, and no code that calls the URL helper outside its own tests. A design
|
||||
system, stylesheet and general asset publication, and the standalone host remain future work.
|
||||
Restart-based discovery, explicit operator-invoked migrations, and the trusted compiled-module model
|
||||
are all unchanged.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user