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
@@ -299,12 +299,38 @@ idempotent `unmount()`. A module may never depend on it — it is host infrastru
|
||||
checker enforces that. See
|
||||
[ADR 0024](docs/architecture/decisions/0024-browser-safe-ui-sdk-and-host-loader.md).
|
||||
|
||||
**Still no host, and one specific reason why.** Core resolves `entrypoints.ui` to a path on the
|
||||
*server's filesystem*; a browser cannot import that. **Ekklesia has no route that serves an installed
|
||||
module's artifact at a browser URL**, so nothing can mount an installed module UI yet — the loader is
|
||||
complete, asset publication is not, and that is the next pass's first job. The Dashboard shell,
|
||||
navigation, a design system, a stylesheet loading contract, and the standalone host also do not
|
||||
exist.
|
||||
Phase 6C pass 1 builds the publication layer those passes were missing. Core resolves
|
||||
`entrypoints.ui` to a path on the *server's filesystem*, and a browser needs a **URL**; the platform
|
||||
now serves one:
|
||||
|
||||
```text
|
||||
GET /api/v1/module-ui/<module-id>/<version>
|
||||
```
|
||||
|
||||
`:moduleId` and `:version` are **lookup keys, not path components**. They are matched against a
|
||||
publication registry Core builds from the discovered, started module inventory, and only an exact
|
||||
match causes the already-canonical artifact path discovery validated to be read. No request value is
|
||||
ever joined 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**, and only the single artifact its
|
||||
manifest declares is served. A module that is not installed, one that declares no UI, and a wrong
|
||||
version return one identical `404`; an artifact that vanished after discovery returns a sanitized
|
||||
`503` carrying no path. The prefix is a *sibling* of `/api/v1/modules/`, not a child, because `ui` is
|
||||
a legal module route segment and a module could otherwise collide with the platform.
|
||||
|
||||
**The route requires no authentication, deliberately.** Fetching the bundle confers no authority —
|
||||
every module API stays behind `requirePermission`, and the host, not the artifact, supplies the
|
||||
principal — while requiring a cookie would break cross-origin `import()`, which sends none. The cost
|
||||
is stated plainly: a caller who already knows a module id *and* its exact installed version can
|
||||
download that bundle. Caching is `no-cache` with a strong `ETag` rather than `immutable`, because
|
||||
without an installer nothing stops a version being rebuilt with different bytes. See
|
||||
[ADR 0025](docs/architecture/decisions/0025-trusted-module-ui-asset-publication.md).
|
||||
|
||||
**This is not Dashboard integration.** A host *could* now obtain a browser URL, which was previously
|
||||
impossible — but nothing in Ekklesia mounts a module UI. There is no Dashboard shell, no navigation,
|
||||
no place a module's screen appears, and no code calls the URL helper outside its own tests. A design
|
||||
system, a stylesheet or general asset publication contract, a module metadata endpoint, and the
|
||||
standalone host also do not exist.
|
||||
|
||||
**Security model.** Installed modules are trusted compiled backend code that runs arbitrary
|
||||
JavaScript in the API process and shares its PostgreSQL role — table prefixes, manifests, permission
|
||||
|
||||
Reference in New Issue
Block a user