ISOLATION: per-User browser caches (background preview, inbox, Money) survive sign-out; shared userStorage + boot hint + clear on session end #555

Closed
opened 2026-09-30 20:29:56 +00:00 by kayg · 7 comments
Owner

Found in review (2026-09-30): per-User data in browser storage outlives the session

Several branches now cache per-User data in the browser, so the app paints instantly (owner rule: render from cache, then revalidate):

  • job/bg-flash (#535): localStorage['calternal.appearance-boot'] and ['calternal.backgrounds'] hold a preview of the User's background picture (possibly a personal photo). The inline boot script in app.html paints it before auth is known.
  • job/reload-423 (#423): apps/web/src/lib/mail/inboxCache.ts (inbox rows: senders, subjects), the Money store cache, and the warm route caches.
  • Probably others (search recents are already per-User keyed in recent.ts; check them).

Sign-out does not clear any of it (no localStorage cleanup found in the sign-out path, +layout.svelte ~491). On a shared browser, User B signing in after User A sees A's background picture before first paint, and possibly A's inbox rows from the cache until revalidation. This is the cross-user isolation failure class (merge blocker).

Fix (one owner, one shared primitive; reuse gate):

  1. A single userStorage module for every per-User client cache (localStorage, sessionStorage, IndexedDB, Cache Storage). Keys are namespaced by User ID. It exposes get/set/remove/clearUser/clearAll. Migrate every existing per-User key to it; a unit/lint check fails on direct localStorage use for per-User data outside the module (a documented allow-list for device-only prefs such as the sidebar width, if they are not personal).
  2. Boot script: before the User is known, paint only data tagged with the last signed-in User ID. That ID is stored in a non-secret, non-HttpOnly device hint written at sign-in and removed at sign-out. If the hint is missing or mismatched, paint the neutral paper colour, never a picture or a list.
  3. Sign-out (this device, "sign out other devices" when it applies to this one, a 401 or expired-session redirect, and a User switch) calls clearUser(id) + removes the hint before navigating to the sign-in page. Private data must not survive a session end.
  4. Cached images use private caching only (already true for the v=1 thumbnails); confirm no service worker caches per-User responses across Users.
    Tests:
  • An e2e with two Users on one browser context: A sets a background picture, opens Mail and Money, signs out; B signs in. Assert that no frame (sample every rAF from navigation start) shows A's picture, and that no A data appears in DOM or storage (dump all storage after B's first paint).
  • The same after A's session expires (401).
  • Add to the cross-user matrix docs (#331).
    Order: bg-flash (#535) and reload-423 (#423) merge only after this lands, or adopt it in their branch. This job owns the module; those jobs reuse it.
## Found in review (2026-09-30): per-User data in browser storage outlives the session Several branches now cache per-User data in the browser, so the app paints instantly (owner rule: render from cache, then revalidate): - **job/bg-flash (#535):** `localStorage['calternal.appearance-boot']` and `['calternal.backgrounds']` hold a preview of the User's background **picture** (possibly a personal photo). The inline boot script in `app.html` paints it **before** auth is known. - **job/reload-423 (#423):** `apps/web/src/lib/mail/inboxCache.ts` (inbox rows: senders, subjects), the Money store cache, and the warm route caches. - Probably others (search recents are already per-User keyed in `recent.ts`; check them). **Sign-out does not clear any of it** (no `localStorage` cleanup found in the sign-out path, `+layout.svelte` ~491). On a shared browser, User B signing in after User A sees A's background picture before first paint, and possibly A's inbox rows from the cache until revalidation. This is the cross-user isolation failure class (merge blocker). **Fix (one owner, one shared primitive; reuse gate):** 1. A single `userStorage` module for every per-User client cache (localStorage, sessionStorage, IndexedDB, Cache Storage). Keys are namespaced by User ID. It exposes `get/set/remove/clearUser/clearAll`. Migrate every existing per-User key to it; a unit/lint check fails on direct `localStorage` use for per-User data outside the module (a documented allow-list for device-only prefs such as the sidebar width, if they are not personal). 2. **Boot script:** before the User is known, paint only data tagged with the **last signed-in User ID**. That ID is stored in a non-secret, non-HttpOnly device hint written at sign-in and removed at sign-out. If the hint is missing or mismatched, paint the neutral paper colour, never a picture or a list. 3. **Sign-out** (this device, "sign out other devices" when it applies to this one, a 401 or expired-session redirect, and a User switch) calls `clearUser(id)` + removes the hint **before** navigating to the sign-in page. Private data must not survive a session end. 4. Cached images use `private` caching only (already true for the v=1 thumbnails); confirm no service worker caches per-User responses across Users. **Tests:** - An e2e with two Users on one browser context: A sets a background picture, opens Mail and Money, signs out; B signs in. Assert that no frame (sample every rAF from navigation start) shows A's picture, and that no A data appears in DOM or storage (dump all storage after B's first paint). - The same after A's session expires (401). - Add to the cross-user matrix docs (#331). **Order:** bg-flash (#535) and reload-423 (#423) merge only after this lands, or adopt it in their branch. This job owns the module; those jobs reuse it.
Author
Owner

Started #555 on job/bg-flash. Base origin/dev: 15e17aeafc. Existing #535 head: a62a8156bd.

The browser-cache audit found unscoped Appearance, settings, Calendar, Files folder preferences, pending uploads and Money selection keys. Search recents and Composer drafts include User IDs but survive session end. I will add one storage primitive and remove legacy caches without a verified owner. The push-only service worker has no fetch handler.

Decision: unowned legacy browser data is discarded instead of assigned to the next signed-in User. The server will bind the non-secret boot hint to the current session on document requests, so a revoked session cannot paint its previous picture before the client sees a 401.

Started #555 on job/bg-flash. Base origin/dev: 15e17aeafc8ea160c109e62fba57f6961c39d21f. Existing #535 head: a62a8156bd84073c438118d9ff2a4a3c5c058c51. The browser-cache audit found unscoped Appearance, settings, Calendar, Files folder preferences, pending uploads and Money selection keys. Search recents and Composer drafts include User IDs but survive session end. I will add one storage primitive and remove legacy caches without a verified owner. The push-only service worker has no fetch handler. Decision: unowned legacy browser data is discarded instead of assigned to the next signed-in User. The server will bind the non-secret boot hint to the current session on document requests, so a revoked session cannot paint its previous picture before the client sees a 401.
Author
Owner

#555 findings and implementation update

The shared storage tests pass (97 focused tests, including the unchanged background and Composer assertions). The raw-storage check now covers localStorage, sessionStorage, IndexedDB and Cache Storage. Its exceptions are the synchronous tagged boot reader, Installation UUID, collapsed chrome, analytics number format, random caret seed and anonymous Public-link passwords. Edge-resize widths, tag collapse state and folder expansion are scoped to the User.

Money kept its previous User's budgets and accounts in a singleton after sign-out. I added a reset and request generation guards. Appearance also held image URLs and readability samples; reset now clears both maps and invalidates pending image decode.

The push-only worker has no fetch handler. The existing #535 thumbnail policy remains private. No Mail row cache exists on this branch's base; job/reload-423 must use this module for its Mail, Money and warm-route caches.

Decision: the storage facade keeps the existing injectable Storage seams, while production values have both a User namespace and an owner tag. Async backends use one IndexedDB database and one Cache Storage namespace per User. Session cleanup removes synchronous values before its first await and waits for async deletion before navigation.

#555 findings and implementation update The shared storage tests pass (97 focused tests, including the unchanged background and Composer assertions). The raw-storage check now covers localStorage, sessionStorage, IndexedDB and Cache Storage. Its exceptions are the synchronous tagged boot reader, Installation UUID, collapsed chrome, analytics number format, random caret seed and anonymous Public-link passwords. Edge-resize widths, tag collapse state and folder expansion are scoped to the User. Money kept its previous User's budgets and accounts in a singleton after sign-out. I added a reset and request generation guards. Appearance also held image URLs and readability samples; reset now clears both maps and invalidates pending image decode. The push-only worker has no fetch handler. The existing #535 thumbnail policy remains private. No Mail row cache exists on this branch's base; job/reload-423 must use this module for its Mail, Money and warm-route caches. Decision: the storage facade keeps the existing injectable Storage seams, while production values have both a User namespace and an owner tag. Async backends use one IndexedDB database and one Cache Storage namespace per User. Session cleanup removes synchronous values before its first await and waits for async deletion before navigation.
Author
Owner

#555 and #535 proof update

The two-User browser flow passed explicit sign-out, a real revoked-session 401 in the mounted app, and a document load with a revoked cookie. A had a real uploaded background picture and Money Budget. All four cache backends were populated. B's first paint, DOM and storage dumps contained no A picture, text or namespace. Mail, Money and Appearance screenshots cover 390, 820 and 1440 px in light and dark.

The #535 positive proof passed 30 navigation cycles with a throttled network and cold reload. All 1,592 sampled frames painted the picture. The first frame painted the inline preview before Appearance resolved. The frame strip is retained for attachment.

Final git fetch origin && git merge origin/dev was run once: Already up to date. The web tests report Test Files 143 passed (143) and Tests 930 passed (930). Per-crate Rust gates are still running.

Performance: the locked perf-test VM used the shared release server without compilation. At 50 keys, read p50/p95 was 0.4/2.7 ms and cleanup was 1.1/1.7 ms. At 500 keys, read p50/p95 was 3.4/4.4 ms and cleanup was 6.8/11.6 ms. Browser CPU was 1.04 s and 2.37 s per phase; final RSS was 489.5 and 517.3 MiB. There is no comparable User storage profile in baseline.json. This profile establishes it.

Final comment review found a physical-key mismatch in the Composer storage-event filter. The correction and regression are committed. Owner-keyed legacy drafts are retained for the verified current User; unowned previews are discarded.

#555 and #535 proof update The two-User browser flow passed explicit sign-out, a real revoked-session 401 in the mounted app, and a document load with a revoked cookie. A had a real uploaded background picture and Money Budget. All four cache backends were populated. B's first paint, DOM and storage dumps contained no A picture, text or namespace. Mail, Money and Appearance screenshots cover 390, 820 and 1440 px in light and dark. The #535 positive proof passed 30 navigation cycles with a throttled network and cold reload. All 1,592 sampled frames painted the picture. The first frame painted the inline preview before Appearance resolved. The frame strip is retained for attachment. Final `git fetch origin && git merge origin/dev` was run once: `Already up to date.` The web tests report `Test Files 143 passed (143)` and `Tests 930 passed (930)`. Per-crate Rust gates are still running. Performance: the locked perf-test VM used the shared release server without compilation. At 50 keys, read p50/p95 was 0.4/2.7 ms and cleanup was 1.1/1.7 ms. At 500 keys, read p50/p95 was 3.4/4.4 ms and cleanup was 6.8/11.6 ms. Browser CPU was 1.04 s and 2.37 s per phase; final RSS was 489.5 and 517.3 MiB. There is no comparable User storage profile in baseline.json. This profile establishes it. Final comment review found a physical-key mismatch in the Composer storage-event filter. The correction and regression are committed. Owner-keyed legacy drafts are retained for the verified current User; unowned previews are discarded.
Author
Owner

#555 frame finding and fix

The stricter session-end sampler found two frames (47 and 48) with A's already-derived Money page text while IndexedDB/Cache Storage deletion was pending. The picture and stored values were already cleared. The old page remained mounted until the awaited cleanup finished and navigation began.

The root layout now uses flushSync to unmount the private page, header, sidebar and overlays as soon as the session-ended event arrives. It clears pending confirmations and toast actions. It shows neutral paper while deletion finishes, then navigates. The frame sampler records violations before pagehide so a later navigation cannot hide a transient failure. The test fixture no longer registers duplicate cleanup listeners that could mask a production integration bug.

#555 frame finding and fix The stricter session-end sampler found two frames (47 and 48) with A's already-derived Money page text while IndexedDB/Cache Storage deletion was pending. The picture and stored values were already cleared. The old page remained mounted until the awaited cleanup finished and navigation began. The root layout now uses `flushSync` to unmount the private page, header, sidebar and overlays as soon as the session-ended event arrives. It clears pending confirmations and toast actions. It shows neutral paper while deletion finishes, then navigates. The frame sampler records violations before pagehide so a later navigation cannot hide a transient failure. The test fixture no longer registers duplicate cleanup listeners that could mask a production integration bug.
Author
Owner

Completed #555 and the #535 cache integration on job/bg-flash.
Head: 88325a89cfe90d4a467cd8575d62eee5d703a4ae. Base: 15e17aeafc8ea160c109e62fba57f6961c39d21f.
The required single fetch/merge of origin/dev returned Already up to date.
No push, deploy or other merge ran. Neither issue is closed.

Built

  • Shared User storage for localStorage, sessionStorage, IndexedDB and Cache Storage. Keys identify the User; local/session values also carry an owner tag. Captured adapters and async operations refuse a different document User. Cleanup waits for all four backends.
  • Migrated every private browser key found on dev: Appearance preview/pictures/fonts, settings, Calendar preferences, panel widths, Files/Notes/Photos choices, Composer drafts/calendar, Money Budget selection, search recents, reminder history and push preferences. The AST reuse gate permits only documented device or anonymous Public-link exceptions.
  • The server writes the non-secret hint from the resolved session on document responses. Expired/anonymous documents remove it before boot. The inline reader paints paper for absent/mismatched hints or tags.
  • Sign-out, private API 401 and User switches clear private state and the hint. Private DOM unmounts synchronously before async deletion. Navigation opens a fresh document so Note/search/upload singletons cannot reach the next session. Other tabs reload on a session revision. Request epochs ignore late old-session 401 replies. Private streams/uploads share the expiry hook.
  • #535 boot rendering remains instant. Private thumbnails retain private caching. The push service worker has no fetch handler and does not cache private responses.
  • Added owner-tag/upgrade/late-reply tests, two-User production-browser regressions, a #331 matrix appendix and a hot-path benchmark.

Evidence

The stricter frame sampler found two frames of the previous Budget title while async deletion waited. Synchronous page removal fixes this. The fixture no longer installs duplicate cleanup listeners: the real app owns cleanup.
All assertions stayed intact. The revoked-document fixture starts rejection in the new document; the previous document has not received a 401. Sign-in screenshots wait for the real form.

All three cases passed: explicit sign-out, live revoked-session 401 and revoked-cookie document boot. Each uses two real Users in one context. A uploads a picture, opens real Mail and creates a real Money Budget. Test-only values populate the four backends. Every rAF is sampled; DOM and storage are checked after B first paints and on Mail, Money and Appearance.

#555 has 24 sign-in/Mail/Money/Appearance screenshots: 390, 820 and 1440 px, light and dark. #535 has 18 Files/Photos/Appearance screenshots and a frame strip. Claude owns visual review; screenshots are not committed.
The throttled/cold #535 navigation audit sampled 1,592 frames. Every sampled frame painted the selected picture. The first frame painted the boot picture before Appearance resolved.

Gate output (verbatim summaries)

cargo fmt --check exited 0 with no output.

cargo clippy -p calternal-plugin-files --all-targets -- -D warnings:

    Finished `dev` profile [unoptimized + debuginfo] target(s) in 7m 01s

cargo test -p calternal-plugin-files:

test result: ok. 136 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 162.86s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

cargo clippy -p calternal-server --all-targets -- -D warnings:

    Finished `dev` profile [unoptimized + debuginfo] target(s) in 49.06s

cargo test -p calternal-server:

test result: ok. 94 passed; 0 failed; 3 ignored; 0 measured; 0 filtered out; finished in 22.22s

bun run check:

User browser caches use userStorage; only documented device/public-link exceptions remain.
svelte-check found 0 errors and 0 warnings

bun run test:

 Test Files  143 passed (143)
      Tests  930 passed (930)
   Duration  258.99s (transform 52%, environment 19%, import 15%, tests 10%, setup 3%)

bun run test src/lib/userStorage.svelte.test.ts (final async guard regression):

 Test Files  1 passed (1)
      Tests  9 passed (9)
   Duration  17.20s (transform 51%, environment 31%, tests 11%, setup 6%, import 1%)

API-client tests:

 9 pass
 0 fail
 31 expect() calls
Ran 9 tests across 1 file. [910.00ms]

bun run build:

✓ built in 1m 25s
✓ built in 105ms
✓ built in 2m 48s

The full web run passed 930 tests. The final async owner guard then added one regression; its targeted file passed nine tests. Rust gates used four build jobs, no incremental build and line-table debug info, per crate only. Full logs remain in artifacts/bg-555/*-final.log.

Corrected browser output:

PASS #555 signout: no A frame, DOM content or storage survives into B
PASS #555 expired-live: no A frame, DOM content or storage survives into B
PASS #555 expired-document: no A frame, DOM content or storage survives into B

The bounded isolation round covered this session failure class. No broad API attack suite ran.

Performance

bench/user-storage.mjs ran once on the perf VM under /root/perf.lock, with the shared release server. No compile ran there. Load inside the lock was 0.21/0.24/0.43 initially and 0.11/0.20/0.39 after setup.

Keys Read p50/p95 Write p50/p95 Clear p50/p95 Eight writers Browser CPU Browser RSS
50 0.4/2.7 ms 1.0/15.1 ms 1.1/1.7 ms 5.7 ms 1.04 s 489.5 MiB
500 3.4/4.4 ms 9.1/22.8 ms 6.8/11.6 ms 6.1 ms 2.37 s 517.3 MiB

docs/perf/baseline.json has no equivalent profile. This starts that baseline; no valid regression ratio exists. The attached #555 archive includes raw counters. docs/perf/user-storage-555.md records the run.

Known gaps

  • #423's persistent Mail rows, Money cache and warm route caches are not on this dev base. Real Mail's empty state and backend test values are covered here. #423 must adopt this module and test actual cached rows. Its branch was not edited.
  • Browser evidence is Chromium. Claude must review the screenshots. No o2 deploy was authorized.
  • An idle remote tab learns revocation on its next private 401 or document request. The hint never authorizes access.

Decisions

  • Drop legacy unowned values. Migrate only owner-keyed drafts/recents/reminder history for the resolved current User.
  • Keep documented non-personal device scalars and anonymous per-link tab passwords outside the User namespace. Panel widths are User preferences.
  • Set the hint on HTML responses rather than all API replies, so late replies cannot replace a new hint.
  • Open a fresh document at session end/switch after deletion. Remove old private DOM synchronously during the wait.
  • Use one IndexedDB database and one Cache Storage cache per User. Keep Storage-compatible seams, plus async methods for future caches.

Files

apps/web/e2e/background-stability-535.mjs
apps/web/e2e/harness.mjs
apps/web/e2e/settings-effects.mjs
apps/web/e2e/user-storage-555.mjs
apps/web/e2e/user-storage-fixtures.mjs
apps/web/package.json
apps/web/scripts/check-user-storage.mjs
apps/web/src/app.html
apps/web/src/lib/actions/edgeResize.ts
apps/web/src/lib/appearance/background-store.test.ts
apps/web/src/lib/appearance/background.svelte.ts
apps/web/src/lib/auth/components/CreateAccountFlow.svelte
apps/web/src/lib/auth/components/RecoverFlow.svelte
apps/web/src/lib/auth/session-expiry.svelte.test.ts
apps/web/src/lib/calendar/prefs.ts
apps/web/src/lib/components/app-sidebar.svelte
apps/web/src/lib/components/search-dialog.svelte
apps/web/src/lib/composer/Composer.svelte
apps/web/src/lib/composer/drafts.svelte.ts
apps/web/src/lib/composer/drafts.test.ts
apps/web/src/lib/composer/nlp.ts
apps/web/src/lib/editor/format/reminderTimeHistory.ts
apps/web/src/lib/files/api.ts
apps/web/src/lib/files/prefs.svelte.ts
apps/web/src/lib/files/uploads.svelte.ts
apps/web/src/lib/mail/attachments.ts
apps/web/src/lib/money/store.svelte.ts
apps/web/src/lib/notes/NoteView.svelte
apps/web/src/lib/notes/NotesExplorer.svelte
apps/web/src/lib/notifications/push.ts
apps/web/src/lib/photos/PhotosView.svelte
apps/web/src/lib/search/recent.ts
apps/web/src/lib/stores/settings-store.ts
apps/web/src/lib/styles/background.css
apps/web/src/lib/themes.test.ts
apps/web/src/lib/userStorage-boot.test.ts
apps/web/src/lib/userStorage.svelte.test.ts
apps/web/src/lib/userStorage.ts
apps/web/src/routes/+layout.svelte
apps/web/src/routes/login/+page.svelte
apps/web/src/routes/settings/admin/BackupsGroup.svelte
apps/web/src/routes/settings/admin/ConfigGroup.svelte
apps/web/src/routes/settings/appearance/BackgroundGroup.svelte
bench/appearance.mjs
bench/user-storage.mjs
contracts/openapi.json
crates/calternal-server/src/wire.rs
crates/plugins/files/src/lib.rs
crates/plugins/files/src/thumbnails.rs
docs/audits/browser-user-storage-555.md
docs/audits/cross-user-2026-09-28.md
docs/perf/user-storage-555.md
packages/api-client/src/generated.ts
packages/api-client/src/index.ts
tests/adversarial/hostile_bytes.mjs
tests/adversarial/run.sh
Completed #555 and the #535 cache integration on `job/bg-flash`. Head: `88325a89cfe90d4a467cd8575d62eee5d703a4ae`. Base: `15e17aeafc8ea160c109e62fba57f6961c39d21f`. The required single fetch/merge of `origin/dev` returned `Already up to date.` No push, deploy or other merge ran. Neither issue is closed. ## Built - Shared User storage for localStorage, sessionStorage, IndexedDB and Cache Storage. Keys identify the User; local/session values also carry an owner tag. Captured adapters and async operations refuse a different document User. Cleanup waits for all four backends. - Migrated every private browser key found on dev: Appearance preview/pictures/fonts, settings, Calendar preferences, panel widths, Files/Notes/Photos choices, Composer drafts/calendar, Money Budget selection, search recents, reminder history and push preferences. The AST reuse gate permits only documented device or anonymous Public-link exceptions. - The server writes the non-secret hint from the resolved session on document responses. Expired/anonymous documents remove it before boot. The inline reader paints paper for absent/mismatched hints or tags. - Sign-out, private API 401 and User switches clear private state and the hint. Private DOM unmounts synchronously before async deletion. Navigation opens a fresh document so Note/search/upload singletons cannot reach the next session. Other tabs reload on a session revision. Request epochs ignore late old-session 401 replies. Private streams/uploads share the expiry hook. - #535 boot rendering remains instant. Private thumbnails retain private caching. The push service worker has no fetch handler and does not cache private responses. - Added owner-tag/upgrade/late-reply tests, two-User production-browser regressions, a #331 matrix appendix and a hot-path benchmark. ## Evidence The stricter frame sampler found two frames of the previous Budget title while async deletion waited. Synchronous page removal fixes this. The fixture no longer installs duplicate cleanup listeners: the real app owns cleanup. All assertions stayed intact. The revoked-document fixture starts rejection in the new document; the previous document has not received a 401. Sign-in screenshots wait for the real form. All three cases passed: explicit sign-out, live revoked-session 401 and revoked-cookie document boot. Each uses two real Users in one context. A uploads a picture, opens real Mail and creates a real Money Budget. Test-only values populate the four backends. Every rAF is sampled; DOM and storage are checked after B first paints and on Mail, Money and Appearance. #555 has 24 sign-in/Mail/Money/Appearance screenshots: 390, 820 and 1440 px, light and dark. #535 has 18 Files/Photos/Appearance screenshots and a frame strip. Claude owns visual review; screenshots are not committed. The throttled/cold #535 navigation audit sampled 1,592 frames. Every sampled frame painted the selected picture. The first frame painted the boot picture before Appearance resolved. - [review-555.zip](https://git.kayg.org/attachments/4015dd44-6938-4fe7-94ef-f3798f94955b) (#555) - [review-535.zip](https://git.kayg.org/attachments/6c2e937f-6fdb-4043-b9ac-ca0fac2101b5) (#535) - [appearance-b-dark-1440.png](https://git.kayg.org/attachments/1851f9a4-25c7-4946-ad7a-73de2d17ccbd) (#555) - [mail-b-light-390.png](https://git.kayg.org/attachments/f3b09630-7d08-42a6-b140-ea45f1f33587) (#555) - [navigation-frame-strip.png](https://git.kayg.org/attachments/924e43c2-6991-4785-b82b-82b80d307754) (#535) ## Gate output (verbatim summaries) `cargo fmt --check` exited 0 with no output. `cargo clippy -p calternal-plugin-files --all-targets -- -D warnings`: ``` Finished `dev` profile [unoptimized + debuginfo] target(s) in 7m 01s ``` `cargo test -p calternal-plugin-files`: ``` test result: ok. 136 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 162.86s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s ``` `cargo clippy -p calternal-server --all-targets -- -D warnings`: ``` Finished `dev` profile [unoptimized + debuginfo] target(s) in 49.06s ``` `cargo test -p calternal-server`: ``` test result: ok. 94 passed; 0 failed; 3 ignored; 0 measured; 0 filtered out; finished in 22.22s ``` `bun run check`: ``` User browser caches use userStorage; only documented device/public-link exceptions remain. svelte-check found 0 errors and 0 warnings ``` `bun run test`: ``` Test Files 143 passed (143) Tests 930 passed (930) Duration 258.99s (transform 52%, environment 19%, import 15%, tests 10%, setup 3%) ``` `bun run test src/lib/userStorage.svelte.test.ts (final async guard regression)`: ``` Test Files 1 passed (1) Tests 9 passed (9) Duration 17.20s (transform 51%, environment 31%, tests 11%, setup 6%, import 1%) ``` `API-client tests`: ``` 9 pass 0 fail 31 expect() calls Ran 9 tests across 1 file. [910.00ms] ``` `bun run build`: ``` ✓ built in 1m 25s ✓ built in 105ms ✓ built in 2m 48s ``` The full web run passed 930 tests. The final async owner guard then added one regression; its targeted file passed nine tests. Rust gates used four build jobs, no incremental build and line-table debug info, per crate only. Full logs remain in `artifacts/bg-555/*-final.log`. Corrected browser output: ``` PASS #555 signout: no A frame, DOM content or storage survives into B PASS #555 expired-live: no A frame, DOM content or storage survives into B PASS #555 expired-document: no A frame, DOM content or storage survives into B ``` The bounded isolation round covered this session failure class. No broad API attack suite ran. ## Performance `bench/user-storage.mjs` ran once on the perf VM under `/root/perf.lock`, with the shared release server. No compile ran there. Load inside the lock was 0.21/0.24/0.43 initially and 0.11/0.20/0.39 after setup. | Keys | Read p50/p95 | Write p50/p95 | Clear p50/p95 | Eight writers | Browser CPU | Browser RSS | | --- | --- | --- | --- | --- | --- | --- | | 50 | 0.4/2.7 ms | 1.0/15.1 ms | 1.1/1.7 ms | 5.7 ms | 1.04 s | 489.5 MiB | | 500 | 3.4/4.4 ms | 9.1/22.8 ms | 6.8/11.6 ms | 6.1 ms | 2.37 s | 517.3 MiB | `docs/perf/baseline.json` has no equivalent profile. This starts that baseline; no valid regression ratio exists. The attached #555 archive includes raw counters. `docs/perf/user-storage-555.md` records the run. ## Known gaps - #423's persistent Mail rows, Money cache and warm route caches are not on this dev base. Real Mail's empty state and backend test values are covered here. #423 must adopt this module and test actual cached rows. Its branch was not edited. - Browser evidence is Chromium. Claude must review the screenshots. No o2 deploy was authorized. - An idle remote tab learns revocation on its next private 401 or document request. The hint never authorizes access. ## Decisions - Drop legacy unowned values. Migrate only owner-keyed drafts/recents/reminder history for the resolved current User. - Keep documented non-personal device scalars and anonymous per-link tab passwords outside the User namespace. Panel widths are User preferences. - Set the hint on HTML responses rather than all API replies, so late replies cannot replace a new hint. - Open a fresh document at session end/switch after deletion. Remove old private DOM synchronously during the wait. - Use one IndexedDB database and one Cache Storage cache per User. Keep Storage-compatible seams, plus async methods for future caches. ## Files ``` apps/web/e2e/background-stability-535.mjs apps/web/e2e/harness.mjs apps/web/e2e/settings-effects.mjs apps/web/e2e/user-storage-555.mjs apps/web/e2e/user-storage-fixtures.mjs apps/web/package.json apps/web/scripts/check-user-storage.mjs apps/web/src/app.html apps/web/src/lib/actions/edgeResize.ts apps/web/src/lib/appearance/background-store.test.ts apps/web/src/lib/appearance/background.svelte.ts apps/web/src/lib/auth/components/CreateAccountFlow.svelte apps/web/src/lib/auth/components/RecoverFlow.svelte apps/web/src/lib/auth/session-expiry.svelte.test.ts apps/web/src/lib/calendar/prefs.ts apps/web/src/lib/components/app-sidebar.svelte apps/web/src/lib/components/search-dialog.svelte apps/web/src/lib/composer/Composer.svelte apps/web/src/lib/composer/drafts.svelte.ts apps/web/src/lib/composer/drafts.test.ts apps/web/src/lib/composer/nlp.ts apps/web/src/lib/editor/format/reminderTimeHistory.ts apps/web/src/lib/files/api.ts apps/web/src/lib/files/prefs.svelte.ts apps/web/src/lib/files/uploads.svelte.ts apps/web/src/lib/mail/attachments.ts apps/web/src/lib/money/store.svelte.ts apps/web/src/lib/notes/NoteView.svelte apps/web/src/lib/notes/NotesExplorer.svelte apps/web/src/lib/notifications/push.ts apps/web/src/lib/photos/PhotosView.svelte apps/web/src/lib/search/recent.ts apps/web/src/lib/stores/settings-store.ts apps/web/src/lib/styles/background.css apps/web/src/lib/themes.test.ts apps/web/src/lib/userStorage-boot.test.ts apps/web/src/lib/userStorage.svelte.test.ts apps/web/src/lib/userStorage.ts apps/web/src/routes/+layout.svelte apps/web/src/routes/login/+page.svelte apps/web/src/routes/settings/admin/BackupsGroup.svelte apps/web/src/routes/settings/admin/ConfigGroup.svelte apps/web/src/routes/settings/appearance/BackgroundGroup.svelte bench/appearance.mjs bench/user-storage.mjs contracts/openapi.json crates/calternal-server/src/wire.rs crates/plugins/files/src/lib.rs crates/plugins/files/src/thumbnails.rs docs/audits/browser-user-storage-555.md docs/audits/cross-user-2026-09-28.md docs/perf/user-storage-555.md packages/api-client/src/generated.ts packages/api-client/src/index.ts tests/adversarial/hostile_bytes.mjs tests/adversarial/run.sh ```
Author
Owner

Final cleanup completed at 88325a89cfe90d4a467cd8575d62eee5d703a4ae. cargo clean exited 0:

     Removed 15956 files, 8.4GiB total

Production web output and job temporary files were removed. Screenshots remain as issue attachments and local review artifacts. The branch is clean.

Final cleanup completed at `88325a89cfe90d4a467cd8575d62eee5d703a4ae`. `cargo clean` exited 0: ``` Removed 15956 files, 8.4GiB total ``` Production web output and job temporary files were removed. Screenshots remain as issue attachments and local review artifacts. The branch is clean.
Author
Owner

Shipped in merge round 4, deployed to calternal.cloud in 1af8ead26 (healthy).

Shipped in merge round 4, deployed to calternal.cloud in 1af8ead26 (healthy).
kayg closed this issue 2026-10-01 09:17:52 +00:00
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
kayg/calternal#555
No description provided.