The theme, text scale, text weight, high contrast, custom theme variable
and custom CSS caches in localStorage were untagged, so on a shared
browser a second account inherited the first account's appearance: with
no server value of its own, every fallback resolved to whatever the
previous account had stored, and the leftover `silo-theme` key also
suppressed the admin-configured default theme for the new account.
DateTimeFormatProvider already solved this by stamping its cache with the
authenticated user id and refusing another account's values. Extract that
mechanism into `createOwnedCache` in utils/storage.ts (where key
namespacing lives) and put all three groups behind it, so appearance and
custom theme get the same protection instead of a third copy of the rule.
- Each group carries its own owner stamp. A shared stamp would be unsafe:
the groups are written by hooks nested inside each other, and effects
run inner-first, so whichever hook stamped first would vouch for the
other's still-stale values.
- A null owner (auth bootstrapping, or signed out) still trusts the
cache, which keeps the warm start and the login screen's last look.
- An unstamped cache is not trusted once an account is known, so existing
users take a one-time appearance reset on first load rather than a
chance of seeing someone else's settings.
- When a foreign cache is detected the values are dropped and the empty
cache is handed to the new account, so a later single save cannot
re-trust the rest of the previous account's state.
Owner is the user id because /settings is user-scoped server side; it
lives in one helper (`appearanceCacheOwner`) so it can be widened if
appearance moves to profile scope. `shouldLoadApiTheme` is gone: it had
become a synonym for `appearanceCacheOwner(...) !== null` with no callers
left.
Part of #376
AI-use disclosure: implemented with Claude Code.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>