CI / Build & Quality Checks (push) Successful in 1m35s
CI / Docker image build & smoke test (push) Skipped
CI / Secret scan (gitleaks) (push) Successful in 10s
CI / Trigger Desktop Build (push) Successful in 10s
CI / Playwright smoke (e2e) (push) Successful in 3m23s
Of the two plaintext-localStorage items in #204, composer drafts were already swept on logout (#41); the presence status message + expiry were deliberately kept. They are PII with an authoritative copy in server presence, so sweep them too. The test's localStorage mock now enumerates keys like the real Storage object, so the prefix sweeps (drafts, status) are actually exercised — the old 'draft preserved' assertion only passed because Object.keys() saw nothing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
98 lines
3.7 KiB
TypeScript
98 lines
3.7 KiB
TypeScript
import { clearTranslationCache } from './translation';
|
|
import { clearScheduledMessages } from './scheduledMessages';
|
|
import { clearRecentSearches } from './recentSearches';
|
|
import { clearRecentForwardTargets } from './recentForwardTargets';
|
|
import { clearRecentGifs } from './recentGifs';
|
|
import { clearRecentStickers } from './recentStickers';
|
|
import { clearNavToActivePathStore } from './navToActivePath';
|
|
import { DRAFT_MSG_KEY_PREFIX } from '../utils/draft';
|
|
|
|
/**
|
|
* [Gitea #41] Wipe every persisted composer draft (`draft-msg-<roomId>`). Drafts
|
|
* hold decrypted, unsent message text with no user scoping, so leaving them in
|
|
* place across logout lets the next account on this device see (and send) the
|
|
* previous user's draft the moment they open the same room.
|
|
*/
|
|
const clearMsgDrafts = (): void => {
|
|
let keys: string[];
|
|
try {
|
|
keys = Object.keys(localStorage);
|
|
} catch {
|
|
return;
|
|
}
|
|
keys.forEach((key) => {
|
|
if (key.startsWith(DRAFT_MSG_KEY_PREFIX)) {
|
|
try {
|
|
localStorage.removeItem(key);
|
|
} catch {
|
|
// Best-effort — a single unreadable/blocked key must not abort the sweep.
|
|
}
|
|
}
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Single auditable place that wipes the `localStorage` caches holding decrypted
|
|
* message content, sent media, or a user's messaging/nav activity. Called on
|
|
* logout so this residue can't survive on a shared device.
|
|
*
|
|
* Swept here:
|
|
* - `cinny_translation_cache_v1` — decrypted translated message text
|
|
* - `cinny_scheduled_messages_v1` — decrypted `IContent.body` of pending sends
|
|
* - `cinny_recent_searches_v1` — search query text (PII)
|
|
* - `cinny_recent_forward_targets_v1` — recent forward contact/room graph (PII)
|
|
* - `cinny_recent_gifs_v1` / `cinny_recent_stickers_v1` — media the user sent
|
|
* - `navToActivePath<userId>` — per-space last-visited room paths (needs userId)
|
|
* - `draft-msg-*` — unsent composer drafts (decrypted message text, unscoped by
|
|
* user — see [Gitea #41]; previously deliberately preserved across logout
|
|
* (N98), which let the next account on this device see/send a prior user's
|
|
* draft, so this is no longer a "by design" exemption)
|
|
*
|
|
* NOT swept here (by design):
|
|
* - session credential keys → `removeFallbackSession()`
|
|
* - the SDK sync/crypto store + all `io.lotus.*` account data (reminders,
|
|
* bookmarks, user notes, status presets — themselves plaintext) → wiped by
|
|
* `mx.clearStores()` on both logout paths
|
|
* - the opt-in encrypted-search index (IndexedDB) → `deleteSearchCacheDatabase()`
|
|
* - (the presence status message + expiry, `lotus-status-msg-*` /
|
|
* `lotus-status-expiry-*`, used to be preserved; since [Gitea #204] they are
|
|
* swept with the rest — the server-side presence status survives, so a
|
|
* re-login loses nothing)
|
|
* - low-sensitivity UI/metadata residue (`io.lotus.mute_timers`, collapsed
|
|
* nav/space categories, `cinny_oidc_dynamic_clients`) is treated as
|
|
* preferences, not swept here
|
|
*/
|
|
/**
|
|
* [Gitea #204] The local mirror of the user's status message (+ its expiry) is
|
|
* PII in plaintext; the authoritative copy lives in server presence.
|
|
*/
|
|
const clearStatusMessage = (): void => {
|
|
let keys: string[];
|
|
try {
|
|
keys = Object.keys(localStorage);
|
|
} catch {
|
|
return;
|
|
}
|
|
keys.forEach((key) => {
|
|
if (key.startsWith('lotus-status-msg-') || key.startsWith('lotus-status-expiry-')) {
|
|
try {
|
|
localStorage.removeItem(key);
|
|
} catch {
|
|
// best-effort
|
|
}
|
|
}
|
|
});
|
|
};
|
|
|
|
export const clearPlaintextCaches = (userId?: string): void => {
|
|
clearTranslationCache();
|
|
clearScheduledMessages();
|
|
clearRecentSearches();
|
|
clearRecentForwardTargets();
|
|
clearRecentGifs();
|
|
clearRecentStickers();
|
|
clearMsgDrafts();
|
|
clearStatusMessage();
|
|
if (userId) clearNavToActivePathStore(userId);
|
|
};
|