Carries the still-applicable findings from the PR #116 review into PR #120, since #120 includes the mobile-bootstrap commit. CORS hardening (removing the unconditional localhost/capacitor origins) is intentionally left out of scope. Service worker routing (latent bug fix + testability): - vite.config.ts: anchor Workbox path matching to a segment boundary (^/<seg>(/|$)) instead of startsWith, so siblings like /apidocs, /collaborators, /socket.iox are no longer mis-routed as API/realtime and forced NetworkOnly; align navigateFallbackDenylist with the same anchors. - new apps/client/src/pwa/sw-strategy.ts holds the canonical predicates (isApiPath, isCollabOrSocketPath) + unit tests; the vite.config regexes mirror it inline (Workbox generateSW serializes urlPattern fns standalone, so they cannot import the module). Server CORS (R1 extraction + coverage): - extract buildCorsAllowlist / isOriginAllowed into cors.util.ts with unit tests (evil-origin rejected, WebView/no-Origin allowed); main.ts rewired to use them with byte-for-byte identical behavior. Privacy — clear offline cache on logout: - new clear-offline-cache.ts purges the persisted query cache (idb-keyval gitmost-rq-cache), the Yjs page.* IndexedDB databases, and the service-worker api-get-cache; wired into handleLogout (best-effort, before the redirect) so a previous user's private data does not linger locally. Conventions & docs: - prettier fixes on main.ts and login.dto.ts. - CHANGELOG: document offline reading, returnToken opt-in, optional Swagger, new env vars, logout cache-clear, and the CORS open->allowlist breaking change. - docs/mobile-app-plan.md: correct the now-false §2.4 claims and update the §12 checklist (native cap add ios left unchecked — generated locally, gitignored). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
91 lines
3.3 KiB
TypeScript
91 lines
3.3 KiB
TypeScript
import { del } from "idb-keyval";
|
|
|
|
import { queryClient } from "@/main.tsx";
|
|
import { OFFLINE_CACHE_KEY } from "./query-persister";
|
|
|
|
/**
|
|
* Best-effort purge of all of the current user's offline data from the browser.
|
|
*
|
|
* On logout the previous user's private data would otherwise linger locally and
|
|
* be readable by the next person on the device. This clears the three offline
|
|
* stores the app writes:
|
|
* 1. the in-memory + IndexedDB-persisted TanStack Query cache (idb-keyval key
|
|
* `OFFLINE_CACHE_KEY`),
|
|
* 2. the Yjs page documents (IndexedDB databases named `page.<id>` created by
|
|
* y-indexeddb in make-offline.ts), and
|
|
* 3. the service worker `api-get-cache` Cache Storage entry (private GET /api
|
|
* responses cached by the Workbox runtime).
|
|
*
|
|
* Fully best-effort: every step is isolated so a single failure neither blocks
|
|
* the remaining steps nor throws to the caller (logout must never be blocked on
|
|
* cache cleanup). Callers may ignore the resolved value.
|
|
*
|
|
* Limitations:
|
|
* - Deleting the Yjs page databases relies on `indexedDB.databases()`, which
|
|
* is unavailable in some browsers (notably Firefox). There we skip silently;
|
|
* those `page.<id>` databases are then left in place.
|
|
* - Cache Storage clearing only runs where `caches` exists (secure contexts /
|
|
* service-worker-capable browsers).
|
|
*/
|
|
export async function clearOfflineCache(): Promise<void> {
|
|
// 1a. Drop the in-memory query cache immediately.
|
|
try {
|
|
queryClient.clear();
|
|
} catch {
|
|
// best-effort: ignore in-memory cache reset failures
|
|
}
|
|
|
|
// 1b. Delete the persisted RQ cache from IndexedDB.
|
|
try {
|
|
await del(OFFLINE_CACHE_KEY);
|
|
} catch {
|
|
// best-effort: ignore persisted-cache deletion failures
|
|
}
|
|
|
|
// 2. Delete the Yjs page IndexedDB databases (`page.<id>`).
|
|
// `indexedDB.databases()` is not implemented everywhere (e.g. Firefox); when
|
|
// it is missing we cannot enumerate the page databases, so we skip silently.
|
|
try {
|
|
if (
|
|
typeof indexedDB !== "undefined" &&
|
|
typeof indexedDB.databases === "function"
|
|
) {
|
|
const dbs = await indexedDB.databases();
|
|
for (const db of dbs) {
|
|
const name = db?.name;
|
|
if (typeof name !== "string" || !name.startsWith("page.")) continue;
|
|
try {
|
|
// Fire-and-forget delete; await a thin wrapper so a slow delete does
|
|
// not race the page teardown, but never reject on it.
|
|
await new Promise<void>((resolve) => {
|
|
const request = indexedDB.deleteDatabase(name);
|
|
request.onsuccess = () => resolve();
|
|
request.onerror = () => resolve();
|
|
request.onblocked = () => resolve();
|
|
});
|
|
} catch {
|
|
// best-effort per database
|
|
}
|
|
}
|
|
}
|
|
} catch {
|
|
// best-effort: ignore enumeration/deletion failures
|
|
}
|
|
|
|
// 3. Clear the service worker API cache (private GET /api responses). The
|
|
// Workbox runtime cache name contains "api-get-cache" (Workbox may prefix it),
|
|
// so match by substring rather than exact name.
|
|
try {
|
|
if ("caches" in window) {
|
|
const keys = await caches.keys();
|
|
await Promise.all(
|
|
keys
|
|
.filter((key) => key.includes("api-get-cache"))
|
|
.map((key) => caches.delete(key)),
|
|
);
|
|
}
|
|
} catch {
|
|
// best-effort: ignore Cache Storage failures
|
|
}
|
|
}
|