The first-run setup POST was vulnerable to cross-site request forgery
because it had no Origin validation. Added isSetupOriginAllowed check
that validates the Origin header against configured allowed_origins.
Requests with a mismatched Origin are rejected with 403. Requests
without an Origin header (same-origin or curl) are allowed through.
DM channel rows were returned by ListChannels and included in both
the REST channel list and the WebSocket ready payload. Added type="dm"
skip in handleListChannels and buildReady filter loops. DMs are already
delivered separately via dm_channels. 2 new tests verify exclusion for
both member and admin roles.
Private attachments were accessible without authentication if the UUID
was known. Added AuthMiddleware to the GET /api/v1/files/{id} route,
uploader_id tracking on uploads, and channel-level permission checks
(guild READ_MESSAGES, DM participant, admin bypass) in handleServeFile.
Migration 010 adds uploader_id column to attachments table.
8 new access-control tests covering all authorization paths.
BUG-122: Remove channelID==0 bypass in deliverBroadcast that leaked
all channel-scoped broadcasts to unfocused clients. Clients must now
send channel_focus to receive channel events.
BUG-126: Reject edits and reactions on soft-deleted messages in
handleChatEdit and handleReaction.
BUG-108: Revoke all other sessions when a user changes their password
or enables/disables TOTP 2FA. Adds DeleteOtherSessions DB function.
7 new test cases covering all three fixes.
Install and configure mutation testing (Stryker for client, go-gremlins
for server), load testing (k6), chaos testing (toxiproxy), WAF middleware
(Coraza with OWASP rules, opt-in via waf_enabled config), and Zod for
runtime schema validation. All tools verified building cleanly.
- Add syncutil package with deadlock-detecting mutex (build-tag switchable)
- Add main_test.go TestMain scaffolding across all server packages
- Harden concurrency in ws, admin, auth, and updater packages
- Update CI workflow, go.mod/sum, Cargo.lock, and root changelogen tooling
PATCH /api/v1/users/me — update username/avatar
PUT /api/v1/users/me/password — change password with old pw verification
GET /api/v1/users/me/sessions — list active sessions (single SQL query)
DELETE /api/v1/users/me/sessions/:id — revoke session with ownership check
New files: profile_handler.go, profile_queries.go + tests for both.
All endpoints follow existing writeJSON/errorResponse patterns.
Create Server/api/constants.go (26 constants) and Server/auth/constants.go
(6 constants) for rate limits, timeouts, size limits, and token generation.
Replace all inline magic numbers with descriptive names across 9 source files.
No behavior changes — same values, just named for contributor readability.
CRITICAL (5):
- Hub panic recovery now calls h.Stop() after 3 panics (ws/hub.go)
- Ring buffer EventsSince returns non-nil empty slice for current seq (ws/ringbuffer.go)
- PTT event listener stores unsubscribe handle to prevent leak (ptt.ts)
- verifyTotp respects config.allowSelfSigned instead of hardcoding (api.ts)
- ptt_listen_for_key uses spawn_blocking to avoid thread pool starvation (ptt.rs)
HIGH - Server (13):
- TOTP rate-limit checked after body decode; counters reset on success
- TOTP enable returns 409 if already enabled (must disable first)
- Global search pre-computes accessible channel IDs for FTS WHERE clause
- DeleteAccount queries roles by name instead of hard-coded IDs
- BackupToSafe uses absClean in VACUUM INTO
- Voice camera slot uses atomic EnableCameraIfUnderLimit DB method
- readPump snapshots voiceChID before unregister for TOCTOU safety
- Voice join sets state after token send; rollback takes broadcast flag
- Updater download uses probe pattern instead of overflow write
- Webhook checks Authorization header before reading body
- Storage.Save adds fsync and fixes double-close
- Default WS origin denies cross-origin (was: accept all)
HIGH - Client (6):
- WS reconnect uses generation counter to discard stale events
- AudioPipeline uses generation counter against stale worklet callbacks
- Screenshare mute state preserved across reconnect (not full leave)
- handleVoiceToken uses iterative loop instead of unbounded recursion
- store.ts re-entrancy guard with pending update queue
- Notification AudioContext cleaned up on logout
Reviewed by 4 parallel agents across Server Core, Server Realtime,
Client & Tauri, and Security. 55 total findings; 24 CRITICAL+HIGH
fixed here, 31 MEDIUM+LOW tracked in vault backlog (T-265–T-295).
- C-3: inject setupLimiter into NewAdminAPI instead of package-level global
- H-5: generateRandomKey returns error instead of panicking
- H-6: replace init() bcrypt with sync.Once lazy initialization
- M-2: remove unsafe-inline from admin CSP
- M-3: sanitize upload filenames (strip control chars, truncate to 255)
- M-5: truncate User-Agent to 512 bytes before storing as device
- M-10: MaxBodySizeUnless uses prefix matching instead of exact path
- M-12: wrap seedExistingDatabase in a single transaction
- M-13: use errors.Is for EOF check in upload handler
- M-14: log writeJSON encoding errors instead of discarding
- M-16: standardize error codes to INTERNAL_ERROR across all handlers
- C-3: inject setupLimiter into NewAdminAPI instead of package-level global
- H-5: generateRandomKey returns error instead of panicking
- H-6: replace init() bcrypt with sync.Once lazy initialization
- M-2: remove unsafe-inline from admin CSP script-src and style-src
- M-3: sanitize upload filenames (strip control chars, truncate to 255)
- M-5: truncate User-Agent to 512 bytes before storing as device
- M-10: MaxBodySizeUnless uses prefix matching instead of exact path
- M-12: wrap seedExistingDatabase in a single transaction
- M-13: use errors.Is for EOF check in upload handler
- M-14: log writeJSON encoding errors instead of discarding
- M-16: standardize error codes to INTERNAL_ERROR across all handlers
Move handleVerifyTOTP, handleEnableTOTP, handleConfirmTOTP, and
handleDisableTOTP along with their request/response types into a
dedicated totp_handler.go file, bringing auth_handler.go from 829
to 583 lines.
Add audit log calls for TOTP lifecycle events:
- totp_verified on successful 2FA login verification
- totp_enabled on successful TOTP enrollment confirmation
- totp_disabled on successful TOTP removal
Cover low-coverage handler functions: handleGetPins, handleSetPinned,
handleMetrics, handleDiagnosticsConnectivity, isPrivateIP, AdminIPRestrict,
handleClientUpdate, and handleLiveKitHealth. Adds 30 new test cases across
4 new test files and 2 modified test files.
Comprehensive tests for handleUpload and handleServeFile covering:
- Route mounting verification
- Successful text and PNG uploads with response validation
- Authentication enforcement (missing/invalid tokens)
- Missing file field and invalid multipart form
- Blocked file types (PE, ELF, Mach-O, shell scripts)
- DB record creation and unlinked message_id
- File serving with correct Content-Type, Cache-Control, Content-Disposition
- File not found (missing DB record and missing storage file)
- CORS headers (matching, non-matching, wildcard, no origin)
- Full upload-then-serve round-trip for text and PNG
Coverage: MountUploadRoutes 100%, handleUpload 78.6%, handleServeFile 83.9%
- Add auto-login toggle on server profiles (lightning bolt icon, single-server
enforcement). On startup, auto-connects with saved credentials; falls back
to login form on failure or 2FA.
- Add online_users field to /api/v1/health endpoint (Go server) and display
user count on server cards in the login page.
- Add periodic health check retry every 15s while on ConnectPage so offline
servers update when they come back online.
- Move DM section above text channels in sidebar, limit to 3 visible DMs
with "View all messages" button for overflow.
- DMs with new messages bubble to top of the list automatically.
- Add total unread badge on DM header (visible even when collapsed).
- Member list collapses to just header bar; state persisted across sessions.
Security fixes (from multi-reviewer code review):
- Add DM participant auth checks to channel_focus, typing, and REST
message endpoints — prevents unauthorized access to DM channels
- Fix TOCTOU race in GetOrCreateDMChannel using IMMEDIATE transaction
- Validate YAML credentials before LiveKit config interpolation
- Add CSS variable injection prevention in custom theme loader
- Validate localStorage JSON before unsafe type casts
LiveKit stability:
- Track remote mic audio elements for cleanup on abnormal disconnect
- Remove duplicate token refresh timer scheduling
- Add .catch() to all floating applyMicMuteState promises
- Clear reconnectAc after async post-connect work completes
- Fix double cmd.Wait() race in LiveKit process Stop()
- Reorder voice_join guards: validate channel before livekit==nil check
- Add startup warning for external LiveKit webhook CIDR mismatch
DM system fixes:
- Emit dm_channel_close WebSocket event from REST close handler
- Re-open DM for caller when channel already exists
- Fix unread count incrementing for own messages and active DMs
- Reset channelBeforeDm after Back navigation (stale state bug)
New feature:
- Voice call duration timer in VoiceWidget (MM:SS / HH:MM:SS elapsed)
- Accent color restored on app startup (was only applied in settings)
Test infrastructure:
- Add DM tables to all test schemas (hubTestSchema)
- Inject test LiveKit client in voice handler tests (fixes 28 failures)
Task A: REST API endpoints in api/dm_handler.go — POST /api/v1/dms
(create/get DM channel), GET /api/v1/dms (list open DMs), DELETE
/api/v1/dms/{channelId} (close DM). Routes registered in router.go.
Task B: WebSocket DM routing — handleChatSend, handleChatEdit,
handleChatDelete, and handleReaction all check channel type and use
participant-based auth for DM channels instead of role permissions.
DM messages delivered via SendToUser to each participant (bypassing
channel-subscription model). Auto-reopens DM for recipient on new
message with dm_channel_open event. New broadcastToDMParticipants
helper. New WS event types: dm_channel_open, dm_channel_close.
Task C: Ready payload includes dm_channels from GetUserDMChannels.
Also adds GetDMParticipantIDs helper to db/dm_queries.go.
- Deafen now also mutes the local microphone (privacy fix)
- Mute/deafen zero the audio pipeline GainNode to guarantee silence
when the replaced sender track bypasses LiveKit's track disable
- Send voice_leave on failed auto-reconnect to prevent ghost states
- Gate voice_join on LiveKit availability (reject if h.livekit == nil)
- IP-restrict /api/v1/livekit/webhook to admin CIDRs
- Warn at startup when LiveKit API keys are auto-generated (ephemeral)
- Fix connecting guard: move pending-join dispatch outside finally block
- Add volume slider + mute button overlay on remote video tiles
- Document token refresh limitation for 4h+ sessions
- Fix TS2306 in rnnoise-worklet test (ts-expect-error for worklet import)
Server fixes:
- voice_leave: broadcast voice_leave even on DB error so peers don't see ghost users (H1)
- migrate: record migration inside transaction for atomicity (H2)
- password: use init() with panic for dummyHash to catch bcrypt init failures (M1)
- password: replace //nolint:errcheck with explicit _ = discard (M2)
- handlers: clarify edit permission comment, fix error message wording (M3)
- auth_handler: distinguish duplicate username (400) from DB error (500) (M4)
Client fixes:
- media: revert YouTube oEmbed to browser fetch — no need to disable cert verification (C1)
- messages.store: fix prependMessages cap to keep newest messages, not oldest (H5)
- ChannelSidebar: ref-count globalDragAc to prevent multi-instance teardown race (H6)
- attachments: replace console.error with project logger (M6)
- ws: clarify lastSeq reset comment to match actual behavior (M5)
CRITICAL:
- Add AuthMiddleware + rate limiting to /livekit/* proxy route (was unauthenticated)
- Remove well-known default LiveKit credentials from source; auto-generate unique
random keys on first run so voice works out of the box securely
- Reject the old "devkey"/"owncord-dev-secret-key-min-32chars" in NewLiveKitClient
HIGH:
- Add 5s timeouts to RemoveParticipant/ListParticipants SDK calls (goroutine leak)
- Fix config.yaml default file permissions from 0644 to 0600
- Fix voice store desync on unexpected LiveKit disconnect (phantom UI state)
- Add in-flight guard to handleVoiceToken (race on rapid channel switch)
- Fix lightbox listener leak on rapid reopen (orphaned mousemove/mouseup/keydown)
- Fix allTracked WeakRef set unbounded growth in media-visibility
- Replace debug console.log with createLogger in embeds.ts
- Stop persisting password in Windows credential blob (only token needed)
Server: suppress errcheck on deferred Close() calls, discard
resp.Body.Close error, remove unused voiceSpeakersPayload type
and buildVoiceSpeakers func.
Client: add unit tests for os-motion, livekitSession, and
safe-render to bring coverage from 74.16% to 76.06% (threshold 75%).
- Add LiveKit health check guard to voice_join (reject if process not running)
- Log webhook DB cleanup errors instead of swallowing
- Reject camera enable if CountActiveCameras query fails
- Add LiveKit health status to /metrics endpoint
- Add 13 unit tests for livekit.go, livekit_process.go, livekit_webhook.go
Server:
- Extract image width/height on upload via image.DecodeConfig (header-only)
- Add width/height columns to attachments table (migration 007)
- Include dimensions in AttachmentInfo JSON (optional, backward-compatible)
Client:
- Reserve exact space for images using server-provided dimensions
- Fallback min-height: 200px for external/old images, cleared on load
- Remove premeasureAll() — was caching wrong heights for unloaded images
- Smart per-type height estimates: 32px dividers, 42/72px text, +220px
per image attachment, +320px for YouTube embeds
- Batch ResizeObserver corrections to single RAF with anchor-based scroll
preservation (topmost visible item stays in place)
- Fenwick tree for O(log n) offset lookups (replaces O(n) linear scans)
- CSS contain: layout style on .msg-image to isolate layout shift
Server:
- Fix YAML injection in LiveKit config generation (quote values)
- Revert token TTL to 4h (no server-side JWT revocation)
- Derive LiveKit publish permissions from user role (prevent SFU bypass)
- Add CAS guard for webhook/voice_leave race condition
- Add voice_leave broadcast to rollbackVoiceJoin (prevent ghost state)
- Limit webhook body to 64KB (prevent memory abuse)
- Add rate limit to voice_token_refresh handler (1/60s)
- Add LiveKit health check endpoint (GET /api/v1/livekit/health, 503 on degraded)
- Add voice_token_refresh WS handler for client-initiated token refresh
- Consolidate voice quality constants (single source of truth)
- Fix video limit TOCTOU race (count from DB instead of LiveKit API)
- Raise default voice_max_video from 10 to 25 (Discord parity)
- Add CountActiveCameras DB query
- Non-blocking broadcast send, circuit breaker, exponential backoff
- Close send channel before context cancel in serve.go
- Guard voice mute/deafen for active channel
- Delete orphaned message on attachment link failure
- Redact query string from proxy logs (prevent token leak)
- Use instance-level HTTP client for health checks (no redirect following)
- Set cmd.WaitDelay to prevent goroutine leak on Windows
- Log buildJSON marshal errors
Client:
- Refactor livekitSession.ts from singleton module to LiveKitSession class
- Share single AudioContext for all analysers (was 1 per participant)
- Extract createRoom() helper (DRY)
- Add token refresh timer (3.5h interval, re-arms on failure)
- Skip setSpeakers if unchanged (sort in-place, no allocations)
- Distinguish user-initiated leave from connection error in retry
- Add YouTube videoId validation (prevent iframe src injection)
- Add try/finally to disableCamera
- Wrap store subscription callbacks in try/catch
- Track and cancel initial scroll RAF on cleanup
- Add 5s timeout + encodeURIComponent to YouTube oEmbed fetch
- Clean raw mic stream on RNNoise suppressor failure
- Full voice cleanup on logout via cleanupAll()
Tests:
- Add 7 new server tests (webhook parsing, voice guards, quality fallback)
- Fix 2 pre-existing test failures (mute/deafen invalid payload)
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
- Add reverse proxy at /livekit/* that forwards to LiveKit server
- Server sends relative URL "/livekit" in voice_token; client
resolves to wss://server:port/livekit using known server host
- Fix API secret minimum length (32 chars required by LiveKit)
- Pass TLS config to LiveKit process manager for TURN certs
- livekit_webhook.go: HTTP handler for LiveKit participant_joined/left
events with JWT verification; syncs stale voice state on crash recovery
- Mount webhook at POST /api/v1/livekit/webhook in router
- Speaker detection handled client-side via LiveKit SDK events
Client:
- Wire up all 4 notification toggles (desktop, taskbar flash, sounds, @everyone)
- Add compact mode CSS with visible layout differences
- Add GIF picker with Tenor API (trending + search)
- Render inline images/GIFs instead of link previews for direct URLs
- Add push-to-talk via Rust GetAsyncKeyState polling (non-consuming)
- Add key capture UI for PTT keybinds (supports mouse buttons)
- Add error/success feedback on account settings (password, username)
- Fix mic stream cleanup on tab switch (VoiceAudioTab factory pattern)
- Fix message timestamps using UTC with proper timezone conversion
- Fix emoji reaction picker (was returning early on empty emoji)
- Fix chat scroll jumpiness with Discord-style overflow-anchor + ResizeObserver
- Pre-measure all message heights on load to prevent first-scroll jump
- Improve scrollbar visibility with semi-transparent white thumb
- Show client version on Logs tab
Server:
- Add admin_allowed_cidrs config to restrict /admin to private networks
- Fix voice config defaults lost when YAML section has omitted fields
- Add negative caching to updater (5min error cache)