feat: add display kiosk host
This commit is contained in:
+88
-5
@@ -81,6 +81,9 @@ No real values are shown here — configure per environment and never commit the
|
||||
| `GET /api/v1/dashboard/navigation` | Ekklesia | The resolved Dashboard navigation: the same mountable modules, grouped, labelled, and ordered as this installation has configured them. Requires `core.modules.ui.read`. See [Dashboard navigation](#dashboard-navigation). |
|
||||
| `PUT\|DELETE /api/v1/dashboard/navigation/{groups,placements}/*` | Ekklesia | Organize the Dashboard: create/rename/order/delete a group, and place a module. Requires `core.dashboard.navigation.manage` (baseline `restricted`) and a trusted `Origin`. See [Dashboard navigation](#dashboard-navigation). |
|
||||
| `GET /api/v1/module-ui/<module-id>/<version>` | Ekklesia | Publishes an installed module's validated `entrypoints.ui` artifact at a browser-loadable URL. No authentication; only the declared artifact, never another file. See [Module UI asset publication](#module-ui-asset-publication). |
|
||||
| `GET /api/v1/module-ui/<module-id>/<version>/display` | Ekklesia | Publishes the exact validated display artifact for that eligible/running module. Artifact bytes are anonymous trusted code; this route is not the authorization boundary. See [Module display host](#module-display-host). |
|
||||
| `GET /display/<module-id>` | Ekklesia | Stable, full-viewport display page with no Dashboard chrome. Requires the module-owned permission declared beside `entrypoints.display`. See [Module display host](#module-display-host). |
|
||||
| `GET /api/v1/module-displays/<module-id>` | Ekklesia | Protected descriptor for one display page: exact artifact, module API/realtime roots, and presentation-only permission hints. Requires that display's module-owned permission. See [Module display host](#module-display-host). |
|
||||
| `GET /api/v1/module-lifecycle` | Ekklesia | Which installed modules are enabled, what this process actually did with each, and whether a restart is pending. Requires `core.modules.lifecycle.manage` (baseline `restricted`). See [Module lifecycle](#module-lifecycle). |
|
||||
| `PUT /api/v1/module-lifecycle/<module-id>/enabled` | Ekklesia | Enable or disable one installed module. Takes effect at the next API start. Requires `core.modules.lifecycle.manage` and a trusted `Origin`. See [Module lifecycle](#module-lifecycle). |
|
||||
| `GET /api/v1/module-configuration/<module-id>` | Ekklesia | One module's declared configuration schema, its stored values, and whether a restart is pending. Never returns a secret value. Requires `core.modules.configuration.manage` (baseline `restricted`). See [Module configuration](#module-configuration). |
|
||||
@@ -990,7 +993,8 @@ root of the same resource the artifact route below serves: one prefix, two repre
|
||||
prefix, which lets one change without touching host or module code.
|
||||
- **The module set is the publication registry**, not a rescan: modules that survived discovery and
|
||||
declare `entrypoints.ui`, captured after the module runtime started. A module with no UI is absent.
|
||||
Order is the registry's own deterministic discovery order.
|
||||
Order is the registry's own deterministic discovery order. Dedicated display artifacts are not
|
||||
merged into this primary-UI inventory; this pass deliberately adds no display enumeration API.
|
||||
- **Authenticated, unlike the artifact route.** Requires `core.modules.ui.read`, baseline
|
||||
`authenticated` — any signed-in user, and nobody else. The artifact bytes are anonymous because
|
||||
fetching them confers no authority, but that route is deliberately not an enumeration oracle, and
|
||||
@@ -1027,7 +1031,9 @@ GET /api/v1/module-ui/<module-id>/<version>
|
||||
joined, resolved, or concatenated into a filesystem path, and the route declares no wildcard
|
||||
segment — so traversal, encoded separators, sibling bundle chunks, package-root files, and source
|
||||
maps are unreachable by construction. **An installed module directory is never a static file
|
||||
root**, and only the single artifact its manifest declares is ever served.
|
||||
root**, and only the single primary artifact its manifest declares is ever served. A separately
|
||||
declared display artifact uses the exact sibling route described below; neither role can select
|
||||
the other's file.
|
||||
- **A sibling of `/api/v1/modules/`, never a child.** A module may legitimately register the route
|
||||
path `/ui/:version`, so nesting publication inside the module prefix would let a module and the
|
||||
platform compete for one URL with routing order deciding the winner.
|
||||
@@ -1058,9 +1064,86 @@ construction of this URL. It accepts identity only and percent-encodes both comp
|
||||
root-relative, so a same-origin host uses it unchanged and a split-origin host resolves it against
|
||||
the same API base URL it already needs for `ModuleUiApiClient`.
|
||||
|
||||
**Nothing mounts a module UI yet.** A host can now obtain a browser URL that
|
||||
`@ekklesia/module-ui-host` accepts, which was previously impossible — but Ekklesia has no Dashboard
|
||||
shell, no navigation, and no code that calls this route outside its own tests.
|
||||
Ekklesia's Dashboard surface consumes the primary inventory and mounts these URLs through
|
||||
`@ekklesia/module-ui-host`. The dedicated display surface reuses the same loader and ABI but obtains
|
||||
its role-specific URL only from the protected descriptor below.
|
||||
|
||||
## Module display host
|
||||
|
||||
Manifest version 6 adds one independently optional display declaration
|
||||
([ADR 0033](../architecture/decisions/0033-display-kiosk-host-contract.md)):
|
||||
|
||||
```json
|
||||
{
|
||||
"entrypoints": {
|
||||
"display": {
|
||||
"artifact": "./dist/display/index.js",
|
||||
"permission": "announcements.display.view"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
That permission must be declared by the same module and have an `authenticated` or `restricted`
|
||||
baseline; `public` is rejected because public/device display identity is deferred. The artifact path
|
||||
uses the normal compiled, package-contained entrypoint rules and may not collide with server,
|
||||
schema, migrations, jobs, or primary UI. A module may declare primary UI only, display only, both,
|
||||
or neither.
|
||||
|
||||
For every display-capable module in the immutable eligible/running publication registry, API
|
||||
composition installs three exact routes:
|
||||
|
||||
```text
|
||||
GET /display/<module-id>
|
||||
GET /api/v1/module-displays/<module-id>
|
||||
GET /api/v1/module-ui/<module-id>/<version>/display
|
||||
```
|
||||
|
||||
The first is a stable bookmark independent of the installed version. It serves the same frozen,
|
||||
module-agnostic full-viewport HTML shell for every module, with no Dashboard application or chrome.
|
||||
The second returns the only descriptor that shell needs:
|
||||
|
||||
```json
|
||||
{
|
||||
"module": {
|
||||
"id": "announcements",
|
||||
"version": "0.1.0",
|
||||
"display": {
|
||||
"url": "/api/v1/module-ui/announcements/0.1.0/display"
|
||||
},
|
||||
"api": { "baseUrl": "/api/v1/modules/announcements" },
|
||||
"realtime": { "baseUrl": "/api/v1/module-realtime/announcements" },
|
||||
"permissions": ["announcements.display.view"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- **Page and descriptor authorization.** Both run the ordinary cookie principal through Core's one
|
||||
evaluator for exactly the display permission stored in the validated publication record. There
|
||||
is no query token, public exception, device principal, or separate authorization system. The
|
||||
descriptor's `permissions` remains presentation-only; module HTTP and realtime routes still
|
||||
enforce their own declared permissions.
|
||||
- **Artifact publication.** The versioned `/display` suffix publishes exactly the canonical display
|
||||
artifact, byte-for-byte, with the same MIME, `nosniff`, strong ETag, `no-cache`, sanitized `404`/
|
||||
`503`, and anonymous-code policy as primary UI. It never falls back to `entrypoints.ui`, and no
|
||||
request component becomes a filesystem path. Fetching code grants no API, realtime, or data
|
||||
authority.
|
||||
- **Lifecycle and restart.** Unknown, primary-only, disabled, dependency-blocked, and failed-startup
|
||||
modules have no display routes and collapse into the normal sanitized `404`. Production reads the
|
||||
exact display-host HTML and script once before binding, freezes them, and serves fixed paths rather
|
||||
than a directory. Installation, manifest, enablement, and build changes take effect on restart.
|
||||
- **Browser behavior.** The shell fetches principal first, then the protected descriptor, and only
|
||||
then dynamically imports the supplied artifact. It supplies the same role-neutral
|
||||
`ModuleUiHostContext` used by primary UI—API, principal, permissions, and realtime—with no mode
|
||||
flag. Failures become fixed states with manual retry. The host fills the viewport but does not
|
||||
request the browser Fullscreen API.
|
||||
- **Unchanged data paths.** Realtime stays on the permission-gated SSE route above. Signed object
|
||||
delivery URLs remain opaque bearer strings generated by module backend code and fetched directly
|
||||
from storage; the display host neither proxies nor re-signs them.
|
||||
|
||||
There is deliberately no display inventory or management API. Named variants, targeting,
|
||||
public/device identity, a standalone host, a shared stylesheet contract, Announcements migration,
|
||||
and Companion integration remain future work.
|
||||
|
||||
## Module database access
|
||||
|
||||
|
||||
Reference in New Issue
Block a user