- I-1: Add key holder election in Hub (lowest userID per channel); reject
non-key-holder voice_e2ee_offer with NOT_KEY_HOLDER error
- I-2: Accept raw (unpadded) base64 in E2EE announce/offer handlers via
decodeBase64Loose fallback
- I-6: Copy E2EE public key value while h.mu.RLock is held in getClientE2EEPubKey
- I-7: Lower loginRateLimitPerMinute from 60 to 5
- C-1: TOCTOU fix — target channel check held under same lock as client lookup
- C-2: Include is_key_holder bool in voice_token payload so client knows
whether to initiate key distribution
- M-5/M-6: Add ErrCodeBadPayload/ErrCodeNotKeyHolder error constants
- Fix pre-existing api build errors: block_handler.go getUserFromContext,
router.go RequirePermission arg count
- Add user_blocks table to all test DB schemas (ws, api DM)
- Add voice_e2ee_test.go and constants_test.go covering all fixes
Replace server-generated symmetric keys with client-side ECDH P-256 key
exchange. The server now only relays opaque public keys and encrypted
room key blobs — it never sees the actual room encryption key.
Protocol:
- voice_e2ee_announce: clients broadcast ECDH public keys
- voice_e2ee_offer: key holder wraps room key for each peer via ECDH+HKDF+AES-GCM
- Key rotation on participant leave (forward secrecy)
Server changes:
- Remove VoiceE2EEKeys (server-side key generation)
- Add relay handlers for announce/offer messages
- Store per-client ECDH public keys on Client struct
- Send existing public keys to new joiners during voice state sync
Client changes:
- New e2eeCrypto.ts: ECDH P-256, HKDF-SHA256, AES-256-GCM key wrapping
- LiveKitSession generates keypair on join, manages key holder election
- Key holder generates room key and wraps for each peer
- Non-holders wait for offer before connecting to LiveKit
- Room key rotated when any participant leaves
https://claude.ai/code/session_01KKo3RwjdmcNzkgXNfUkgNT
Server generates a per-channel 256-bit symmetric key (crypto/rand) when
the first participant joins voice. The key is distributed to all
participants via the voice_token WS message (already TLS-encrypted) and
cleared when the channel empties for forward secrecy per session.
Client configures LiveKit Room with ExternalE2EEKeyProvider and an
SFrame e2ee-worker. All audio/video frames are encrypted client-side
before reaching the SFU — the server never sees plaintext media.
Changes:
- Server: new VoiceE2EEKeys store, e2ee_key in voice_token payload
- Client: E2EE Room options, key provider wiring for connect/reconnect
- CSP: added worker-src 'self' blob: for the E2EE Web Worker
https://claude.ai/code/session_01KKo3RwjdmcNzkgXNfUkgNT
Architecture:
- Add subscribeSelector to store.ts for selective state subscriptions
- Create reconcileList utility for DOM list patching without rebuild
- Create shared createContextMenu utility (dedup 3 files)
- Convert all 20 subscribe() calls to subscribeSelector across 11 files
- Split renderers.ts (1131L) into 7 focused files by concern
- Split ConnectPage.ts (838L) into ServerPanel + LoginForm + shell
- Fix ineffective (s) => s selector in ChannelSidebar
GIF visibility:
- Add media-visibility.ts with IntersectionObserver + canvas snapshots
- GIFs auto-pause after 10s, play/pause button overlay on hover
- Freeze GIFs on scroll-away, window blur, and minimize
- Wire into media.ts, attachments.ts, embeds.ts renderers
Tests: 46 new tests (1073 total), all passing
Net: -1166 lines across client codebase