feat: add browser auth shell
CI / verify (pull_request) Successful in 38s
CI / infrastructure-smoke (pull_request) Successful in 12s
CI / migration-smoke (pull_request) Successful in 1m0s

This commit is contained in:
ekklesia
2026-08-23 22:01:26 -05:00
parent 91aa9c2930
commit a8c7c50b4d
26 changed files with 2702 additions and 194 deletions
+41 -8
View File
@@ -155,7 +155,9 @@ elevation — but is disabled by default as of Phase 5D pass 1, below. See
[ADR 0011](decisions/0011-identity-http-transport-and-public-enrollment.md) and
[the API guide](../development/api.md), and
[the identity development guide](../development/identity.md) for the underlying schema and
contracts. It is now possible to publicly register, sign in, resolve a principal, and sign out.
contracts. Email/password sign-in, principal resolution, and sign-out are operational. Public
registration is available only when explicitly enabled for development/test; production rejects it
until verified enrollment and durable shared abuse controls exist.
Phase 4C is complete: Core's authorization foundation — a schema, evaluator, and store for
platform owners, explicit grants, and relationship-derived grants, plus an immutable permission
@@ -226,8 +228,8 @@ takes a DOM element and returns an unmount function, so a module's framework and
independent.
The contract lives in `@ekklesia/module-ui-sdk`, a browser-safe shared package with no runtime
dependencies, consumed by module authors and hosts alike. Its host context is four members —
`moduleId`, `api`, `principal`, `permissions`. Module UI reaches its own backend through
dependencies, consumed by module authors and hosts alike. Its host context is five members —
`moduleId`, `api`, `principal`, `permissions`, and `realtime`. Module UI reaches its own backend through
`api.request(path)` with a path relative to the module's own API root, never by constructing
Ekklesia's `/api/v1/modules/<module-id>` prefix, which is what keeps a standalone host possible.
Client-side permission hints are presentation only; server-side enforcement remains the sole
@@ -244,8 +246,9 @@ option in one `tooling` package so module authors write no build configuration.
rather than externalized, keeping `mount()` the ABI and a module artifact hostable by any host;
styling is inline style objects only, and a stylesheet import fails the build rather than emitting an
asset nothing declares. Building remains an authoring step — Core never builds, and discovery still
only validates the artifact a module shipped. The Dashboard shell, a UI runtime, a design system, a
stylesheet loading contract, the standalone host, and every feature module remain future work.
only validates the artifact a module shipped. At that checkpoint the Dashboard shell and UI runtime
were still future work; later Phase 6 passes supplied them. A design system, stylesheet loading
contract, standalone host, and every real feature module remain future work.
**Phase 6B pass 3 makes the browser boundary real and adds the generic host loader.**
[ADR 0024](decisions/0024-browser-safe-ui-sdk-and-host-loader.md) removes the UI SDK's last
@@ -338,9 +341,9 @@ permission the inventory does; organizing requires the new platform permission
shell renders that arrangement and computes none of it.
**Still not the finished Dashboard.** No editor or drag and drop, no nested folders, no icons, no
Dashboard Home or widgets, no design system, and no client routing. A production CSP and a deployment
for the built shell, sign-in UI, general asset publication, and the standalone host remain future
work.
Dashboard Home or widgets, no design system, and no client routing. A production CSP and deployment
for the built shell, general asset publication, and the standalone host remain future work. Phase 7B
pass 1 later supplies the host-owned sign-in flow described below.
Restart-based discovery, explicit operator-invoked migrations, and the trusted compiled-module model
are all unchanged.
@@ -504,3 +507,33 @@ whether routes exist, and installed-manifest changes remain restart-based. Realt
object URLs are unchanged and flow through the same portable contracts. Named display variants,
targeting, display inventory/management, device/public identity, a standalone host, shared CSS,
Announcements migration, and Companion integration remain later work.
Phase 7B pass 1 closes the Dashboard's browser authentication loop
([ADR 0034](decisions/0034-host-owned-browser-authentication.md)). The surface, not a module, owns
the email/password form, sign-out control, pending/failure states, and session transitions. Bootstrap
stays principal-first: anonymous renders login, authenticated proceeds to protected Dashboard
navigation, and an identity infrastructure failure remains a retryable failure rather than a false
anonymous result. Successful sign-in is followed by a fresh `/api/v1/principal` resolution before
any module mounts; sign-out is followed by the same resolution. Leaving authenticated state removes
the Dashboard subtree so the generic loader performs the selected module's normal cleanup, including
its realtime subscription teardown.
The browser posts to Better Auth's existing `/api/auth/sign-in/email` and `/api/auth/sign-out`
routes with credentials included. `apps/api` normalizes any body-bearing successful `2xx` email
sign-in response to fixed `{"ok":true}` while preserving its status and the HttpOnly `Set-Cookie`;
the redundant Better Auth token and user object never reach browser application JavaScript. The
token-bearing native `/api/auth/get-session`, `/api/auth/list-sessions`,
`/api/auth/update-session`, and `/api/auth/change-password` paths are unavailable behind a sanitized
`404`; `/api/v1/principal` is the browser's only supported session read, and password/account
management remains out of scope. The cookie remains host-only and exact trusted origins,
credentialed CORS, and origin/CSRF checks remain unchanged. Login stays at the current Dashboard URL
and accepts no callback or return target, so this pass introduces no redirect sink.
The five-member module UI context is unchanged; modules receive no auth method, credential, cookie,
or session token. Platform-owned bootstrap `401` maps directly to unauthenticated, but module-owned
responses are not host session authority: module `401` and `403` both trigger a deduplicated
principal revalidation, and only an anonymous result resets authentication and unmounts the module.
An authenticated result preserves the response, session, and mounted UI. Display device identity
remains deferred and the display bundle gains no Dashboard login. Public Website routing,
production Dashboard asset hosting/DNS/TLS/proxy/CSP, enrollment and recovery, shared throttling,
and the missing production first-user creation path are also outside this pass.