Update LiveKitSession tests to use _state discriminated union instead of
old flat field names (room, currentChannelId, latestToken, etc.) removed
in the state machine refactor. Also fix renderers.test.ts URL resolution
by setting a server host in beforeEach so isSafeUrl can parse relative
attachment URLs in jsdom. Stage all four Go test files so the CI Go job
runs them.
Additionally fix a regression in connectAndSetup's finally block: when a
pendingJoin is queued during a stale-join abort, preserve the connecting
state so handleVoiceToken's drain loop can pick it up rather than losing
it by resetting to idle.
- 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
- Fix typo in setup-buildx-action pin (d8db...→d36ec...) that caused CI to fail
- Replace stale TenorGif type with GifResult in gif-picker.test.ts lines 531-532
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
Tenor shuts down June 30, 2026. Klipy is a drop-in replacement built
by the ex-Tenor team, free for production use.
- Replace tenor.ts with gifProvider.ts (api.klipy.com/v2)
- CDN allowlist updated to *.klipy.com (static.klipy.com is the real CDN)
- Add Klipy watermark logo to sent GIFs in chat (bottom-left)
- Update attribution text to "Powered by Klipy"
- Wire VITE_KLIPY_API_KEY secret into release workflow
- Add .env to .gitignore to protect local API key
- updater.go: DownloadAndVerify now uses parseChecksumFileAny with
checksumEntryNamesForGOOS so the linux/ path prefix produced by the
release workflow's sha256sum is found correctly (fixes
TestDownloadAndVerify_Success on ubuntu-latest)
- video-grid.test.ts: add setScreenshareAudioVolume to the
@lib/livekitSession mock so vitest does not throw on the export
that VideoGrid.ts imports
- ci.yml: add top-level `permissions: contents: read` to restrict
GITHUB_TOKEN to minimum required (fixes 3 missing-workflow-permissions alerts)
- claude-code-review.yml: remove unsafe `ref: pull_request.head.sha`
checkout in pull_request_target workflow and pin checkout to SHA
(fixes untrusted-checkout/high alert)
- tenor.ts: add codeql suppression comment for hard-coded-credentials;
the fallback key is Google's public anonymous demo key, not a secret
- 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>
- Add validateAvatarURL helper enforcing https:// scheme, non-empty host, and 512-char max length
- Add rate limiting (10/min) to PATCH /api/v1/users/me profile update endpoint
- Guard avatar rendering in DmSidebar, DmProfileSidebar, and UserProfilePopup with isSafeUrl check to prevent unsafe URL injection in the UI
Update golangci-lint-action to v9.2.0 with correct commit SHA. Change
eslint-disable-next-line to oxlint-disable-next-line for oxlint-specific
rules (consistent-function-scoping, prefer-add-event-listener,
require-post-message-target-origin) that ESLint doesn't recognize.
Soundboard was never implemented — remove USE_SOUNDBOARD permission bit,
rate limiter, protocol entry, admin mockup reference, TODOS entry, and
all related test assertions.
Add user_update event so other clients see profile changes in real-time
without needing to reconnect. Also updates saved credentials in Windows
Credential Manager when the current user changes their username.
Fixes: livekit-session test mock missing unpublishTrack property.
The client was sending DELETE /api/v1/invites/{id} with a numeric database
ID, but the server expects the invite code string. This caused a 404 since
no invite has a code matching a numeric ID.
Both setupAudioPipeline and teardownAudioPipeline called replaceTrack
as fire-and-forget. If teardown's replaceTrack resolved after setup's,
the WebRTC sender would be bound to the wrong track. Both paths now
capture _pipelineGeneration before the async call and detect stale
completions, preventing out-of-order track replacement.
BUG-107: cleanupAllAudioElements now calls pause() and sets
srcObject = null before removing elements from DOM, ensuring streams
are fully released during reconnection cleanup.
BUG-121: Diagnostics endpoint now has 5 req/min rate limit as
documented, preventing enumeration of internal topology.
BUG-132: DeleteOrphanedAttachments uses DELETE ... RETURNING stored_as
(atomic) instead of separate SELECT then DELETE, eliminating the race
where a file could be deleted after its attachment was linked.
Refine auto-open to match Discord behavior: grid opens automatically
when the local user enables camera or screenshare, but NOT when remote
users do. Remote video requires clicking the user row to open.
BUG-105: checkVideoMode now auto-opens the video grid when any video
stream (local or remote camera/screenshare) becomes active. Previously
tiles were added to a hidden grid container.
BUG-139: All GitHub Actions pinned to commit SHAs instead of mutable
tags. Tool installs (govulncheck, tauri-typegen, cargo-audit) pinned
to specific versions instead of @latest.
BUG-099: Auto-reconnect now reapplies saved audio input/output devices
via switchActiveDevice, matching the initial join path.
BUG-102: Screenshare tile volume slider now calls
setScreenshareAudioVolume with the normalized value instead of only
toggling mute. Intermediate volumes (e.g. 50%) work correctly.
BUG-104: attachScrollCollapse moved from update() to component creation
so only one listener is attached to the container, preventing
accumulation on every voice state change.
BUG-100: Camera/screenshare tracks are now stopped in catch blocks when
publishTrack fails, releasing hardware immediately.
BUG-101: Screen video track now has an 'ended' listener that triggers
the full disableScreenshare flow when the OS "Stop sharing" button is
clicked, keeping UI and WS state in sync.
BUG-103: retryMicPermission now checks localDeafened state. If deafened,
the mic is acquired but kept muted so audio is not published while the
UI shows deafened.
wirePostAuth always called saveCredential regardless of the "Remember
password" checkbox state, leaving the session token in Windows
Credential Manager even when the user opted out. Added rememberPassword
parameter to wirePostAuth and skip saveCredential when false.
teardownForReconnect only cleaned up audio pipeline and token timer,
leaving manual camera/screenshare MediaStreamTracks capturing
indefinitely after unexpected disconnect. Added stopManualCameraTrack
and stopManualScreenTracks calls before room is nulled, plus store
flag resets so the UI reflects the actual state.
The cert-tofu event listener now handles "trusted_first_use" status
and shows a visible notification banner with the server hostname and
SHA-256 fingerprint. Adds onCertFirstTrust callback to the WS client
API. First-use certificate trust is no longer silent.
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.
- audio-pipeline.test.ts: replace no-op assertions with state checks
- device-manager.test.ts: replace toBeDefined/not.toThrow with actual
value and behavior assertions
- livekit-session.test.ts: replace not.toHaveBeenCalled with state
verification and return value checks
The quality debounce timer could fire after the stats poller was stopped,
calling listeners against a dead room. Sub-issues A (autoplay listener)
and C (VAD timer) were already fixed in prior work.
Track mute events can fire during network blips or SFU layer switching.
Previously this removed the tile entirely, requiring a new TrackSubscribed
event to restore it. Now mute adds a CSS class (track-muted) and unmute
removes it, while only the ended event triggers tile removal.
- 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
Server generates LiveKit participant identities as "user-{id}:{voiceJoinToken}"
but parseUserId regex required exact "user-{id}" (with $ anchor), returning 0
for all remote participants. This caused the userId > 0 guard in
handleTrackSubscribed to silently drop all remote video callbacks.
- Update parseUserId regex to accept both "user-{id}" and "user-{id}:{token}"
- Fix getRemoteVideoStream to iterate remoteParticipants instead of exact
identity lookup (which also failed due to the token suffix)
- Add test cases for token-suffixed identities
- Fix pre-existing noUncheckedIndexedAccess TS errors in test files
The deafen guard in applyRemoteAudioSubscriptionState only unsubscribed
participants already in the room. Participants joining after deafen had
their audio unconditionally attached. Added a guard at the top of
handleTrackSubscribedAudio that checks localDeafened and calls
publication.setSubscribed(false) before any audio element is created.
Replace .sort() with .toSorted() and .reverse() with .toReversed()
to avoid in-place mutation (consistent with project immutability rules).
Only disable no-map-spread rule — new Map(existingMap) is the correct
immutable copy pattern, not a perf issue worth flagging.
Oxlint: 0 warnings, 0 errors with all rules enabled except no-map-spread.
The previous implementation used serverHost.split(":")[0] to extract
the hostname, which fails for IPv6 addresses — "::1:7880".split(":")[0]
yields "" instead of "::1". Now handles three formats:
- Bracketed: [::1]:7880 → host = "::1"
- Bare IPv6: ::1 → host = "::1" (multiple colons detected)
- IPv4/hostname: example.com:443 → host = "example.com"
Also fixes ensureLiveKitProxy to wrap bare IPv6 in brackets and
correctly detect port presence in bracketed notation.
Add non-null assertions to mock .calls[0] access in notifications tests
and mockListen implementation reference in ws tests. Required by
Stryker's stricter TS checker vs vitest runtime.
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
Fix 5 typecheck errors caught by tsc strict mode:
- Remove soundboard_play reference from types.test.ts
- Use optional chaining on MountableComponent.destroy
- Add string fallback for ROLE_COLORS lookup
- Add missing Channel fields in screen-share test mock
- Add non-null assertion on mock.calls index
Update 111 test files to match security hardening changes:
- acceptInvalidCerts now conditional on allowSelfSigned
- Credential store no longer returns passwords over IPC
- File type validation uses strict MIME allowlist
- Search rate limiter timing adjustments
- Dispatcher cleanup mock additions
- Audio elements screenshare mute preservation
2962 tests passing across 110 test files.