Audit findings:
- mosaic:session:{sessionId}:* — session-scoped; sessionId is a UUID (not
guessable); keys don't need userId embedded because session-ID ownership
is enforced at the WebSocket/HTTP auth layer before any key access occurs
- mosaic:auth:poll:{token} — token is crypto.randomUUID(); userId is stored
in the value (not the key); TTL 5 min; no enumeration risk
- sweepOrphans() accepted a _userId param but ignored it, allowing any
authenticated user to trigger a system-wide GC sweep via /gc; fixed by
removing the unused param and promoting /gc command scope to 'admin'
- All three KEYS calls (collect, sweepOrphans, fullCollect) replaced with a
private scanKeys() helper using SCAN cursor iteration to avoid Valkey
event-loop stalls under production key volumes
No key-pattern schema changes needed: session keys are already sufficiently
opaque (UUID entropy). The cross-user action risk was in /gc dispatch scope.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>