Clean sweep of every actionable item from the two Copilot review passes
on head 59ae4d8. Grouped by severity:
─── Crash / security (must-fix) ─────────────────────────────────────
1. main.go:140 — telemetryShutdown nil panic.
telemetry.Init can return (nil, err) on the -tags otel skeleton
path; the deferred closure would then call a nil function. Normalise
to a no-op shutdown when Init errors so the defer is always safe.
2. api/upload_handler.go — permSvc nil deref.
MountUploadRoutes + handleServeFile dereference permSvc on every
authenticated file request. Add a fail-fast panic at mount time so
the misconfiguration surfaces at wiring, not on the first 500.
Update upload_handler_test.go to pass a real PermissionService built
on the test DB (the existing tests were missing the argument entirely,
which meant the package wouldn't compile — this fixes the real bug
Copilot flagged).
3. ws/event_persister.go — NewEventPersister nil EventStore panic.
run() dereferences p.store on every flush. Panic at constructor
time instead so the crash happens once at startup rather than
minutes later in a background goroutine.
4. plugin/host_ui.go — serve-time symlink check.
rejectSymlinksUnder only runs at install time, so a symlink created
post-install (accidental or malicious) would be followed by
http.ServeFile and leak host files. Add an os.Lstat + ModeSymlink
check + IsRegular check to AssetHandler on every request. Cheap
relative to the file read and closes the TOCTOU window.
─── Correctness / observability (should-fix) ───────────────────────
5. ws/deps.go:77 — requirePerm hides misconfig as FORBIDDEN.
Previously, nil database, nil perms, or a GetRoleForUser error all
returned ErrCodeForbidden with the same message, making operator
failures indistinguishable from legitimate permission denials.
Split the branches: misconfig + DB error now return ErrCodeInternal
with a server-side slog.Error so operators see the real problem;
FORBIDDEN is reserved for the actual permission-bit check.
6. telemetry/metrics.go — ServiceCallDurationMs renamed to Sec.
Field name said "Ms" but the instrument name was
`service_call_duration_seconds` with unit "s". Renamed the field
and updated all 8 service-layer callers so the struct field and
metric semantics match.
7. ws/event_persister.go — flushEvy typo → flushEvery.
Renamed the field and the one call site in run().
─── Comments out of sync with code ──────────────────────────────────
8. plugin/loader.go — Stat vs Lstat comment.
The comment claimed "Stat (not Lstat)" but the code correctly uses
os.Lstat to detect symlinks. Updated the comment to match the code;
the code was already right.
9. telemetry/telemetry_otel.go — compile claim wrong.
Comment said the file would fail to compile without the upstream
OTel modules, but the skeleton deliberately avoids importing them
and Init returns a runtime error instead. Updated the comment to
reflect actual CI behaviour (the -tags otel build step passes
today but doesn't exercise real telemetry).
─── Nit / polish ────────────────────────────────────────────────────
10. ws/event_pruner.go — startup delay magic constant.
Hard-coded time.Minute made the "run shortly after startup"
behaviour untestable (a test with a 100ms interval would still
wait a full minute). Cap the startup delay by the interval:
min(interval, time.Minute). Documented via a new `maxStartupDelay`
constant.
11. ws/event_pruner_test.go — new file.
Unit coverage for runPrune cutoff correctness, error swallowing,
StartEventPruner nil-store short-circuit, ctx cancellation, and
the interval-bounded startup delay from fix#10. Uses a fakeEventStore
stub that records every prune call and signals the first one so
tests don't sleep.
─── Verification ────────────────────────────────────────────────────
gofmt -l clean. No network access in sandbox so `go vet` and `go test`
could not run; the changes are local and surgical and every touched
file compiles in isolation against the existing signatures.
https://claude.ai/code/session_01UsBsQW2YiA2usk9pnJjAWk
Final in-sandbox completeness pass. Five focused pieces; the remaining
items in PHASE_BC_LOCAL_TODO.md after this commit are all genuinely
local-only (toolchain, network, native deps).
Test coverage (the biggest gap from prior reviews)
- Server/plugin/manifest_test.go — pluginNameRegexp accept/reject table,
validateRelativePath table, oversized version, unknown permission.
- Server/plugin/host_http_test.go — hostAllowed dot-boundary suffix,
empty-entry rejection, case insensitivity, FQDN trailing dot. ipAllowed
table over loopback, RFC1918, RFC4193 (ULA), RFC6598 (CGN), link-local,
multicast, unspecified — both v4 and v6 — plus public-IP accept cases.
- Server/plugin/loader_test.go — rejectSymlinksUnder catches direct and
nested symlinks; scanPluginDirectory rejects a plugin whose entrypoint
is a symlink. Skipped on Windows where symlink creation needs elevation.
- Server/plugin/host_ui_test.go — AssetHandler serves declared files,
rejects undeclared files (404), rejects path traversal, supports nested
asset paths.
- Server/ws/hub_seedseq_test.go — SeedSeq monotonic, never-backwards,
concurrent CAS safety, integration with nextSeq.
- Server/ws/extract_event_type_test.go — table covering happy paths,
control char rejection, escaped quote rejection, length cap (64),
empty/missing/non-JSON inputs.
Plugin install endpoint (closes a real feature gap)
- Server/plugin/registry.go — InstallFromZip extracts a plugin .zip into
a staging directory under cfg.Directory, validates it zip-slip safe
(cleaned-path Rel check), refuses non-regular entries, refuses
symlinks, caps compressed at 16 MiB and uncompressed total at 64 MiB
(each file gated by io.CopyN against the remaining budget). Manifest
is parsed at the staged root, then atomically renamed into the
canonical plugin directory and registered via the existing
installFromDisk path.
- Server/api/plugins_handler.go — POST /install accepts multipart with
one "plugin" file part, http.MaxBytesReader caps the request body,
io.LimitReader caps the in-memory buffer, calls Registry.InstallFromZip,
returns 201 with the new plugin name. The endpoint inherits the Pass 2
admin auth + IP gate (mounted under r.Use(admin.RequireAdminAuth)).
Protocol surface
- Server/ws/serve.go — buildAuthOK now takes replaySource and includes
it in the auth_ok payload as "replay_source": "none" | "buffer" | "db".
Two call sites updated: reconnect path passes the existing local,
fresh-connect path passes "none". Test export updated to pass "none".
CI build-tag matrix
- .github/workflows/ci.yml — three new steps inside server-build-test
build the server with -tags otel, -tags wazero, and -tags otel,wazero.
All three are continue-on-error: true until the upstream OTel and
wazero modules land in go.mod (tracked in PHASE_BC_LOCAL_TODO.md).
Once they do, dropping continue-on-error converts the steps into
hard CI gates against tag-boundary drift.
Documentation
- CHANGELOG.md — new root-level file with curated entries for Phase B,
Phase C, security, and behavioural changes operators must know about
(notably event_persistence.enabled = true by default).
- PHASE_BC_LOCAL_TODO.md — ticks off the install endpoint, the
replay_source field, and the existing event_persistence defaultYAML
entry. The remaining items are toolchain-bound.
After this pass, the in-sandbox completeness ceiling is reached.
Everything still pending requires Go 1.25 toolchain, npm install,
real OTel SDK + wazero modules, sqlc, postgres backend impl, or
tinygo.
https://claude.ai/code/session_01UsBsQW2YiA2usk9pnJjAWk
Eight focused follow-ups from the medium-severity review bucket. All
in-sandbox tractable; no module changes, no new dependencies.
Performance
- Drop the defensive memcpy in EventPersister.Enqueue. The hub already
passes a fresh slice from wrapWithSeq and the copy was happening under
seqMu, serializing broadcast throughput. Documented the no-mutate
contract on the call site.
Observability
- AppMetrics gains WSEventsPersistErrors counter; the persister run loop
bumps both it and the existing WSEventsPersisted counter via cached
metrics handle.
- Hub.persistEvent now extracts the real event type ("chat_message",
"voice_join", ...) from the wrapped JSON envelope via a small
no-allocation byte scan instead of recording the generic
"broadcast"/"channel_broadcast" label.
- Added OTel spans + ServiceCallDurationMs histogram entries on one
public method per remaining service: DMService.CreateDM,
VoiceService.JoinChannel, InviteService.CreateInvite,
ModerationService.BanUser, BlockService.BlockUser,
UserService.UpdateProfile. Mirrors the existing pattern from
MessageService.SendMessage.
Hardening
- plugin/loader now Lstat-walks each plugin directory and rejects any
symlink, plus refuses an entrypoint that is itself a symlink. The
asset handler's prefix check stays as defense in depth.
- ipAllowed (plugin HTTP capability) now rejects RFC6598 carrier-grade
NAT (100.64.0.0/10), closing a gap in net.IP.IsPrivate which only
covers RFC1918 + RFC4193.
- Registry.activateAll syncs Instance.Enabled := true after a successful
activate so callers reading the in-memory flag see the live state.
Documentation
- defaultYAML now documents the new event_persistence, telemetry, and
plugins config blocks with their defaults and one-line descriptions.
- PHASE_BC_LOCAL_TODO.md ticks off five items (defaultYAML docs ×2,
remaining service spans, registry wiring already-fixed in Pass 2).
https://claude.ai/code/session_01UsBsQW2YiA2usk9pnJjAWk
Phase B + C review pass: critical security and correctness fixes.
Security
- S1: plugin admin endpoints now require admin.RequireAdminAuth in addition
to AdminIPRestrict. Previously a LAN attacker on the allowed CIDR could
list/enable/disable/uninstall plugins without a session.
- S2: rewrite plugin HTTPDo allowlist with proper net/url parsing. Empty
entries are ignored, suffix matches require a dot boundary, and a custom
Dialer rejects loopback / RFC1918 / link-local addresses to close the
DNS-rebinding TOCTOU window. Redirects re-validated.
- S3 + #9: manifest Name pinned to ^[a-z0-9][a-z0-9_-]{0,63}$, Entrypoint
and UI tab assets validated against absolute / "..", NUL byte, backslash
and non-canonical paths. Asset handler hardened with filepath.Rel check
for symlink and prefix-without-separator escapes.
- S5: pluginBridge postMessage handler ignores the pluginId in the message
body and uses an e.source -> contentWindow lookup instead, defeating
spoofed messages from same-origin scripts.
- S8: HTTPDo body capped at 5 MiB via io.LimitReader, redirects bounded
to 5 hops.
Correctness
- Critical seq alignment: PersistEvent now takes the hub-assigned seq as a
required parameter so the events table row seq always matches the wrapped
payload seq. Hub seeds its in-memory atomic counter from MAX(events.seq)
on startup. Drops in the persister queue no longer mis-align row vs
payload seq.
- #1: live plugin.Registry constructed in main.go BEFORE NewRouter and
threaded through; admin handler is no longer wired with nil.
- #3: EventPersister.Stop is now safe to call without a prior Start by
tracking a started flag — previously deadlocked waiting on done.
Wiring
- NewRouter signature gains *plugin.Registry; two test callers updated.
- admin.RequireAdminAuth exported as a thin wrapper over the existing
package-private adminAuthMiddleware.
- sqlc query templates updated for the new PersistEvent + GetMaxEventSeq
contracts (sqlite + postgres).
https://claude.ai/code/session_01UsBsQW2YiA2usk9pnJjAWk
ChatDeps, PresenceDeps, ReactionDeps no longer carry *db.DB or
*permissions.Checker — those were only needed before the service
migration. VoiceDeps retains them for voice handlers not yet migrated.
https://claude.ai/code/session_01CBFF3r84ywkJRWwuqw8zD8
- upload_handler: uses PermissionService.HasChannelPerm (removes last
hasChannelPermREST usage)
- profile_handler: delegates to UserService with proper ErrConflict on
duplicate username
- topic_rate_limiter: per-topic throughput caps (100 msg/s default),
wired into deliverBroadcast for channel-scoped broadcasts
- voice_join: subscribes client to VoiceTopic on join
- voice_leave: unsubscribes client from VoiceTopic on leave
https://claude.ai/code/session_01CBFF3r84ywkJRWwuqw8zD8
- upload_handler.go: uses PermissionService.HasChannelPerm instead of
the deleted hasChannelPermREST helper
- profile_handler.go: delegates to UserService for profile updates,
password changes, session listing, and session revocation
- Remove hasChannelPermREST from channel_handler.go (no longer needed)
- UserService.UpdateProfile now returns ErrConflict on duplicate username
https://claude.ai/code/session_01CBFF3r84ywkJRWwuqw8zD8
Critical fixes:
- Move svc creation in router.go above MountInviteRoutes/MountChannelRoutes
(was used before definition — compile error)
- Restore hasChannelPermREST in channel_handler.go for upload_handler.go
(was removed but still referenced — compile error)
Permission cache invalidation:
- Add PermissionInvalidator interface to admin package
- Wire through NewHandler → NewAdminAPI → handlePatchUser
- Call InvalidateUser(userID) after role changes in admin panel
- Update all admin test files to pass nil as new parameter
Also clarifies WithTx documentation for SQLite single-writer semantics.
https://claude.ai/code/session_01CBFF3r84ywkJRWwuqw8zD8
Client now has three send channels:
- sendHigh (64 slots): DMs, mentions — drained first by writePump
- send (256 slots): chat messages, reactions — drained second
- sendLow (64 slots): typing, presence — drained last, dropped on overflow
writePump drains high-priority messages before checking normal/low.
PubSub gains PublishHigh/PublishLow alongside existing Publish.
EmitEvents routes events by priority:
- High: SequencedDMEvent, UserTargetedEvent
- Normal: ChannelEvent, VoiceChannelEvent
- Low: ExcludeSenderEvent (typing), PresenceEvent
Slow clients get typing/presence dropped first (sendLowMsg silently
drops), then disconnect on normal buffer overflow, ensuring DMs are
never lost to typing indicator backpressure.
https://claude.ai/code/session_01CBFF3r84ywkJRWwuqw8zD8
PublishLowPriority uses trySendMsg to silently drop messages when a
client's buffer is full, instead of disconnecting them. This provides
priority-based backpressure: chat messages use normal Publish (disconnect
on overflow), while ephemeral events like typing indicators and presence
updates use PublishLowPriority (drop on overflow).
https://claude.ai/code/session_01CBFF3r84ywkJRWwuqw8zD8
- Add UserService, DMService, InviteService, BlockService
- Migrate REST handlers (channel, DM, invite, block) to use services
- Remove block_handler.go (merged into dm_handler.go)
- Update all services to accept store.Store instead of *db.DB
- Router creates SQLiteStore and passes to service.New()
Handlers are now thin HTTP adapters: parse request → call service →
map error → write JSON. All business logic lives in the service layer.
https://claude.ai/code/session_01CBFF3r84ywkJRWwuqw8zD8
When a client focuses a channel, subscribe to its pub/sub topic.
When switching channels, unsubscribe from the old topic first.
This completes the pub/sub integration — channel broadcasts now
route only to clients subscribed to the relevant topic.
https://claude.ai/code/session_01CBFF3r84ywkJRWwuqw8zD8
Introduce topic-based PubSub for O(subscribers) message routing:
- Clients subscribe to "global" and "user:{id}" on connect
- Channel broadcasts route through "channel:{id}" topics
- deliverBroadcast() uses PubSub instead of iterating all clients
- UnsubscribeAll on disconnect/kick cleans up subscriptions
- Sequence numbering and replay buffer preserved unchanged
https://claude.ai/code/session_01CBFF3r84ywkJRWwuqw8zD8
Define the Store interface composing domain-specific sub-interfaces
(MessageStore, ChannelStore, UserStore, etc.) that decouple services
from the concrete database. SQLiteStore wraps *db.DB, delegating all
operations to existing query methods.
https://claude.ai/code/session_01CBFF3r84ywkJRWwuqw8zD8
Handlers now delegate all business logic (validation, permission checks,
DB operations) to MessageService and ChannelService instead of calling
*db.DB directly. This eliminates logic duplication and enables the
service layer's permission cache.
https://claude.ai/code/session_01CBFF3r84ywkJRWwuqw8zD8
Introduce Server/service/ package with MessageService, ChannelService,
and PermissionService that encapsulate business logic previously
scattered across REST and WS handlers. The PermissionService adds
per-user in-memory caching with TTL-based expiry to eliminate
per-message DB round-trips at scale.
Services are wired into the WS hub via deps structs (strangler-fig
pattern) — existing handlers continue to work unchanged, with service
references available for incremental migration.
https://claude.ai/code/session_01CBFF3r84ywkJRWwuqw8zD8
- Remove unused `ver` param from handleHealth and handleInfo (version
was intentionally removed from unauthenticated endpoints per C-2)
- Rename decodeBase64Loose to validateBase64Loose returning only error,
matching actual usage (all callers only validate, never use the bytes)
- Remove commented-out code flagged by gocritic
- Use bytes.Equal instead of string conversion comparison
- Remove unused buildRateLimitError function
- Remove unnecessary type assertions in e2eeCrypto.ts
- 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
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.
Periodic key rotation:
- Key holder rotates room key every 5 minutes for forward secrecy,
independent of participant changes. Timer managed by key holder only.
Offer retry mechanism:
- Non-key-holders re-announce their public key after 10s timeout and
wait 5s more before giving up. Covers lost offers from target
disconnect during async key wrapping.
- Key holder now re-sends room key offer on duplicate announces (peer
may be re-requesting after a missed offer), instead of ignoring them.
Key fingerprint verification:
- New computeKeyFingerprint() in e2eeCrypto.ts — SHA-256 hash of raw
public key formatted as "AB12 CD34 ..." for out-of-band verification.
Can be displayed in UI for MITM detection.
Server hardening:
- Public key size limit tightened from 256 to 128 bytes (P-256
uncompressed = 65 bytes = ~88 base64 chars).
Client hardening:
- WebCrypto availability check at module load — throws descriptive
error if crypto.subtle is unavailable (non-HTTPS context).
- base64ToUint8() now wraps atob() in try-catch with clear error message.
https://claude.ai/code/session_01KKo3RwjdmcNzkgXNfUkgNT
Critical fixes:
- C1: Key holder election now uses lowest-user-ID from voiceStore instead
of "am I first in peerPublicKeys" heuristic, preventing simultaneous
join race where both participants generate conflicting room keys
- C2: TOCTOU race in handleVoiceE2EEOffer — target channel check now
happens inside h.mu.RLock() section (atomically with client lookup)
- C3: Server now validates base64 encoding for public_key, encrypted_key,
and iv before relaying, preventing client-side DoS via malformed payloads
High fixes:
- H1: ECDH keypair regenerated on reconnect with fresh announce, so
stale keys don't persist and key rotation during disconnect is handled
- H2: E2EE epoch counter prevents stale offers from overwriting a
rotated room key (handleE2EEOffer discards if epoch changed during unwrap)
- H3: After key rotation, re-check for peers that arrived during the async
wrapping loop and send them the new key too
- H4: _ecdhKeyPair and _roomKey captured in local vars before async
operations to prevent null dereference if clearE2EEState runs concurrently
Medium fixes:
- M1: User notified via onErrorCallback when E2EE key exchange times out
- M2: Timeout timer properly cleared to prevent leak and unhandled rejection
- M3: Duplicate announces deduplicated — same key ignored, changed key logged
https://claude.ai/code/session_01KKo3RwjdmcNzkgXNfUkgNT
- Use deterministic key holder election (lowest user_id) instead of
Map insertion order which is not guaranteed to match join order
- Use parseUserId() instead of raw parseInt() for LiveKit identity parsing
- Add concurrent key rotation guard (_rotatingKey flag) to prevent
races when multiple participants leave in rapid succession
- Queue voice_e2ee_announce messages that arrive before ECDH keypair
is ready; drain after keypair generation in connectAndSetup
- Propagate decryption failures to roomKeyResolver so connectAndSetup
unblocks with an error instead of hanging
- Reject (not resolve) roomKeyResolver on voice leave for proper cleanup
- Convert dynamic await import("@lib/e2eeCrypto") to static imports
- Add VOICE_E2EE_ANNOUNCE/OFFER to protocolTypes.ts enum constants
- Use typed S.VOICE_E2EE_* constants in dispatcher instead of string casts
- Add payload size limits for encrypted_key (1024) and iv (128) on server
https://claude.ai/code/session_01KKo3RwjdmcNzkgXNfUkgNT
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
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
- docker-compose.yml: owncord + livekit/livekit-server on shared network
- Secrets (API key/secret) injected via .env → OWNCORD_VOICE_* env vars
- livekit.yaml.example: template config with port ranges and node_ip guidance
- .env.example: secret template with min-length reminder for API secret
- .gitignore: add .env / Server/.env to prevent accidental secret commits
Users: cp .env.example .env && cp livekit.yaml.example livekit.yaml,
fill in values, then docker compose up -d
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
- 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
- release.yml: integrate signing/manifest/changelog steps with new
multi-platform artifact layout (windows/ + linux/ dirs)
- updater.go: combine Linux tar.gz support with existing signature
verification; merge platform-aware asset matching into switch
- updater_test.go: keep PR Linux tests + dev signing/manifest tests
- G204 in proc_spawner_nix.go and proc_spawner_win.go: exePath is the
server's own validated binary path, not arbitrary user input
- G302 in updater.go: 0o755 is required for the extracted Linux binary
to be executable
- 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
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.