Commit Graph
298 Commits
Author SHA1 Message Date
J3vb 16fbfcce87 fix(client): bump vite to patch high-severity CVEs (GHSA-4w7w-66w2-5vf9, GHSA-p9ff-h696-f583)
npm audit fix resolved vite <=6.4.1 → 6.4.2; 0 high/critical vulnerabilities remain.
2026-04-07 09:10:01 +02:00
Claude 1c476ccb58 merge: reconcile sister branch claude/plan-phases-b-c-bGpoS
Resolves the parallel Phase B/C work that landed on the sister branch
while this branch was in review. Both branches independently implemented
real OTel + Wazero runtimes; this merge keeps the best of each.

Conflict resolution
- Server/plugin/sandbox_wazero.go: rewritten as a hybrid. Keeps the
  HEAD lifecycle (eager `platformInit` with WASI preview-1, explicit
  `platformDeactivate` per-instance, runtime closed via Registry.Close)
  AND adopts the sister branch's richer artefacts:
    * `WithMemoryLimitPages` actually enforces `cfg.MaxMemoryMB`,
    * the JSON-over-linear-memory ABI
      (`allocate` / `command_dispatch(ptr,len) → (ptr,len)` /
      `deallocate`),
    * `listExportedCommands` auto-binds commands the plugin exports
      via `list_commands` at activation time (capability-gated).
- Server/plugin/registry.go: kept the HEAD `activate()` snapshot
  pattern (read `runtimePlatform` under RLock, pass into
  `activateWithRuntime` as a parameter) so a concurrent Close can't
  race the wazero call. Sister branch's LoadAll stale-staging cleanup
  and UninstallPlugin on-disk dir removal came in via auto-merge.
- Server/telemetry/telemetry_otel.go: kept the HEAD implementation
  (race-fixed AppMetrics rebind, uint64 overflow guard, idempotent
  shutdown, trace-provider cleanup on prom failure) and wired in the
  sister branch's `OTLPInsecure` config field for plaintext gRPC opt-in.
- Server/go.mod: accepted sister branch's `BurntSushi/toml v1.6.0`
  for the new TOML manifest support.
- Client/tauri-client/vitest.config.ts: union of both globs
  (`tests/**/*.test.ts`, `src/**/*.test.ts`, `src/**/*.test.tsx`).
- PHASE_BC_LOCAL_TODO.md: combined the two checkbox histories;
  TOML manifest, hello.wasm fixture, and OTLPInsecure are all marked
  done now.

Sister branch additions accepted via auto-merge
- Server/plugin/examples/hello/{hello.wasm,main.go}: precompiled 925
  KiB TinyGo plugin with the full ABI (allocate, deallocate,
  list_commands, command_dispatch, on_event).
- Server/plugin/manifest_{toml,nottoml}.go: TOML manifest parser
  behind the wazero build tag, JSON fallback elsewhere.
- Server/plugin/loader.go: prefers `plugin.toml`, falls back to
  `plugin.json`.
- Server/api/plugins_handler.go: structured error responses + slog.
- Server/main.go, Server/config/config.go: OTLPInsecure plumbing,
  defaults polish.
- docs/{contributing.md,server-configuration.md}: documentation
  updates.

Test status
- `go build` passes on default, -tags otel, -tags wazero, and
  -tags otel,wazero.
- `go vet` passes on every tag combination.
- `go test ./...` passes on default and on -tags otel,wazero.
- Client: `npx tsc --noEmit` clean; vitest 3188/3188 across 112 files.

https://claude.ai/code/session_01AZni6CDSQeu67WSWY1YCDX
2026-04-07 06:23:09 +00:00
Claude 47d848ee0a feat(phase-bc): implement real OTel + Wazero runtimes; harden install path
Phase B Step 8 (OpenTelemetry) and Phase C Step 9 (Wazero plugin runtime)
were structurally scaffolded but the tagged builds were placeholders that
errored at runtime. This commit lands the real implementations behind the
existing build tags, plus three review passes worth of fixes across the
plugin admin handler, plugin registry, telemetry adapter, and Solid client.

Telemetry (Phase B Step 8)
- Add real go.opentelemetry.io/otel{,/sdk,/exporters/{prometheus,otlp...}}
  modules to go.mod plus contrib/instrumentation/net/http/otelhttp.
- Replace the telemetry_otel.go skeleton with a working Provider that
  wires Prometheus + OTLP/gRPC exporters, otelhttp middleware, span and
  meter adapters, and an idempotent Shutdown.
- AppMetrics cache is now reset *before* SetGlobal to close a race where
  a concurrent NewAppMetrics() could observe a swapped provider but read
  stale no-op instruments.
- Init releases the trace provider on a later prometheus exporter
  failure so Init never leaks gRPC connections.
- convertAttrs handles int32/uint/uint32/uint64/float32 explicitly;
  uint64 values that exceed math.MaxInt64 fall back to a STRING attr
  rather than wrapping into a negative int64 and corrupting metrics.
- Tests under -tags otel cover the prometheus scrape, span lifecycle,
  histogram recording, shutdown idempotency, AppMetrics rebind, and
  the uint64 overflow fallback.

Plugin runtime (Phase C Step 9)
- Add github.com/tetratelabs/wazero v1.11.0 to go.mod.
- platformInit creates a shared wazero.Runtime with WASI preview1
  pre-instantiated; activateWithRuntime compiles + instantiates each
  plugin module under that runtime; platformDeactivate closes per-
  plugin modules without tearing down the runtime.
- DisablePlugin now calls platformDeactivate so the wazero module is
  freed immediately instead of leaking until registry Close.
- activate() captures runtimePlatform under r.mu.RLock and passes it as
  a parameter to activateWithRuntime; the call no longer re-reads the
  field, closing a race with concurrent Close.
- invokeCommand calls the plugin's command_dispatch export when
  present; missing/broken exports return a user-facing diagnostic
  instead of crashing the dispatcher.
- Tests under -tags wazero cover registry creation, module compilation,
  re-enable after disable (verifies the leak fix), close-twice safety,
  invalid wasm rejection, and DispatchCommand with a missing export.
  Fixture is a 41-byte embedded add.wasm; no external asset required.

Plugin admin handler hardening
- /api/v1/admin/plugins/install now rejects uploads whose multipart
  Content-Type is not application/zip|x-zip-compressed|octet-stream
  (415) and uploads whose body lacks the PK\\x03\\x04 / PK\\x05\\x06
  zip magic (400). The 16 MiB cap and registry-side zip-slip / symlink
  / size-bomb defences are still applied as before.
- New plugins_handler_test.go covers list-empty, install-503-when-nil,
  content-type rejection, magic rejection, happy path, lifecycle 503,
  invalid id, and isZipContentType / hasZipMagic helpers.

Solid client (Phase B Step 6) cleanup
- vitest.config.ts now wires vite-plugin-solid and broadens the test
  glob to include src/**/*.test.tsx so Badge.test.tsx is actually
  discovered (it was silently skipped).
- pluginBridge.ts targets postMessage at window.location.origin
  instead of "*", and exposes a destroy() that detaches the message
  listener and clears mounted frames.
- solidMount.ts imports the JSX type from "solid-js" instead of
  "solid-js/web" (the latter does not re-export it), unblocking
  npx tsc --noEmit.

Build/test status
- go build succeeds on default, -tags otel, -tags wazero, and
  -tags otel,wazero.
- go test passes on every tag combination across telemetry, plugin,
  api, ws, service, store, and the rest of the tree.
- Client: npx tsc --noEmit clean; vitest 3188/3188 across 112 files.

PHASE_BC_LOCAL_TODO.md is updated to mark the OTel modules + real Init,
the wazero module + real platformInit, and the test coverage that
landed in this commit as completed.

https://claude.ai/code/session_01AZni6CDSQeu67WSWY1YCDX
2026-04-06 21:46:22 +00:00
J3vb ae496082b3 feat(phase-bc): TOML plugin manifests + Solid vitest preset
Plugin TOML support (-tags wazero):
- manifest_toml.go: tryLoadPluginTOML using BurntSushi/toml v1.6.0
- manifest_nottoml.go: no-op stub for default build
- loader.go: prefers plugin.toml, falls back to plugin.json

Solid vitest preset:
- vitest.config.ts: add vite-plugin-solid, expand include to
  src/components/solid/**/*.test.tsx — Badge.test.tsx now runs
  automatically as part of npm test (112 files / 3188 tests)
2026-04-06 23:25:16 +02:00
J3vb 031f20fb96 fix(phase-bc): Solid.js npm install — fix JSX import and lint error
- npm install: pulled solid-js, vite-plugin-solid, @solidjs/testing-library
- Fix solidMount.ts: import JSX from "solid-js" not "solid-js/web"
- Fix PluginContainer.tsx: suppress no-unassigned-vars for Solid ref pattern
- Build green (tsc + vite); 111 test files / 3186 tests all pass
2026-04-06 23:05:50 +02:00
Claude 46aeccc49b fix(phase-bc): address review findings — auth, SSRF, seq alignment
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
2026-04-06 09:29:29 +00:00
Claude a2cb224323 feat: scaffold Phase B + C (events, telemetry, plugins, Solid.js)
Phase B Step 6 — Solid.js incremental migration
  - vite-plugin-solid + solid-js + @solidjs/testing-library in package.json
  - vite.config.ts compiles src/components/solid/** as Solid TSX
  - tsconfig.json gains jsx: preserve / jsxImportSource: solid-js
  - lib/solidAdapter.ts wraps existing custom Stores as Solid signals
  - lib/solidMount.ts adapts Solid render to {mount,destroy} contract
  - components/solid/Badge.tsx (proof-of-concept leaf)
  - components/solid/ChannelListItem.tsx (store-subscribed leaf)
  - components/solid/Badge.test.tsx pipeline smoke test
  - components/solid/README.md documents the migration recipe

Phase B Step 7 — Event persistence layer
  - SQLite + Postgres migrations for the events table
  - sqlc query files for both engines
  - EventStore interface + SQLite raw-SQL impl + MemStore impl + pg stubs
  - ws.EventPersister: async batched writer (queue / flush / drain / drop)
  - ws.StartEventPruner: background retention pruner
  - hub persists every replay-buffer push and exposes reconnect-tier counters
  - serve.handleReconnect: tiered replay (buffer -> DB -> full re-sync)
  - EventPersistenceConfig + main.go wiring
  - event_persister_test.go covers batching / drops / drain

Phase B Step 8 — OpenTelemetry skeleton
  - Server/telemetry package with public Provider/Tracer/Meter/Counter API
  - telemetry_default.go (no-op build) + telemetry_otel.go (build tag otel)
  - telemetry/metrics.go declares the AppMetrics bundle
  - HTTPMiddleware mounted in Chi router (pass-through in default build)
  - PrometheusHandler optionally mounted at /metrics
  - Spans on MessageService.SendMessage, PermissionService.HasChannelPerm,
    ChannelService.ListVisibleChannels
  - Reconnect-tier counter wired into the global meter
  - TelemetryConfig defaults

Phase C Step 9 — Wazero plugin runtime skeleton
  - Server/plugin package: manifest parser, loader, registry, host APIs
    (commands, storage, events, http, ui), errors
  - sandbox_default.go (no-op) + sandbox_wazero.go (build tag wazero)
  - SQLite + Postgres migrations for plugins + plugin_kv tables
  - PluginStore interface + impls + pg stubs
  - plugin/examples/hello manifest + README
  - plugin_test.go covers manifest, loader, capability gating
  - api/plugins_handler.go admin REST surface, mounted under admin group
  - PluginsConfig + main.go wiring (disabled by default)
  - Client: lib/pluginBridge.ts iframe + postMessage host
  - Client: components/solid/PluginContainer.tsx Solid host component

Verification
  - Default build (no -tags) is intended to compile cleanly with no new
    third-party dependencies. The sandbox lacked Go 1.25.0 so go build
    could not run; PHASE_BC_LOCAL_TODO.md enumerates the local follow-up
    work (npm install, go mod tidy, sqlc-generate, real otel/wazero
    wiring, remaining service spans, full Solid migration).
2026-04-06 09:00:47 +00:00
J3vb 34c8e12d0f fix: preserve isKeyHolder in pendingJoin drain loop
PendingVoiceJoin was missing the isKeyHolder field, so when the drain
loop called connectAndSetup for a queued join it defaulted to false,
entering the "wait for room key from key holder" E2EE path and hanging
indefinitely. Store isKeyHolder in the pending join and forward it.
2026-04-05 20:41:04 +02:00
J3vb e66cb7413b fix: update test mocks for createLogger, ExternalE2EEKeyProvider, and keybinds string
- e2eeCrypto.test: mock createLogger instead of log
- livekit-session.test: add ExternalE2EEKeyProvider to livekit-client mock
- keybinds-tab.test: update expected string to "Press a supported key..."
2026-04-05 19:57:01 +02:00
J3vb ef2dee4adc fix: resolve floating promise ESLint errors in livekitSession.ts
- void localRoom.disconnect() in error handler
- void this.rotateKeyPeriodically() in setTimeout callback
2026-04-05 19:46:41 +02:00
J3vb e2f8858019 fix: resolve CI lint and ESLint failures
- 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
2026-04-05 19:37:00 +02:00
J3vb 6e4a007b91 refactor: migrate WS handlers to V2 Command/Event architecture
Strangler-fig migration of 15 WebSocket handlers from V1 (Hub method,
*Client) to V2 (pure functions: Command, ClientInfo, deps -> Result).
V2 handlers are testable without a running Hub and produce declarative
Result values that the dispatch loop applies.

New abstractions:
- Command interface + typed constructors with input validation
- 7 Event routing interfaces (Channel, ExcludeSender, SequencedDM,
  UserTargeted, BroadcastAll, VoiceChannel, VoiceChannelGuarded)
- Per-domain deps structs (PingDeps, ChatDeps, PresenceDeps,
  ReactionDeps, VoiceDeps) with interface-based DI
- EmitEvents router matching events to delivery mechanisms
- DispatchV2 with panic recovery and runtime.Stack logging

Security hardening:
- Pre-sanitize byte length guard before bluemonday (DoS prevention)
- GetRoleForUser single-JOIN query avoids password hash on hot path
- channel_id positivity enforced in all command constructors
- Log injection prevention: msgType/reqID capped to 64 chars
- Nil KeyHolder dep returns ErrCodeInternal (not silent bypass)
- VoiceChannelGuardedEvent atomic check-and-send under h.mu.RLock

V1-only (complex state/mutex requirements): voice_join, voice_leave.

All tests pass with -race. No CI regressions expected.
2026-04-05 19:03:22 +02:00
J3vb 5239a75642 Merge pull request #1126 from J3vb/claude/security-audit-full-mmIrL
Force merge: CI issues will be fixed in upcoming Command/Event architecture refactor
2026-04-05 11:32:26 +02:00
J3vb f841ba87d7 fix: resolve tsc errors — logger import, Uint8Array generics, VoiceTokenPayload type, test assertion 2026-04-04 23:40:37 +02:00
J3vb 36e81ebec7 fix: address code review IMPORTANT issues — atomic TOCTOU, updateKeyHolder race, dead keyReceived var 2026-04-04 23:30:36 +02:00
J3vb 59aa5a7808 fix: wire is_key_holder from server payload through dispatcher to handleVoiceToken 2026-04-04 23:25:41 +02:00
J3vb 6a29a3a143 fix: TypeScript E2EE hardening — key holder from server, base64, timeout, session fixes
- M-1: Replace uint8ToBase64 string concat loop with Array.from().join()
- M-3: Remove non-null assertion on hex.match() in computeKeyFingerprint
- M-4: Replace TextEncoder module-level side effects with precomputed Uint8Array literals
- C-2/I-4: Read is_key_holder from voice_token payload; remove voiceStore-based key holder election
- I-5: Hard fail on E2EE timeout — call leaveVoice() and emit e2ee_timeout error instead of proceeding without E2EE
- C-3: Disconnect localRoom in connectAndSetup catch block to prevent resource leaks
- I-3: Atomic reconnect state transition already handled via setReconnectAc (no change needed)
2026-04-04 23:23:54 +02:00
J3vb 50bb54510f test: add e2eeCrypto unit tests — wrap/unwrap, fingerprint, entropy 2026-04-04 23:20:09 +02:00
J3vb f962d59cd6 fix: update tests and fix pending-join drain regression for CI
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.
2026-04-04 21:50:57 +02:00
Claude 774a7bcce9 fix: remaining E2EE hardening — rotation, retry, fingerprint, validation
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
2026-04-04 19:43:17 +00:00
Claude 277c2d3e76 fix: address critical E2EE review findings — races, validation, reconnect
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
2026-04-04 19:35:41 +00:00
Claude b89a7efa6a fix: harden E2EE key exchange — election, rotation, queuing, error handling
- 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
2026-04-04 19:13:51 +00:00
Claude 0c4d9f702c feat: implement true E2EE for voice via client-side ECDH key exchange
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
2026-04-04 19:02:21 +00:00
Claude 1d8bfe2d6a fix: revert breaking security changes and update tests for version removal
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
2026-04-04 18:02:26 +00:00
Claude 1673c37b9c fix: comprehensive security hardening from full codebase audit
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
2026-04-04 16:48:57 +00:00
Claude 330fd8eed7 feat: add end-to-end encryption for voice/video via LiveKit SFrame
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
2026-04-04 16:34:52 +00:00
J3vb 321428c7f4 feat: security hardening - restrict PTT capture keys, gate devtools to DEV, fix race conditions and DM sequencing
- 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
2026-04-04 09:08:01 +02:00
J3vb c3a8aa477c fix: resolve 20 code review bugs across Rust, TypeScript, and Go
Critical/High Rust (Tauri client):
- BUG-140: replace .run() with .build() + RunEvent::Exit handler; native error dialog on startup failure
- BUG-141: eliminate PTT thread TOCTOU race with Mutex critical section; add AtomicBool shutdown and catch_unwind
- BUG-144: fix TOFU cert store corruption — read-before-write rollback restores previous fingerprint on save failure (all 3 write sites)
- BUG-145: add VK code range guard (1..=254) in is_key_down; fix cast to (state as i16) < 0
- BUG-147: replace bare spawns with JoinSet; abort_all + drain on exit; unconditional closed event
- BUG-150: add CRLF guard in handle_connection before header rewriting
- BUG-151: wrap header read loop in tokio::time::timeout(10s)
- BUG-158: extract CERTS_STORE/SETTINGS_STORE to constants.rs (eliminate 3 duplicates)
- HIGH-2: PTT thread self-cleanup uses unwrap_or_else defensive pattern
- HIGH-4: ws_send distinguishes Full vs Closed errors; warn log on backpressure

Critical/High TypeScript (Tauri client):
- BUG-142: join-generation counter prevents stale connectAndSetup completions
- BUG-143: replace 8 mutable LiveKit session fields with discriminated union SessionState
- BUG-146: 60s token refresh deadline; cleared on reply or voice leave
- BUG-148: ResizeObserver hoisted to outer scope; disconnect() in destroy() before ac.abort()
- BUG-152: dismissSignal.aborted guard already present (no change needed)
- BUG-153: measureRendered split into two-pass read-then-write; eliminates per-message reflow
- BUG-154: WS dedup cache batch-evicts to 80% on overflow (amortised O(1))
- BUG-157: pendingUpdates replaced with coalesced function-composition slot (O(1) queue depth)

Go server:
- BUG-149: safe two-value type assertion in getOutboundIP with localhost fallback
- BUG-155: broadcast buffer 256→1024; broadcastDrops atomic counter exposed in /api/v1/metrics
- BUG-156: LiveKitHealthCheck and implementations accept ctx context.Context; all call sites pass r.Context() (12 files)
- BUG-159: MaxMessageBytes constant in config/constants.go; replaces 1<<20 literals in serve.go and updater.go
- HIGH-1: cert store rollback reads old value before write; restores previous cert on save failure

All validation passes: go build, go vet, cargo check, npm typecheck
2026-04-03 23:18:06 +02:00
j3vbandClaude Sonnet 4.6 dcbcc09777 fix: update device_query v2 Keycode variants in ptt.rs for Linux
Return→Enter, Meta→LMeta|RMeta, remove NumLock/ScrollLock (not in v2).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 17:51:32 +02:00
J3vb 1fccb1cb9f fix: format gif-provider.test.ts with Prettier 2026-04-03 16:59:10 +02:00
J3vb ae6ee0b06d fix: correct docker/setup-buildx-action SHA and update TenorGif to GifResult in tests
- 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
2026-04-03 16:49:07 +02:00
J3vb 03969575e0 feat: Linux client port, ARM64 CI, and server Docker image
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
2026-04-03 14:26:26 +02:00
J3vb 0a08fd72bd feat: migrate GIF picker from Tenor to Klipy
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
2026-04-03 13:58:40 +02:00
J3vb 4393bf1349 fix: remove duplicate setScreenshareAudioVolume in video-grid mock 2026-04-03 12:43:24 +02:00
J3vb 5b5fb708f3 feat: add server Linux support (#105)
PR#88 Added server linux support and changed workflow
2026-04-03 12:42:30 +02:00
J3vb 0b1d178d31 fix: wire parseChecksumFileAny for Linux checksum lookup and add missing mock export
- 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
2026-04-03 12:27:33 +02:00
J3vb 0fab93361c fix: resolve CodeQL code scanning alerts
- 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
2026-04-03 11:47:44 +02:00
J3vbandClaude Sonnet 4.6 dc7859d284 fix: resolve Tauri full build TypeScript errors from tauri-typegen
- 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>
2026-04-03 11:10:40 +02:00
J3vb d4f254b945 fix: bump rustls-webpki 0.103.9 → 0.103.10 (RUSTSEC-2026-0049) 2026-04-03 10:40:09 +02:00
J3vb 9a7318dd35 fix: resolve Clippy -D warnings in Tauri client
- Remove unused `use tauri::Manager` in commands.rs
- Remove dead hotkeys.rs module (register_push_to_talk, unregister_all never called)
- Fix `mut cred` → immutable in credentials.rs (CredWriteW takes &CREDENTIALW)
2026-04-03 10:21:06 +02:00
J3vb f1eee8dfc3 fix: add setScreenshareAudioVolume to livekitSession mocks in video tests 2026-04-03 09:05:18 +02:00
J3vb 485040be32 security: validate avatar URLs on server and client
- 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
2026-04-03 08:10:26 +02:00
J3vb 5dbb89f237 feat: auto-grant microphone permission in WebView2 via --use-fake-ui-for-media-stream 2026-04-02 23:41:01 +02:00
J3vb 4ffd6731c2 fix: resolve CI failures from invalid golangci-lint SHA and ESLint unknown rules
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.
2026-04-02 17:32:36 +02:00
J3vb 2762e9a4ef chore: remove soundboard feature entirely
Soundboard was never implemented — remove USE_SOUNDBOARD permission bit,
rate limiter, protocol entry, admin mockup reference, TODOS entry, and
all related test assertions.
2026-04-02 17:00:27 +02:00
J3vb 7cdc2ef1ca feat: broadcast username changes to all connected clients via WebSocket
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.
2026-04-02 16:33:11 +02:00
J3vb 9f74710d91 fix: send invite code instead of numeric ID for revoke endpoint
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.
2026-04-02 15:41:25 +02:00
J3vb 1754046c7d fix: guard replaceTrack with pipeline generation counter (BUG-106)
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.
2026-04-02 13:47:22 +02:00
J3vb 827b370ac1 fix: audio cleanup srcObject, diagnostics rate limit, orphan cleanup race (BUG-107, BUG-121, BUG-132)
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.
2026-04-02 13:45:41 +02:00
J3vb e9543ce6d6 fix: video grid auto-opens only for local camera/screenshare (BUG-105)
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.
2026-04-02 13:42:00 +02:00