Add the account area and account-synced Appearance preferences
CI / verify (pull_request) Successful in 1m10s
CI / infrastructure-smoke (pull_request) Successful in 34s
CI / package-release (pull_request) Successful in 50s
CI / migration-smoke (pull_request) Successful in 2m26s
CI / image-build (pull_request) Successful in 1m12s
CI / deploy-smoke (same-origin) (pull_request) Successful in 42s
CI / package-publication (pull_request) Successful in 54s
CI / deploy-smoke (split) (pull_request) Successful in 3m17s

Stage 1 PR 6 (ADR 0062). /account shows who is signed in; /account/appearance
chooses System, Light, or Dark and reduced motion (System, Reduce, Allow).

- Core: ekklesia_core_user_preference, one typed, CHECK-constrained,
  nullable column per preference (null = never chosen), migration
  0008_user_preferences (additive), @ekklesia/core/account store, and the
  authenticated-baseline permission core.account.self.manage.
- API: GET /api/v1/account (name, email, preferences) and partial
  PATCH /api/v1/account/preferences, self-only by construction: no user id
  is accepted anywhere; trusted Origin and body limit on the mutation.
- Shell: the account control shows name, email, and initials (never the
  unvetted image URL) and offers Account, Appearance, Sign out; native
  radio groups apply a choice at once through the ADR 0060 controller and
  save it; the server's answer wins and a failed save reverts.
- First paint: a classic, same-origin, render-blocking appearance.js in
  <head> applies a two-word localStorage cache before paint (no inline
  script, CSP unchanged). The account replaces the cache on every load,
  sign-out clears it, and other tabs follow via the storage event.
- Deploy smoke: real-Chromium checks for light/dark, OS emulation,
  overrides, reload persistence, pre-paint, Shadow DOM modules, reduced
  motion, the display staying light, and phone width; the browser
  container gets a 512 MiB /dev/shm so a long session no longer fails
  requests with ERR_INSUFFICIENT_RESOURCES.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
ekklesia
2026-10-14 23:56:50 -05:00
co-authored by Claude Opus 5.5
parent 8571e6854c
commit cd0287537e
88 changed files with 6404 additions and 158 deletions
+22
View File
@@ -425,6 +425,28 @@ enforced by `requirePermission` against the composed platform permission registr
enforcement neither knows nor cares what a browser decided. Hiding a control is a usability
improvement, never an access control. Treat any claim that a UI check protects anything as a bug.
### A person's account is theirs alone, and the browser's copy of it is not authority
The account area
([ADR 0062](../architecture/decisions/0062-account-area-and-synced-appearance-preferences.md))
returns a signed-in person's name, email, and preferences, and lets them change their preferences. It
is **self-only by construction**: `GET /api/v1/account` and `PATCH /api/v1/account/preferences` take
the account from the session and accept no user id anywhere, so no request can name another person.
Both routes require `core.account.self.manage` (baseline `authenticated`), which no device satisfies.
The mutation requires an exact trusted `Origin` and a bounded body, like every Core mutation.
- **Nothing more leaves the server.** The account's image URL, verification state, timestamps, and
user id are never returned.
- **No avatar image is loaded.** The account's `image` is an arbitrary, unvetted URL, and fetching it
would disclose every Dashboard visit to whatever host it names. Initials are drawn instead.
- **Nothing about the account is in the HTML.** The Dashboard page is still one snapshot for everyone.
The pre-paint `appearance.js` is a same-origin external script, so the page still has no inline
script and a strict `script-src` needs no exception.
- **The browser keeps two words.** It stores the appearance and motion words under
`ekklesia.dashboard.appearance` for the next first paint. That copy holds nothing about the account,
is validated strictly before use, is replaced by the account's answer on every load, and is cleared
on sign-out. Tampering with it changes nothing but this browser's colours until the account answers.
## What this platform must not claim
Ekklesia today has **none** of the following, and no document, README, or marketing surface may