The REST path previously used tauri-plugin-http with
danger.acceptInvalidCerts, so it accepted ANY certificate while the WS
and LiveKit paths were TOFU-pinned in Rust — and the bearer token rides
every REST request. This routes REST through a new Rust loopback
TCP->TLS proxy that pins the server certificate to the same
trust-on-first-use fingerprint as the WS proxy.
Rust (src-tauri):
- New http_proxy.rs: per-host loopback tunnels (HttpProxyState map);
per-connection TOFU via CaptureVerifier + tofu_check, sharing
ws_proxy's cert store (cert_store_key) and emitting the same
cert-tofu events (first-use banner / mismatch modal). First request's
Host is rewritten and Connection: close injected so one request rides
each connection. Mismatch returns a clean 502 to the loopback fetch.
- Register HttpProxyState + start_http_proxy/stop_http_proxy in lib.rs.
- Drop the dangerous-settings feature from tauri-plugin-http.
TypeScript (src):
- New lib/httpProxy.ts: ensureHttpProxy(host) (per-host cache +
concurrent-start dedup) / stopHttpProxy(host).
- api.ts, profiles.ts (health), attachments.ts (image + download) resolve
server URLs to http://127.0.0.1:{port}; remove the allowSelfSigned
config field and every acceptInvalidCerts block. External hosts (CDNs,
OG previews, YouTube) keep normal TLS validation.
- main.ts constructs the API client without allowSelfSigned.
- capabilities/default.json: allow http://127.0.0.1:* fetch scope.
Tests:
- New tests/unit/http-proxy.test.ts (cache, dedup, stop/restart).
- api.test.ts and attachments-render.test.ts: mock httpProxy, replace the
acceptInvalidCerts assertions with proxy-origin assertions.
Verified: tsc --noEmit clean; new + affected vitest suites green
(176 tests); the http_proxy pure logic (host validation, header rewrite)
passes as standalone Rust unit tests; oxlint/eslint counts unchanged
from HEAD; prettier clean. The full Tauri build (cargo) requires GUI
system libs not present in this environment and runs on CI/real runners.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UA17KPvqGBX3XbXYnMf1rA
A minimized window reports placeholder coordinates (-32000 on Windows); the
move event fired by minimize was persisting them, so quitting while
minimized silently discarded the remembered position (the new off-screen
validation then falls back to centered). Skip the save while minimized so
the last real geometry survives.
Follow-up to #124.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwtnpHAoSFr1ZibQgQkNQK
On Wayland sessions (notably GNOME + NVIDIA), WebKitGTK's DMABUF renderer
can crash or render a blank window, so the client failed to start. Set
WEBKIT_DISABLE_DMABUF_RENDERER=1 on Wayland unless the user has already set
it themselves.
device_query's global key state needs an X11/XWayland display and panicked
per poll on pure-Wayland setups. Use DeviceState::checked_new() so push-to-
talk degrades to inactive with a single warning instead.
Fixes#96
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwtnpHAoSFr1ZibQgQkNQK
Restoring a stale window position (e.g. from a disconnected monitor) placed
the window off-screen with no way to see it. Before applying the saved
position, check that the rect is reachable on some monitor reported by
availableMonitors(): at least 100px of horizontal overlap and a grabbable
title bar row. If not, keep the default centered placement. Also reject
non-finite or non-positive saved dimensions. If monitors cannot be queried,
restore proceeds unchanged.
Fixes#124
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwtnpHAoSFr1ZibQgQkNQK
Forward-only versioning for the alpha reset: 1.1.0-alpha.N ascends
past the superseded v1.0.0 for every installed client, and the alpha
series stays below 1.1.0-beta.N and the final 1.1.0.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
'Network' is not a tauri-bundler AppCategory, so every bundling run
failed with 'invalid category' after compilation succeeded.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Bump crossbeam-epoch 0.9.20, quinn-proto 0.11.16, rustls-webpki
0.103.13 in Cargo.lock (semver-compatible; RUSTSEC-2026-0204, -0185,
-0098, -0099, -0104 and the second quick-xml instance resolved).
quick-xml 0.37 remains pinned by tauri-winrt-notification 0.7 (via
tauri-plugin-notification) with no compatible route to the fixed 0.41;
it only parses toast XML the library itself builds, never
attacker-controlled input, so RUSTSEC-2026-0194/-0195 are ignored in
.cargo/audit.toml with removal criteria documented inline.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- cfg-gate open_devtools entirely behind the devtools feature (its
registration in lib.rs already was), removing the dead-code warning
in non-devtools builds
- drop let-bindings of unit-returning store.set in the three cert
rollback paths (let_unit_value)
- remove a needless borrow on the startup-error dialog description
(only compiled on non-Linux, hence Windows-only finding)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Restores dangerous-settings and allowSelfSigned which are required for
self-hosted servers with self-signed certificates. Makes HealthResponse.version
optional to match server-side removal, and updates router tests to assert
version is correctly omitted from unauthenticated endpoints.
https://claude.ai/code/session_01KKo3RwjdmcNzkgXNfUkgNT
Addresses 14 findings from the security audit across all severity levels:
CRITICAL:
- C-1: Add user blocking system (migration, DB queries, REST API, WS DM
send check) to prevent harassment via unconsented DMs
- C-2: Remove server version from unauthenticated /health and /info endpoints
to prevent fingerprinting
HIGH:
- H-1: Remove dangerous-settings feature from tauri-plugin-http
- H-3: Default allowSelfSigned to false in API client (was hardcoded true)
- H-4: Cap invite expiration to 30 days (720 hours)
- H-5: Add 256KB message size limit to LiveKit WS proxy (prevents OOM)
- H-6: Cap concurrent sessions to 25 per user (evicts oldest on overflow)
- H-8: Restrict /diagnostics/connectivity to ADMINISTRATOR role
MEDIUM:
- M-2: Deny access to legacy NULL-uploader unlinked attachments
- M-4: Log warnings on TOTP plaintext decryption fallback paths
- M-8: Remove acceptInvalidCerts from OG preview fetches
- M-10: Expand file upload blocklist (Java .class, OLE2, WASM, .lnk)
- M-12: Add LIMIT to ListInvites (200) and ListMembers (1000)
- M-14: Add CHECK constraint trigger on channels.type (text/voice/dm)
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
- Restrict PTT key capture to non-text keys only (function, navigation, mouse buttons) via allowlist (BUG-136)
- Gate DevTools button and F12/Ctrl+Shift+I shortcut behind import.meta.env.DEV
- Disable devtools Tauri feature in production (Cargo.toml default feature removed)
- Remove overly broad http:default capability, replace with scoped http:allow-fetch
- Set withGlobalTauri to false to avoid global __TAURI__ surface exposure
- Fix reconnect race: add abort checks after room creation, URL resolve, and connect (BUG-070)
- Fix ws.ts reconnect guard: bail out safely when config is null after disconnect
- Fix DM broadcast double-send and add monotonic seq + replay buffer support via sendSequencedToUsers
- Add seqMu mutex to serialize seq assignment across broadcastDM and deliverBroadcast paths
- Fix handleFreshConnect to unregister client and close connection on buildReady failure
- Add tests for PTT allowlist, ws reconnect config-null guard, livekit abort-after-connect, and DM sequencing
Client:
- Replace Win32 Credential Manager with cross-platform keyring crate
(Windows Credential Manager / Linux Secret Service / macOS Keychain)
- Add Linux PTT support via device_query crate with VK-code-compatible
mapping; thread-local DeviceState avoids repeated /dev/input/ opens
- Add AppImage + deb bundle targets to tauri.conf.json with Linux
metadata and deb runtime dependencies
Cargo.toml:
- Add keyring = "3" (all platforms)
- Add device_query = "2" (Linux only, cfg guard)
- Remove Win32_Security_Credentials feature (no longer needed)
CI/CD:
- Add ubuntu-22.04 and ubuntu-22.04-arm to tauri-build matrix
- Fix Linux deps step condition: startsWith(matrix.os, 'ubuntu')
- Add server Docker build verification job (build-only, no push)
- Add release-client-linux (x86_64) and release-client-linux-arm64
jobs producing AppImage + deb artifacts
- Add release-server-docker job pushing to ghcr.io on version tags
- Update publish job to include all Linux and ARM64 artifacts
Server:
- Add multi-stage Dockerfile (golang:1.25-bookworm → distroless/static)
- Non-root user (uid 65532), /app/data volume, port 8443 exposed
- Add .dockerignore excluding binaries, data, and local config
- Consolidate ws-state and cert-tofu emit calls in ws_proxy.rs into
private helper functions (emit_ws_state, emit_cert_tofu). One call
site per event name prevents tauri-typegen 0.5.0 from generating
duplicate event listener functions.
- Add CI fixup step that injects 'export type Value = unknown' into
generated types.ts — tauri-typegen cannot map serde_json::Value to
a TypeScript type, so the generated file references an undefined type.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace danger_accept_invalid_certs(true) with PinnedVerifier-based
rustls config that validates server cert against TOFU fingerprint from
the cert store. For CA-signed servers (no stored fingerprint), system
TLS is used. Shared PinnedVerifier, cert_store_key, and
load_stored_fingerprint are now pub(crate) for reuse.
- Integrate stream preview into VoiceChannel sidebar for remote users
with active camera/screenshare
- Add track lifecycle listeners (ended/mute) to VideoGrid to auto-remove
stale black tiles
- Call video.play() explicitly for WebView2 autoplay compatibility
- Prevent redundant voice join when already in channel (ChannelSidebar)
- Add attachScrollCollapse for preview cleanup on scroll
- Remove tauri_typegen from build.rs
- Add Server/server.exe to gitignore
- Add stream-preview and video-mode-controller test coverage
- 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
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).
- 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)
The Rust save_credential command was accepting but ignoring the
password parameter (_password). Now stores it in the Windows
Credential Manager JSON blob alongside username and token.
DPAPI encrypts the blob at rest, tied to the Windows user —
plaintext is never written to disk.
Also wire the checkbox state to the profile's rememberPassword
field so it persists across sessions. Previously it was
hardcoded to false on every login.
- Add --autoplay-policy=no-user-gesture-required to WebView2 config
so remote participants' audio plays immediately on join (desktop app
doesn't need browser autoplay restrictions)
- Add AudioPlaybackStatusChanged handler with click-to-unlock fallback
for browsers that still block autoplay
- Replace broken VAD implementation that used setMicrophoneEnabled/
mediaStreamTrack.enabled (both fought LiveKit's track lifecycle) with
a unified GainNode audio pipeline:
rawMic → AnalyserNode (VAD) → GainNode (volume × gate) → sender
- VAD now gates by setting gain=0 instead of touching the track —
analyser always sees real audio, no stale track references
- Merge input volume and VAD into single pipeline (always active)
- Add voice settings UI: draggable sensitivity threshold on mic meter,
input/output volume sliders (0-200%), audio processing toggles
Co-Authored-By: claude-flow <ruv@ruv.net>
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)
- Detach existing audio elements before attaching to prevent double playback on reconnects
- Remove webAudioMix to eliminate Web Audio overhead compounding with multiple participants
- Use participant.setVolume() for full 0-200% per-user volume range
- Add pli_throttle and active_loopback_prevention to LiveKit server config
- Bump version to 1.3.0
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
Speaking detection:
- Replace unreliable ActiveSpeakersChanged with per-participant
IsSpeakingChanged events (fires locally, more responsive)
- Wire speaking detection on local + remote participants
- setSpeakers updates all users including local (LiveKit is sole
authority, no local VAD)
DevTools:
- Add F12/Ctrl+Shift+I shortcut to open WebView2 DevTools
- Enable devtools feature in Tauri release builds
- Add open_devtools Rust command
CSP:
- Allow http://ipc.localhost for Tauri IPC protocol
- Allow http/ws://localhost:* for LiveKit signal connection
Connection:
- Fix LiveKit API secret minimum 32 chars
- Remove TURN TLS config that crashed LiveKit server
- Add reverse proxy at /livekit/* (kept for future use)
- Add http://localhost:* and ws://localhost:* to CSP connect-src
(WebView2 was blocking LiveKit signal connection)
- Add connection retry (3 attempts, 2s delay) for LiveKit server
startup race condition ("could not find any available nodes")
- Remove broken TURN TLS config from generated livekit.yaml
- Send direct LiveKit URL instead of proxy path (localhost is
treated as secure context in Chromium/WebView2)
- Set LiveKit server host from API config for URL resolution
- KeybindsTab: fix stale savedVk closure — currentVk now updates on
successful capture and clear, so timeout/error reverts correctly
- AccountTab: add 1–32 char username length validation
- VoiceAudioTab: fix orphaned camera previewStream on repeated tab
visits — consolidated to startCameraPreview/stopCameraPreview
helpers with registrar stopping old tracks before new ones
- VoiceAudioTab: camera preview errors now shown to user instead
of silently swallowed; error cleared on retry
- All toggles: new createToggle() helper in helpers.ts with
role="switch", tabindex="0", aria-checked, Enter/Space support
- Applied createToggle to AppearanceTab, NotificationsTab,
VoiceAudioTab (5 toggle instances)
- Fix SDP signaling race condition: add per-client negoMu to serialize
renegotiateParticipant / handleVoiceOffer / handleVoiceAnswer so
concurrent OnTrack goroutines don't race through rollback
- Fix handleVoiceLeave triple-fire: early return when clearVoice()
returns zeros so ICE callbacks don't re-enter and corrupt state
- Fix SQLite SQLITE_BUSY errors: add busy_timeout=5000 pragma and
SetMaxOpenConns(1) for file-based databases
- Fix deafen bypass: new remote audio elements now respect localDeafened
state so late-arriving streams are muted immediately
- Add debug-level logging for SDP negotiation, track fan-out, ICE
candidates, voice state changes, room lifecycle, and participant
add/remove
- Bump version to 1.1.1
- README.md: added video chat, GIF picker, inline images, PTT, desktop
notifications, compact mode, admin IP restriction, config table entry
- CLAUDE.md: added Key Features section and new critical rules for video
track IDs and Tenor API key
- tenor.ts: documented public anonymous Tenor API key
- ptt.rs: added 10s timeout to key capture to prevent thread leak
- KeybindsTab.ts: handle timeout (vk=0) from key capture gracefully
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)