this repo has no description
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Wipe decrypted-plaintext caches on cabinet teardown and preview switch

The module-level `decryptCache` in FilePreview and `readmeCache` in
DirectoryReadme hold decrypted document bytes keyed by AT-URI. Both
exist for Suspense stability across unmount/remount — correct, but
also the reason eviction needed explicit wiring. It never got it.

Concrete leaks the fix closes:

- Open preview for file A, open preview for file B without closing A
first: cache[A] lingers. Now `handlePreview` evicts the prior URI
before setting the new one.
- Navigate out of FileView: the active preview's cache entry lingers
across the unmount. New unmount effect evicts the current URI.
- Logout / session transition without a full page reload: auth.ts
nulls opakeInstance and OpakeProvider.wipeState() runs, but the
WASM-only wipe can't reach JS heap maps. New user's session sees
the previous user's plaintext under document URIs still in memory
(typed into devtools or a router prefetch with the wrong DID).

Fix: add `clearPreviewCache()` (matching `evictAllReadmeCaches` which
already existed unused). Call both from a CabinetLayout unmount
effect — that's the auth-gate boundary; a route change out of
`/cabinet/*` is the same event as provider unmount and the cleanest
hook point for "all decrypted material goes away now."

Does not address the related Rust-side issue that `HeldTree`'s
cabinet X25519 private key doesn't zeroize on drop — that's a
separate commit (requires RedactedDebug on the cabinet key type).

+44 -1
+11
apps/web/src/components/cabinet/FilePreview.tsx
··· 55 55 decryptCache.delete(cacheKey); 56 56 } 57 57 58 + /** 59 + * Drop every cached preview. Call on logout / account switch — the cache 60 + * holds decrypted plaintext keyed by document URI, and one account's 61 + * plaintext must not linger on the JS heap while another account is 62 + * active in the same tab. 63 + */ 64 + export function clearPreviewCache(): void { 65 + // eslint-disable-next-line functional/immutable-data -- module-level cache cleanup 66 + decryptCache.clear(); 67 + } 68 + 58 69 async function run(decrypt: () => Promise<DecryptedBlob>): Promise<DecryptResult> { 59 70 try { 60 71 const blob = await decrypt();
+18 -1
apps/web/src/components/cabinet/FileView.tsx
··· 250 250 ); 251 251 252 252 const handlePreview = useCallback((item: FileItem) => { 253 - setPreviewUri(item.uri); 253 + // Evict the previous preview's decrypted plaintext before overwriting 254 + // the URI. Without this, opening a sequence of files leaves one cache 255 + // entry per file in the module-level Map until the user closes the 256 + // pane — decrypted bytes accumulate on the JS heap. 257 + setPreviewUri((prev) => { 258 + if (prev && prev !== item.uri) evictPreviewCache(prev); 259 + return item.uri; 260 + }); 254 261 }, []); 255 262 256 263 const handleClosePreview = useCallback(() => { ··· 259 266 return null; 260 267 }); 261 268 }, []); 269 + 270 + // Flush the currently-shown preview's cache on unmount so a route 271 + // change away from the file browser doesn't leave decrypted bytes 272 + // behind under the previous URI. 273 + useEffect( 274 + () => () => { 275 + if (previewUri) evictPreviewCache(previewUri); 276 + }, 277 + [previewUri], 278 + ); 262 279 263 280 // Decrypt thunk for the current preview. Stable per (fileManager, previewUri, 264 281 // metadata snapshot) so FilePreview's Suspense-cached promise stays valid.
+15
apps/web/src/routes/cabinet/route.lazy.tsx
··· 7 7 CreateWorkspaceDialog, 8 8 type CreateWorkspaceDialogHandle, 9 9 } from "@/components/cabinet/CreateWorkspaceDialog"; 10 + import { clearPreviewCache } from "@/components/cabinet/FilePreview"; 11 + import { evictAllReadmeCaches } from "@/components/cabinet/DirectoryReadme"; 10 12 import { getOpake, useAuthStore } from "@/stores/auth"; 11 13 import { taskStore } from "@/stores/tasks"; 12 14 import { loading } from "@/stores/app"; ··· 26 28 // calls startSseConsumer on mount and stopSseConsumer (including 27 29 // `TreeKeeper::uninstall_all`) on unmount so the previous user's 28 30 // `ContentKey`s / decrypted names don't linger across login. 31 + 32 + // JS-side decrypted-plaintext caches (preview + readme Suspense maps) 33 + // live at module scope in their respective components so they survive 34 + // Suspense unmount/remount cycles. WASM's wipeState clears the keepers 35 + // but can't reach these — drain them here when the cabinet layout 36 + // tears down (logout, session switch, auth-gate redirect). 37 + useEffect( 38 + () => () => { 39 + clearPreviewCache(); 40 + evictAllReadmeCaches(); 41 + }, 42 + [], 43 + ); 29 44 30 45 // Background daemon — timer polling for maintenance tasks only. 31 46 useEffect(() => {