Commit Graph
24 Commits
Author SHA1 Message Date
J3vbandClaude Fable 5 f2966c2527 chore(server): delete production-dead code; move test helpers to export_test
Applied from a deadcode (RTA from mains, all build tags) sweep with
per-symbol adversarial verification:

Deleted (nothing but their own self-tests used them):
- admin.Handler (deprecated since Phase 6; production mounts NewHandler)
  plus its two self-tests
- ws.Hub.broadcastVoiceStateUpdate + wrapper + two self-tests (pre-V2
  leftover; the live voice_state path is the hub voice routines)
- ws.VoiceLeaveEvent + methods ('retained as scaffolding', never
  constructed in production; MsgTypeVoiceLeaveBC stays — live via the
  leave routine)
- ws.parseIdentity (production calls parseParticipantIdentity directly;
  ParseIdentityForTest now exercises the real parser)
- telemetry.Float64 (String/Int64 are used; the float case is covered by
  the otel-tagged internal test, re-addable when a caller appears)

Moved into export_test.go so they leave the production binary (all
callers are same-package tests): the eight ws test-client constructors
and voice/E2EE setters from ws/client.go, admin.SetBackupBaseDir
(new admin/export_test.go), api.SecurityHeaders (test-only wrapper;
production uses SecurityHeadersWithTLS — docs/api.md updated to the
real name). Client.getVoiceJoinToken/setVoiceChID inlined into their
existing ForTest wrappers; TestSetVoiceChID_* self-tests deleted.

Kept after verification: updater.SetBaseURL (11 cross-package test call
sites) and telemetry.resetAppMetricsForInit (live under -tags otel —
untagged deadcode false positive).

Full gate green: gofmt/vet, 4 build-tag variants, full suite, deadlock,
race.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 15:28:41 +02:00
J3vbandClaude Opus 4.8 e98c1d7cbc fix(ws): resolve live role in hasChannelPerm to honor mid-session demotions
hasChannelPerm resolved permissions from the connect-time role snapshot (c.user.RoleID), so a user reassigned to a lower role kept the old role's voice privileges (CONNECT_VOICE and the SPEAK/VIDEO grants in the LiveKit token) until reconnect. Resolve the current role via GetRoleForUser(c.userID), matching the V2 handlers. (Security scan F5)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 11:59:13 +02:00
J3vbandClaude Fable 5 249e35af91 feat(server): unify channel-visibility through permissions.Checker
Route the four "must mirror" READ_MESSAGES filters — REST
ListVisibleChannels, ws buildReady, reconnect replay
computeAllowedChannels, and hub RefreshChannelVisibility — through a
single permissions.Checker predicate so a drift can never leak a
private channel (audit A-2026-07-07 / backlog item 3).

- add permissions.Checker.VisibleChannelIDs + ChannelRef (skips dm,
  fails closed, admin bypass via HasChannelPermBatch)
- delegate the three batch sites; RefreshChannelVisibility uses
  HasChannelPerm instead of its inline EffectivePerms copy
- REST/WS agreement test asserting all three sites yield the identical
  non-DM set across admin / member-with-deny / denied-everywhere

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 11:28:03 +02:00
Claude 8c590c0b6d fix(server): harden private-channel visibility propagation
Three review findings on the #93 feature:

- RefreshChannelVisibility targeted clients by their connect-time role
  snapshot; a user whose role changed mid-session was evaluated against the
  stale role. Resolve the current role from the DB per client (fail closed).
- Visibility updates are targeted, unsequenced messages, so a client that
  disconnected before an override change and later resumed via replay never
  converged (stale sidebar until a fresh connect). Track a visibility-change
  sequence watermark and force resumes from at/before it onto the
  full-ready path.
- The admin SPA interpolated channel/user names into single-quoted JS
  strings inside onclick attributes with HTML-escaping only; a name
  containing a quote broke out of the string literal (XSS in the admin
  panel, reachable by any user allowed to create channels). Add a jsq()
  helper (JS-escape then HTML-escape) and use it for every onclick name
  interpolation.

Follow-up to #93.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwtnpHAoSFr1ZibQgQkNQK
2026-07-19 11:20:15 +00:00
J3vbandClaude Fable 5 522003ebe3 fix(ws): wire topicLimiter into hand-rolled test hubs
newEmitTestHub and NewHubForTest built raw Hub literals without the
topic rate limiter, so deliverBroadcast panicked on a nil receiver and
TestEmitEvents_ChannelEvent_CallsBroadcastToChannel could never pass.
CI never surfaced it because the pipeline died at govulncheck first.
Wire the limiter exactly as NewHub does; no production nil-guard, since
a nil limiter in production would silently disable rate limiting.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 11:45:30 +02:00
Claude 59ae4d8ad2 test+feat(phase-bc): pass 4 — test coverage, install endpoint, CHANGELOG
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
2026-04-06 10:30:18 +00:00
Claude 87055819b9 wire pub/sub channel subscriptions into channel_focus handler
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
2026-04-05 20:52:02 +00:00
J3vb 9f32736278 fix: resolve all 12 golangci-lint issues
- nilerr: mark intentional nil returns in DecryptTOTPSecret (backwards compat for unencrypted legacy secrets)
- gosec G703: suppress path traversal false positives in backup handlers (paths already sanitized by HasPrefix guard)
- contextcheck: thread context through handleWebhookParticipantJoined/Left; nolint goroutine in handleFreshConnect that intentionally detaches from request context
- errcheck: handle fmt.Fprintf return in handleWAFInterruption
- gocritic elseif: flatten else-if chain in upload_handler access check
- gocritic ifElseChain: rewrite asset name matching as switch in updater
- unparam: remove unused totpKey param from handleLogin (TOTP verification handled by separate handleVerifyTOTP endpoint)
2026-04-03 08:33:55 +02:00
J3vb 5edebb43b3 fix: add 30s session sweep to kick revoked WS connections (BUG-109)
Idle WebSocket connections only revalidated sessions every 10 sent
messages, allowing revoked tokens to stay connected indefinitely.
Add sweepRevokedSessions() on a 30s ticker that checks all connected
clients against the DB and kicks any with deleted/expired sessions
or banned users.
2026-04-02 12:12:56 +02:00
jevb 125512d591 fix: fail closed on role lookup in WS ready flow (BUG-094)
If GetRoleByID fails or returns nil during WebSocket connect, the
server now disconnects the client instead of serving a permissive
ready payload with all channels visible. In buildReady, nil role is
now treated as zero-access (no channels) instead of full-access.
Updated tests to pass explicit Owner role where channel visibility
is expected.
2026-04-02 11:29:10 +02:00
jevb cbc39d7ab5 fix: address code review findings from bug fix session
- voice_join.go: treat GetVoiceState DB error as switch failure instead
  of silently proceeding (HIGH: could bypass capacity check)
- voice_leave.go: move ctx to first parameter per Go idiom, remove
  nolint:revive directive (MEDIUM: style compliance)
- Update all call sites for new parameter order
2026-04-01 18:33:33 +02:00
jevb 32068e06f6 fix: resolve 4 server bugs (Phase 2: BUG-084, BUG-086, BUG-088, BUG-089)
- BUG-084: Broadcast filter now delivers channel messages to unfocused
  clients (channelID==0) instead of silently dropping them
- BUG-086: leaveVoiceChannelWithRetry retry goroutine respects context
  cancellation and hub stop to prevent leaks on shutdown
- BUG-088: Voice channel switch verifies old state is cleared before
  joining new channel, preventing capacity bypass on DB failure
- BUG-089: handleFreshConnect RemoveParticipant goroutine checks hub
  stop and documents identity-based targeting safety
2026-04-01 18:08:28 +02:00
jevb 2a62f31c39 test: boost server coverage — auth 60→95%, db 69→81%, config 75→85%
Add comprehensive tests across all Go packages:
- auth: username validation, concurrent rate limiting, TOTP stores, timing
- config: env overrides, default credential detection, voice defaults
- db: search, message queries, special char handling
- api: handler edge cases, error paths, DM/invite/TOTP coverage
- ws: voice handler paths, integration scenarios
- updater: version comparison, timeout handling

6 of 8 packages now at 80%+ coverage.
2026-04-01 11:38:11 +02:00
jevb a40b42bbed fix: resolve 24 critical and high issues from full code & security review
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).
2026-04-01 09:23:17 +02:00
jevb a0fd5e8fda security: fix 11 vulnerabilities from security review
Batch 1 — Immediate priority:
- C4: Atomic voice channel capacity (JoinVoiceChannelIfCapacity)
- H5: Sanitize emoji field with bluemonday (stored XSS)
- H8: Permission check before FTS search (timing oracle)
- M8: Filter ready payload channels by ReadMessages
- H10: Remove password/TOTP from admin ListAllUsers query

Batch 2 — Next sprint:
- C1: TOTP replay prevention (UsedTOTPCodeStore, 90s TTL)
- C2: Per-user TOTP brute-force rate limit (10/15min)
- C3: Delete requires SendMessages or ManageMessages
- H1: Expired sessions deleted on detection
- H3: Bearer token whitespace trimmed
- H6: Log warning when WS origin checking disabled
2026-03-31 19:10:42 +02:00
jevb deda095db1 test: add WS coverage tests + refactor handlers, update docs
Add hub, livekit, export, and coverage boost tests for Server/ws.
Refactor handlers_chat.go and serve.go for testability.
Sync docs: fix backup endpoint path, add DELETE /auth/account,
add audit logging + account deletion to security.md.
2026-03-31 18:07:23 +02:00
jevbandClaude Opus 4.6 b36c030cac feat: LiveKit video grid improvements, voice state cleanup, and internal tooling
- Video grid: sync stream type attribute on updates, add screenshare data attribute
- Dispatcher: handle voice_token messages, improve video track event handling
- LiveKit session: add video track publication support
- Hub: stale client timeout cleanup, improved voice state management
- Voice join/leave: context propagation, better error handling
- Livekit webhook: structured event handling with room/participant data
- Server DB: voice query improvements, new test coverage
- WS integration tests: expanded coverage for voice and LiveKit flows
- Gitignore: add internal dev tools directory, owncord-server.exe

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 11:41:59 +02:00
jevb 6b6a6fbea8 refactor: context propagation, LogAudit deadlock fix, ESLint v9, code quality
- Propagate context.Context from WS upgrade through all 17 handlers
- Add ExecContext/QueryRowContext/QueryContext/BeginTx to DB wrapper
- Fix LogAudit deadlock: move audit writes after tx.Commit to avoid
  SQLite write-lock contention (TestAdminAPI_PatchUser_UnbanUser)
- Add ESLint v9 with no-floating-promises, no-unused-vars
- Refactor livekitSession.ts: remove duplicate audio pipeline (267 lines)
- Add delete account UI tests (7 tests)
- Expand WS integration tests
2026-03-29 19:39:46 +02:00
jevb 6a5d3d53cb fix: address CEO review findings — health check, error logging, camera guard, metrics, tests
- Add LiveKit health check guard to voice_join (reject if process not running)
- Log webhook DB cleanup errors instead of swallowing
- Reject camera enable if CountActiveCameras query fails
- Add LiveKit health status to /metrics endpoint
- Add 13 unit tests for livekit.go, livekit_process.go, livekit_webhook.go
2026-03-22 19:29:42 +01:00
jevb 7978ec40e8 fix: security hardening, LiveKit class refactor, and eng review fixes
Server:
- Fix YAML injection in LiveKit config generation (quote values)
- Revert token TTL to 4h (no server-side JWT revocation)
- Derive LiveKit publish permissions from user role (prevent SFU bypass)
- Add CAS guard for webhook/voice_leave race condition
- Add voice_leave broadcast to rollbackVoiceJoin (prevent ghost state)
- Limit webhook body to 64KB (prevent memory abuse)
- Add rate limit to voice_token_refresh handler (1/60s)
- Add LiveKit health check endpoint (GET /api/v1/livekit/health, 503 on degraded)
- Add voice_token_refresh WS handler for client-initiated token refresh
- Consolidate voice quality constants (single source of truth)
- Fix video limit TOCTOU race (count from DB instead of LiveKit API)
- Raise default voice_max_video from 10 to 25 (Discord parity)
- Add CountActiveCameras DB query
- Non-blocking broadcast send, circuit breaker, exponential backoff
- Close send channel before context cancel in serve.go
- Guard voice mute/deafen for active channel
- Delete orphaned message on attachment link failure
- Redact query string from proxy logs (prevent token leak)
- Use instance-level HTTP client for health checks (no redirect following)
- Set cmd.WaitDelay to prevent goroutine leak on Windows
- Log buildJSON marshal errors

Client:
- Refactor livekitSession.ts from singleton module to LiveKitSession class
- Share single AudioContext for all analysers (was 1 per participant)
- Extract createRoom() helper (DRY)
- Add token refresh timer (3.5h interval, re-arms on failure)
- Skip setSpeakers if unchanged (sort in-place, no allocations)
- Distinguish user-initiated leave from connection error in retry
- Add YouTube videoId validation (prevent iframe src injection)
- Add try/finally to disableCamera
- Wrap store subscription callbacks in try/catch
- Track and cancel initial scroll RAF on cleanup
- Add 5s timeout + encodeURIComponent to YouTube oEmbed fetch
- Clean raw mic stream on RNNoise suppressor failure
- Full voice cleanup on logout via cleanupAll()

Tests:
- Add 7 new server tests (webhook parsing, voice guards, quality fallback)
- Fix 2 pre-existing test failures (mute/deafen invalid payload)
2026-03-21 11:59:14 +01:00
jevb 72d5412ba7 feat: rewrite server voice handlers for LiveKit (Phase 1)
Replace custom Pion WebRTC SFU with LiveKit token-based flow.

Server changes:
- client.go: remove PeerConnection, voiceDone, negoMu; add
  setVoiceChID/clearVoiceChID
- voice_handlers.go: 961 -> ~295 lines; voice_join now generates
  LiveKit token, voice_leave calls RemoveParticipant; delete
  SDP/ICE/RTP/soundboard handlers
- hub.go: replace SFU + VoiceRooms with LiveKitClient +
  LiveKitProcess; remove speaker broadcast goroutine
- messages.go: add buildVoiceToken, remove buildVoiceOffer/
  Answer/ICE; simplify buildVoiceConfig
- handlers.go: remove voice_offer/answer/ice/soundboard dispatch
- router.go: replace NewSFU with NewLiveKitClient, add optional
  LiveKit process auto-start

Deleted files (14):
- sfu.go, voice_room.go, speaker_detector.go, rtp_audio_level.go,
  speaker_broadcast.go, api/voice_handler.go
- All corresponding test files

All server tests pass (go test ./...).
2026-03-20 05:30:27 +01:00
jevb fca1b0cc0c test: boost server test coverage to 80%+ across all packages
Add comprehensive tests for db, storage, updater, and ws packages
covering edge cases, error paths, and voice handler functions.
New test files for attachment queries and WebSocket coverage boost.
2026-03-18 05:03:38 +01:00
jevb 8889faddef feat: add buildVoiceOffer and buildVoiceICE message builders
Add two new server-to-client WebSocket message builders following the
existing buildVoiceAnswer pattern. Expose them via export_test.go
wrappers and cover with ws_test package tests (TDD: RED → GREEN).
2026-03-18 03:44:38 +01:00
jevb 4d1a1676c7 feat: TOFU cert pinning, settings cache refactor, ban enforcement, and 80%+ test coverage
- Implement TOFU certificate pinning in Rust WS proxy with accept_cert_fingerprint command
- Refactor settings cache from package-level globals to Hub methods (eliminates global state)
- Add runtime ban check on WS message handling (kicks banned users mid-session)
- Sanitize reaction error messages to prevent IDOR information leaks
- Add slog error logging to REST handlers (channel, invite, search)
- Handle channel_delete for active channel in client dispatcher
- Add certMismatchBlock to prevent auto-reconnect on TOFU mismatch
- Consolidate root-level spec docs into docs/brain/06-Specs/ vault
- Add 80%+ test coverage for ws (80.9%) and admin (81.7%) packages
- Delete completed TODOS.md (all items resolved)
2026-03-17 11:05:52 +01:00