Closes BLOCKER B4 — three reinforcing paths could serve one account's
cached data to the next user on a shared device:
- The Workbox 'api-data' cache keyed trip/user-scoped GETs by URL only
(cookie-blind). Changed to NetworkOnly; offline reads come from the
per-user IndexedDB cache via the repo layer instead.
- IndexedDB had no per-user scoping. The Dexie connection is now scoped
per user (trek-offline-u<id>) behind a Proxy so the ~19 importers keep a
stable binding; login opens the user DB, logout deletes it and returns
to the anonymous DB.
- logout() was fire-and-forget and racy: background flush/syncAll could
re-seed the DB after the wipe. It is now async and ordered — close an
auth gate, unregister sync triggers, disconnect, clear caches, delete
the user DB — and flush()/syncAll() bail when the gate is closed.
**#541 — File downloads broken in PWA standalone mode**
Replace getAuthUrl + window.open pattern with blob-based fetch using
credentials:include. The old approach minted a 60s single-use ephemeral
token then called window.open, which handed the URL to the system browser
on Android/iOS — losing the PWA cookie jar and producing "invalid or
expired token". The new approach fetches the file directly inside the
PWA WebView as a blob URL, so no auth handoff occurs.
New helper client/src/utils/fileDownload.ts with downloadFile and openFile.
Updated FileManager, ReservationsPanel, ReservationModal, PlaceInspector,
CollabNotes.
Security hardening in fileDownload.ts:
- assertRelativeUrl() guard prevents credentials being sent to external hosts
- openFile() checks blob.type against a safe-inline allowlist; HTML, SVG and
other script-capable MIME types are forced to download instead of being
opened inline, preventing same-origin XSS via blob URLs
- resp.ok check covers all non-2xx responses, not just 401
**#505 — PWA offline session lost on reload**
Wrap authStore with Zustand persist middleware, serializing only
{user, isAuthenticated} to localStorage key trek_auth_snapshot.
maps_api_key is intentionally excluded from the snapshot.
On cold start with no network: persist hydrates isAuthenticated:true,
App.tsx clears isLoading and calls loadUser({silent:true}), ProtectedRoute
renders the dashboard immediately. The network error from loadUser leaves
isAuthenticated intact so no login redirect occurs.
On 401 or logout: store state is cleared, persist writes
{isAuthenticated:false} — stale snapshot does not grant offline access
after session expiry.
jsdom's FormData is incompatible with undici's ReadableStream serialisation
used by MSW 2.x — requests hang under CI resource constraints but pass locally.
Replace server.use() + implicit HTTP roundtrip with vi.spyOn().mockResolvedValueOnce()
for all five FormData POST tests (uploadAvatar, uploadRestore, addFile, importGpx).