Files
cinny/src/app/utils/searchCacheInvalidation.ts
T
jaredandClaude Opus 5 c8e49d3855 fix(security): persistent search index forgets redacted and left-room text
Nothing ever removed an indexed row: redacted messages stayed searchable
with full plaintext and rendered as normal results. Now: a client-level
RoomEvent.Redaction listener deletes the row, leave/ban clears the room
(clearRoom finally has a caller), m.replace edits upsert the original
row instead of indexing the "* fallback" separately, and cached rows
whose local event is redacted render through the existing
redacted_because placeholder. Unit-tested.

Fixes #14

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
2026-09-12 19:46:05 -04:00

44 lines
1.6 KiB
TypeScript

import { useEffect } from 'react';
import { useAtomValue } from 'jotai';
import { MatrixEvent, Room, RoomEvent } from 'matrix-js-sdk';
import { useMatrixClient } from '../hooks/useMatrixClient';
import { searchCacheEnabledAtom } from '../state/searchCacheEnabled';
import { clearRoom, deleteRow } from './searchCache';
/**
* Gitea #14 — the persistent search cache (`searchCache.ts`) had no
* invalidation path other than logout or the manual "Clear cached index"
* button, so redacted messages and rooms the user left kept their decrypted
* plaintext searchable on disk indefinitely.
*
* While the cache is enabled, listen client-wide for redactions (delete the
* redacted row) and for leaving/being banned from a room (wipe the room's
* cached rows via the existing `clearRoom`).
*/
export const useSearchCacheInvalidation = (): void => {
const mx = useMatrixClient();
const cacheEnabled = useAtomValue(searchCacheEnabledAtom);
useEffect(() => {
if (!cacheEnabled) return undefined;
const onRedaction = (event: MatrixEvent, room: Room) => {
const redactedEventId = event.getAssociatedId();
if (redactedEventId) deleteRow(room.roomId, redactedEventId);
};
const onMyMembership = (room: Room, membership: string) => {
if (membership === 'leave' || membership === 'ban') {
clearRoom(room.roomId);
}
};
mx.on(RoomEvent.Redaction, onRedaction);
mx.on(RoomEvent.MyMembership, onMyMembership);
return () => {
mx.off(RoomEvent.Redaction, onRedaction);
mx.off(RoomEvent.MyMembership, onMyMembership);
};
}, [mx, cacheEnabled]);
};