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
@@ -9,11 +9,13 @@ platform context, the application shell, top-level routing, navigation, the glob
|
||||
design-token contract, error boundaries, and trusted-origin/CORS policy are Core's and the host
|
||||
application's. A module consumes those; it never redefines or bypasses them.
|
||||
|
||||
> **What exists today:** a module may *declare* a UI entrypoint in its manifest, and Core validates
|
||||
> it. **Nothing loads, mounts, or renders it yet** — the host shell, the UI runtime, and the
|
||||
> standalone host are all later Phase 6 work. Writing a module UI is not yet possible end to end;
|
||||
> see [Declaring a UI entrypoint](#declaring-a-ui-entrypoint) for exactly what is and is not
|
||||
> supported.
|
||||
> **What exists today:** you can author a module UI in React, build it into the compiled artifact your
|
||||
> manifest declares, have Core validate it, and have Ekklesia serve it at a browser URL that
|
||||
> `@ekklesia/module-ui-host` can load and mount. **What does not exist is a host that does so** — no
|
||||
> Dashboard shell, no navigation, no standalone host, and nothing in Ekklesia that renders your UI to
|
||||
> a user. See [Declaring a UI entrypoint](#declaring-a-ui-entrypoint) and
|
||||
> [How Ekklesia publishes your artifact](#how-ekklesia-publishes-your-artifact) for exactly what is
|
||||
> and is not supported.
|
||||
|
||||
> **Before you publish or install a module:** modules are trusted compiled backend code. Your
|
||||
> module runs arbitrary JavaScript inside the API process and shares its PostgreSQL role, and
|
||||
@@ -383,11 +385,12 @@ Layer rules are unchanged: module UI source may not import Core, an app, a surfa
|
||||
another module. An unsupported framework aliased behind an allowed name — `"react": "npm:vue@3"` —
|
||||
is resolved and rejected by its real target.
|
||||
|
||||
> **Still no host.** You can author, build, and test a module UI end to end, and
|
||||
> `@ekklesia/module-ui-host` can load and mount a built artifact from a browser URL. **Nothing serves
|
||||
> your artifact at such a URL yet** — Ekklesia has no module-asset publication route, and the
|
||||
> Dashboard shell and standalone host do not exist. Your artifact is proven by tests, not mounted in
|
||||
> production. See [How a host loads your UI](#how-a-host-loads-your-ui).
|
||||
> **Your artifact is now served, but nothing mounts it.** Ekklesia publishes an installed module's
|
||||
> compiled UI artifact at `GET /api/v1/module-ui/<module-id>/<version>`
|
||||
> ([ADR 0025](../architecture/decisions/0025-trusted-module-ui-asset-publication.md)), which
|
||||
> `@ekklesia/module-ui-host` can load. **But no Ekklesia host calls it**: there is no Dashboard shell,
|
||||
> no navigation, and no standalone host, so your UI is proven by tests and reachable by URL, not
|
||||
> rendered anywhere in production. See [How a host loads your UI](#how-a-host-loads-your-ui).
|
||||
|
||||
### What this does and does not do today
|
||||
|
||||
@@ -409,7 +412,8 @@ You do not write this — a host does — but knowing the shape explains what yo
|
||||
|
||||
```text
|
||||
compiled artifact (dist/ui/index.js)
|
||||
↓ asset publication — host-specific, NOT YET IMPLEMENTED for Ekklesia
|
||||
↓ asset publication — host-specific; Ekklesia serves it at
|
||||
↓ GET /api/v1/module-ui/<module-id>/<version>
|
||||
browser-loadable module URL
|
||||
↓ loadModuleUi({ source, container, context })
|
||||
dynamic import → parseModuleUiExport → identity checked against the manifest
|
||||
@@ -439,10 +443,37 @@ What that means for you as a module author:
|
||||
- **Your UI may be mounted more than once**, into different containers, if your implementation
|
||||
supports it. Each mount gets its own cleanup.
|
||||
|
||||
> **Asset publication is the missing piece.** Core resolves `entrypoints.ui` to a path on the
|
||||
> *server's filesystem*; a browser cannot import that. Something must serve your compiled artifact at
|
||||
> a URL first, and Ekklesia has no such route yet. Until it does, no host can mount an installed
|
||||
> module's UI — the loader is ready, the plumbing to reach it is not.
|
||||
### How Ekklesia publishes your artifact
|
||||
|
||||
Core resolves `entrypoints.ui` to a path on the *server's filesystem*, which a browser cannot import.
|
||||
Ekklesia turns that into a URL
|
||||
([ADR 0025](../architecture/decisions/0025-trusted-module-ui-asset-publication.md)):
|
||||
|
||||
```text
|
||||
GET /api/v1/module-ui/<module-id>/<version>
|
||||
```
|
||||
|
||||
Four things about it matter to you as a module author:
|
||||
|
||||
- **Only the artifact your manifest declares is served.** The module id and version in that URL are
|
||||
lookup keys, never path components — there is no way to request a second file from your package.
|
||||
Your source map, any sibling chunk, and every package-root file stay unreachable. If your UI needs
|
||||
an asset, it must be inside the bundle; there is no sidecar contract yet, which is also why a
|
||||
stylesheet import still fails the build.
|
||||
- **The version must match exactly.** A request for any other version is refused rather than served
|
||||
your installed artifact, so a stale host cannot mount something it did not ask for.
|
||||
- **Your artifact is public.** The route requires no authentication — fetching it grants no
|
||||
authority, and your backend routes remain permission-enforced regardless. Never put a secret, a
|
||||
private endpoint, or anything you would not publish in your UI bundle.
|
||||
- **It is served byte-for-byte, never executed on the server.** No transformation, no re-bundling, no
|
||||
evaluation. What `pnpm build` produced is exactly what a browser receives.
|
||||
|
||||
The response carries `Content-Type: text/javascript; charset=utf-8`, `X-Content-Type-Options:
|
||||
nosniff`, `Cache-Control: no-cache`, and a strong `ETag`, so rebuilding your module at the same
|
||||
version is picked up on the next request rather than being cached indefinitely.
|
||||
|
||||
> **Still nothing mounts it.** The URL exists and the loader accepts it, but Ekklesia has no Dashboard
|
||||
> shell, no navigation, and no code that requests your UI. That is the next pass.
|
||||
|
||||
### What a module UI will never own
|
||||
|
||||
|
||||
Reference in New Issue
Block a user