Commit Graph
2 Commits
Author SHA1 Message Date
John O'Keefe dd963993eb fix(sync): scan events keep instance-wide fan-out as the sanctioned BroadcastToAll
Verifying the per-user chokepoint end to end surfaced a UI regression
it would have caused: web/src/header.ts runs the sitewide scan
indicator on scan_progress/scan_complete events in every browser, and
routing those to the triggering identity alone froze the indicator for
everyone else at its page-load state (active-summary stays the
load-time fallback, but live movement died). The source's own comment
declares the indicator sitewide by design, and scan payloads carry no
user data — job ids, file counts, progress percentages only.

Resolution honors both truths: BroadcastToAll is re-added as the ONE
sanctioned instance-wide fan-out, its doc comment making the decision
explicit (no user-derived data; anything carrying books, notes, or
annotations belongs in BroadcastToUser), and the two scan emitters use
it. TestBroadcastToAllReachesEveryone pins the exception alongside
TestBroadcastIsPerUser — audiences are now named decisions the
compiler and the tests both enforce.
2026-11-02 18:17:37 -05:00
John O'Keefe a431dfa10b fix(sync): websocket events are per-user — chokepoint the fan-out
Owner-flagged after the dev-docs audit: BroadcastProgressUpdate,
BroadcastAnnotationUpdate, and BroadcastConflictNotification fanned out
through the untargeted Broadcast, delivering one user's book progress,
conflict notices, and — worst — annotation payloads (which can carry
note/highlight text) to EVERY connected client on the instance. A
family-lens violation of exactly the kind the RLS move eliminated for
database reads: user B's client received events for user A's books,
including books in libraries hidden from B.

Fix is the RLS philosophy applied to the fan-out layer: enforcement at
one choke point. The untargeted Broadcast method and its channel/loop
machinery are deleted — BroadcastToUser is now the only door, and the
compiler forces every future emitter to name its audience instead of
relying on review. The three helpers take the acting user's ID; the
four sync call sites (progress.go x2, annotations.go x2) already had
the acting user in scope. Scan events were already per-triggering-
identity and collections already per-user — unchanged. Logging is
untouched: the only log line in the diff is the deleted method's own,
so docker logs remains the complete administrator audit trail.

TestBroadcastIsPerUser pins the invariant (alice's two devices receive
her three events; bob's client receives nothing — any message there is
a cross-user leak). Both websocket docs updated in the same commit:
the wire doc's fan-out section now states per-user delivery and the
chokepoint, and the gated endpoint doc's intro matches. Full make test
green.
2026-11-02 18:06:45 -05:00