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
- 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
- Fix race condition: handleDisconnected no longer nulls the room during
initial connect, allowing the retry loop to complete all 3 attempts
- Fix TLS proxy port: default to 443 instead of 8443 when server host
has no explicit port (servers behind nginx/reverse proxy)
- Fix cert store key: strip :443 suffix so LiveKit proxy fingerprint
lookup matches ws_proxy's stored key format
- Add node_ip config option for LiveKit WebRTC ICE candidates (required
for remote users behind NAT)
- Add resolved URL to all connection error/retry/reconnect logs for
easier debugging
- Add diagnostic logging to resolveLiveKitUrl showing which path was
taken (direct/proxy/passthrough)
- 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)
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)
Camera fixes:
- Optimistic setLocalCamera(true) before async setCameraEnabled for instant
button highlight, with revert on failure
- Only call checkVideoMode when camera-relevant state changes, not on every
speaking poll tick (100ms)
- VideoGrid.addStream updates existing cells in place instead of
destroy+recreate to prevent black frame flashes
- VideoModeController tracks localTileAdded to avoid redundant addStream calls
Security & hardening (from prior session review):
- Settings store key allowlist prevents arbitrary key writes
- Certificate fingerprint validates SHA-256 colon-hex format
- CredentialData Debug impl redacts token and password
- LiveKit config file written with 0600 permissions
- Token TTL reduced from 24h to 4h
- Null-check on client.user before token generation
- Thread-safe getChannelID/trySendMsg helpers on Client
- Warn on default dev LiveKit credentials
- Devtools feature-gated behind cfg(feature = "devtools")
- Updater uses configure_client for self-signed cert acceptance
- Clamp voice sensitivity input to 0-100 range
- Clear lastConnectToken/Host on logout
- Fix animation frame leak in VoiceAudioTab mic meter
- 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
- Replace VoiceConfig STUN/TURN/MediaPort fields with LiveKit
API key, secret, URL, and binary path
- Add livekit_process.go: companion process manager with
auto-restart and config generation
- Add livekit.go: token generation, room service client,
participant management, video track counting
- Add LiveKit server SDK dependency (server-sdk-go/v2)
- Update config tests for new VoiceConfig fields
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)
Voice was broken over NAT due to multiple issues across the audio pipeline:
- Fix GainNode silence: WebView2 silences remote WebRTC streams routed through
Web Audio createMediaStreamSource→GainNode→createMediaStreamDestination.
Replaced with direct HTMLAudioElement playback for remote audio.
- Fix NAT traversal: Add Google public STUN server (stun.l.google.com:19302)
so remote clients can discover their public IP for ICE connectivity.
- Fix signaling race: Catch createOffer InvalidStateError when server
renegotiation offer arrives before client's initial offer is sent.
- Fix device switch: Use replaceTrack() instead of removeTrack+addTrack
to avoid SDP renegotiation. Safe rollback on failure (stop old last).
- Fix speaking flicker: setSpeakers skips local user (VAD is sole authority).
- Fix VAD sample rate: Force 48kHz AudioContext instead of system default
(192kHz) which spread FFT bins too wide for voice frequency detection.
- Fix CSP for WASM: Add wasm-unsafe-eval to script-src for RNNoise.
- Fix clearAuth leak: leaveVoice() called before resetVoiceStore().
- Fix ICE rate limit: Separate limit for ICE candidates (50/s vs 20/s).
- Fix stale ICE errors: Silently drop voice_ice with no PeerConnection.
- Fix audio play() race: Deferred to queueMicrotask after DOM attachment.
Debugging infrastructure:
- Logs tab: Copy All button, Voice Diagnostics panel with live session
state, Probe Audio Levels (measures actual signal at 3 pipeline points),
Test Direct Playback button, Copy Diagnostics button.
- Client logging: WebRTC (PeerConnection lifecycle, ICE candidates with
type/address, track events, negotiation), VAD (start/threshold/destroy),
Audio (device acquisition with settings, device changes), noise suppression
(WASM load timing, worklet vs fallback path), voiceSession (remote stream
parsing failures, deafen state, audio element playback events).
- Server logging: SFU init config, voice room mode transitions/track
lifecycle/close, RTP forwarding with packet counts and first-packet
detection, 5s no-packet warning, track fan-out counts, subscriber
transceiver state, ICE candidate details, voice credentials issued.
- Logger: Error objects now serialize .message and .stack instead of {}.
Per-user volume right-click now works on voice user rows in sidebar.
RNNoise ML noise suppression with AudioWorklet + ScriptProcessor fallback.
Tests: 7 new test cases (replaceTrack, setSpeakers skip-local, clearAuth).
- Scaffold Go module (github.com/owncord/server) with all package dirs
- config: koanf-based YAML loader with env var overrides, default generation
- db: pure-Go SQLite (modernc, no CGO), WAL mode, FK enforcement, full
15-table schema from SCHEMA.md including FTS5 and idempotent migrations
- auth/tls: ECDSA P-256 self-signed cert generation, LoadOrGenerate for
all 4 TLS modes (self_signed, acme, manual, off)
- api: chi router with request ID middleware, /health and /api/v1/info
- main: graceful shutdown (30s timeout), structured slog JSON logging
- Stubs for ws, storage, admin packages ready for Phase 2+
Test coverage: api 100%, auth 85.7%, db 82.4%, config 80.6%
Binary: chatserver.exe 12MB, GOOS=windows GOARCH=amd64