mirror of
https://github.com/J3vb/OwnCord.git
synced 2026-09-03 03:50:00 +03:00
4fa8f6f84da59fc5d9015aede8bb9ebdf56f06bf
25
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
49595e48d7 |
release: v1.1.0-alpha.4 — first-run setup wizard, LiveKit auto-download, WAF & CI fixes (#1292)
* fix(admin): accept same-origin first-run setup requests A freshly generated config.yaml leaves allowed_origins commented out, so the list is empty. The setup handler's CSRF guard assumed "no Origin header means same-origin", but browsers send Origin on same-origin POSTs too — Chrome and Edge always, Firefox since 70. The admin panel's own setup call is one of those POSTs, so every new install hit "cross-origin setup request blocked" and could never create an owner account. The guard now accepts a request whose Origin names the same host:port as the request's own Host header, falling back to the allowlist otherwise. That is what the original comment intended. CSRF protection is unaffected: a cross-site attacker cannot set Origin, the browser does, and a foreign origin still needs an explicit allowlist entry. Scheme is not compared. Nothing in this server derives the external scheme (no r.TLS or X-Forwarded-Proto handling exists anywhere), so a scheme check would reject legitimate requests behind a TLS-terminating proxy. Tests: isSameOrigin table covering port/host/suffix/schemeless/opaque-origin cases, plus two handler-level tests pinning both halves — same-origin succeeds against an empty allowlist, a foreign origin still 403s and creates no user. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat(identity): implement identity keypair caching and error handling * fix(client): use the real OS credential store, not keyring's mock (#1281) The `keyring` crate declares no `default` feature. Every platform arm in its lib.rs selects a backend only when that platform's feature is on and otherwise falls through to `pub use mock as default`, so the client's bare `keyring = "3"` compiled the in-memory mock store on Windows, macOS and Linux alike. The mock keeps its secret in the `Entry` object itself, and each command built its own `Entry`: save_identity_key -> Entry::new(..) -> set_password -> Ok(()) load_identity_key -> Entry::new(..) -> get_password -> NoEntry So a save reported success, the very next read in the same process returned nothing, `NoEntry` was mapped to `Ok(None)` so neither side logged anything, and no entry was ever written to Credential Manager on any machine. Downstream, the voice-E2EE identity keypair was regenerated on reconnect, the published identity key stopped matching the key that signed the announce, and peers correctly rejected it as a possible MITM. Name the platform backends explicitly, and stop trusting a store that reports a write it did not keep: - secret_store: read every write back and compare before reporting success. If the store returns a value we did not write, purge it so it cannot shadow the fallback on the next read. - On Windows only, fall back to a DPAPI-protected file in the app data dir, engaged solely after a proven round-trip failure and cleared as soon as the real store works again. The account name is mixed into the DPAPI entropy so a blob cannot be moved between entries and decrypt. macOS/Linux report an error instead of writing secrets to plaintext. - Log the compiled backend at startup and add `probe_credential_store` so an affected machine can be diagnosed from its own log file. - Guard the regression: `compiled_keyring_backend_is_persistent` fails the build if the features are ever dropped again. Verified to fail against `keyring = "3"`. The E2EE fail-closed posture is unchanged: a peer whose announce signature does not verify is still rejected. Linux builds now need `libdbus-1-dev` for the Secret Service backend. Claude-Session: https://claude.ai/code/session_016oUHtEUWWxC79eB88GvX58 Co-authored-by: Claude <noreply@anthropic.com> * fix(client, admin): make the settings panel, client, and admin panel do what they say (#1282) * fix(client): make the settings panel do what it says Functional review of every control in the settings overlay. Each fix below closes a gap between what a control promised and what it did. - Appearance: picking a theme no longer drops a saved accent colour. applyThemeByName strips every inline custom property from <body>, which includes the accent override; under neon-glow (whose body class sets --accent) the user's colour silently reverted until restart. - Overlay: reopening the panel rebuilds the active tab. The Voice & Audio mic meter and camera preview are torn down on close, so a reopened panel showed a dead meter and a black preview; tabs also now re-read prefs. The Logs tab's live listener is released when you switch away from it. - Status: the UserBar picker always started at "online" and never persisted, while the Account tab read a pref nobody else wrote — the two surfaces disagreed. Both now go through lib/userStatus, sync live via the pref-change event, and the saved status is re-asserted on connect. - Notifications: Do Not Disturb now suppresses the desktop notification and the chime, as its description in the panel claims. The taskbar flash, a passive cue, stays. - Keybinds: Ctrl+F, Ctrl+M, Ctrl+D, Ctrl+Shift+V and Ctrl+U were listed but unimplemented. They are wired now (voice ones only while in voice, all of them suspended while the settings panel is open). "Mark as Read" had no feature behind it at all and is replaced by the Escape behaviour that actually exists. - Account: backup codes now carry a "you won't see them again" warning and a copy button; the change-password form requires the current password before spending a server attempt and disables itself while in flight. - Advanced: removed the Hardware Acceleration toggle. Nothing read the preference it wrote — the webview decides GPU compositing before any JS runs, so honouring it needs a Rust startup change. - The settings sidebar name/avatar follow a rename instead of going stale, and settings/helpers no longer keeps a drifted copy of lib/preferences (the copy lacked the write guard, so a failed save could throw). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019TDE7ZPi38jLfEKhj7kvmm * fix(client): close silent-failure gaps in the inline admin surface Continuation of the settings-panel review into the rest of the client. - Member context menu had no styling at all: AdminActions renders BEM class names (context-menu__item and friends) that appear nowhere in the CSS, so the menu had no hover, no danger colour, and the "Change Role" submenu pushed the menu open instead of flying out. Added the missing rules. - The submenu offered a hardcoded admin/moderator/member list. On a server with custom roles those roles were unreachable, and picking a name that didn't resolve to a role id silently did nothing. Roles now come from the server's ready payload (owner excluded), and an unresolvable role reports an error instead of dead-ending. - Kick / ban / delete-channel now show an in-flight state, and the two-click confirm disarms after a few seconds so a menu left open can't turn a stray click into a ban (docs/architecture/ux/settings-and-admin.md §3). - Ban collects a reason, which the server already stores and displays (adminBanMember has always accepted one; the menu never passed it). - Copying an invite code was silent: no confirmation, and a clipboard rejection looked identical to success. It now toasts either way. - Creating an invite double-click-minted two of them, and revoking — which kills a live link — had neither a confirm nor an in-flight guard. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019TDE7ZPi38jLfEKhj7kvmm * fix(client): restore moderator message deletion and formatting - The delete affordance was offered only on your own messages, so a moderator could not moderate anything from the client. It now also appears when the signed-in user's role carries MANAGE_MESSAGES, derived from the role bitmasks the server already sends in `ready` (this is what docs/architecture/ux/messaging.md §4 specifies as "Delete (own / moderator)"). lib/permissions.ts existed for exactly this and had no callers at all. - Developer-mode "Copy ID" was silent on success and swallowed clipboard failures; it toasts either way now. - prettier --write on AdminActions.ts (Client Static Checks). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019TDE7ZPi38jLfEKhj7kvmm * fix(admin): stop the panel reporting success it didn't have Functional review of the server admin web panel. - An expired admin session left the panel on screen toasting "invalid or expired session" for every action, with no way back to the login form — only the log-stream code handled it. api() now handles 401 centrally: clear the token, return to login, and say why. - Deleting a backup called fetch() without looking at the response, so a failed delete reported "Backup deleted" and left the file in place. It now goes through api(), and — like every other destructive action here — asks for confirmation first. - A failed update check rendered as "Up to date. You're running the latest version", which is a lie that hides a broken update path. It now says the check failed and why. A failed apply no longer leaves the button stuck on "Applying...". - The Edit Channel modal could only rename. PATCH /channels/{id} accepts topic, slow_mode, position and archived, and the channel table has an Archived column — which was read-only state with no control behind it. All four are editable now. - Banned users showed "Yes" with no reason, even though the ban reason is collected on ban and returned by the API. It's now displayed. - Login and first-run setup had no in-flight guard, so a double-click spent two attempts against the login lockout / setup rate limit. Settings' Save stayed enabled after a successful save, implying unsaved changes. - Clipboard copies (invite code, new API token) had no rejection path: a refused clipboard looked exactly like a successful copy. - Backup names in inline onclick handlers go through jsq() like every other interpolated string. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019TDE7ZPi38jLfEKhj7kvmm * feat(admin): add the plugin management UI the backend already had /api/v1/admin/plugins has exposed list/install/enable/disable/uninstall since Phase C Step 9 — its own header says it "exposes plugin lifecycle operations to the admin panel", and docs/architecture/ux/settings-and-admin.md tells operators plugin management lives in the web panel. The panel had no Plugins section at all, so installing a plugin meant hand-crafting a multipart POST. Panel: - Plugins section: installed table (name, manifest description and requested permissions, version, enabled state, install date), zip upload with the 16 MB server cap stated up front, enable/disable, and uninstall behind a confirm. One lifecycle call at a time. - The lifecycle API sits under a different prefix than the rest of the panel and answers errors as plain text (http.Error), not JSON, so it gets its own fetch helper — sharing api() would have surfaced "unexpected token" instead of the server's reason. 401 still routes back to login. Server: - PluginRow had no JSON tags, so the list marshalled Go field names and every column would have rendered empty. Now snake_case like the rest of the API. - GET /plugins returns X-Plugin-Runtime: enabled|disabled. An empty list means "nothing installed" on a live runtime and "you can't install anything" on a disabled one; the body can't tell them apart, so the panel's empty state had no way to be honest about it. The plugin-store test helper now hands back the database the registry writes to — the existing happy-path test wired a *different* in-memory DB into the handler, which is why nothing noticed the list was always empty. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019TDE7ZPi38jLfEKhj7kvmm * feat(client): gate the composer on slow mode instead of failing the send Verified the optimistic message lifecycle against docs/architecture/ux — pending → chat_send_ok → sent, failed rows with mapped reasons, retry and delete-draft all behave as documented. One thing did not: slow mode. The UX spec (§5) says slow mode should "disable send with a live countdown in the composer; do not drop the drafted message". In practice the composer knew nothing about it: you typed, sent, and got a red failed row back — the exact enabled-then-rejected pattern §6.2 forbids. The client never even received the channel's slow_mode value. - Server: channel payloads (ready, channel_create, channel_update) now carry slow_mode alongside can_send, for the same reason can_send is there — the client can express the limit as affordance. The server still enforces. - Client: after an accepted send the composer disables itself for the channel's cooldown with a per-second countdown, and a SLOW_MODE refusal restarts the full window (the server's limiter is the authority on when the next send is allowed). The draft stays in the textarea. Moderators, who bypass slow mode server-side, are not gated. - The MANAGE_MESSAGES lookup added for moderator deletes moves into lib/permissions as currentUserPermissions/currentUserHasPermission/ canManageMessages, so the composer and the message renderer share one definition instead of two. - WsErrorCode listed 9 of the server's 16 codes: SLOW_MODE, CONFLICT, BAD_REQUEST, INVALID_JSON, UNKNOWN_TYPE, BAD_PAYLOAD, NOT_KEY_HOLDER and ALREADY_JOINED were missing, so code switching on it could not name cases the server actually sends. Now mirrors Server/ws/errors.go. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019TDE7ZPi38jLfEKhj7kvmm * fix(admin): make backup restore actually restart, and fail closed without a safety copy Verification pass over the remaining review items. Two real defects in restore, one duplicate resolved; cert TOFU and the replay path checked out as-is. Restore: - The handler closed the database, swapped the file underneath it, told the admin "database restored — server restarting", broadcast a 5-second restart countdown to every client... and then kept running. Nothing restarted it, so the server answered every subsequent request against a closed DB until an operator noticed. It now respawns for real, reusing the update-apply pattern (SpawnDetached → SIGTERM → os.Exit backstop) behind a test seam. - A failed pre-restore backup was a warning, and the irreversible overwrite went ahead anyway — removing the safety net the panel explicitly promises ("A pre-restore backup will be created"), precisely when it matters. It now aborts with the database untouched. - The safety copy was written to a cwd-relative "data/backups" while every other backup handler uses the absolute backupBaseDir, so a server started from another directory filed it somewhere the operator would never find. Both new tests were confirmed to fail against the previous behaviour. Client: - SidebarArea kept a private 140-line copy of the member-list wiring that SidebarMemberSection already provides (the extracted, tested one was never imported). Fixing the silent role-change failure earlier meant patching both; now there is one copy. Verified without changes: the optimistic send lifecycle (pending → chat_send_ok → sent, failed rows with mapped reasons, retry, delete-draft), reconnect replay (monotonic last_seq, dedup on reconnect, replay suppression of unread/notifications), and cert TOFU (first-use and mismatch modals, accept re-pins and reconnects, reject disconnects back to connect). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019TDE7ZPi38jLfEKhj7kvmm * fix(admin): remove the data race in the restart test hook CI (-race) failed identically on ubuntu and windows: TestHandleRestoreBackup_ Success polled a plain bool that the restore handler's goroutine wrote, and swapped the restartSelf package var from the test goroutine while that handler read it. The hook is now behind a mutex with an atomic flag in StubRestart. Production behaviour is unchanged — the race was entirely in the test seam I added. Verified with `go test -race -count=2 ./admin/`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019TDE7ZPi38jLfEKhj7kvmm --------- Co-authored-by: Claude <noreply@anthropic.com> * refactor + perf: split largest source files into modules; optimize hot paths (#1283) * refactor(updater): split updater.go into cohesive files Split the 1070-line updater.go into four files within the same package: updater.go (core types, release checking), download.go (download and tarball extraction), verify.go (signatures, checksums, staged binary), and assets.go (client assets, text-asset cache, HTTP fetching). Pure mechanical move — no behavior or API changes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BJM9kF4JBhRHEqtcv3sasu * refactor(ws): split hub.go into cohesive files Split the 1289-line hub.go into five files within the same package: hub.go (Hub struct, lifecycle, register/unregister), hub_broadcast.go (broadcast fan-out and per-user sends), hub_events.go (sequencing, replay, persistence), hub_sweep.go (stale client/session/voice sweepers), and hub_livekit.go (LiveKit accessors). Also optimizes wrapWithSeq on the hot broadcast path: build the seq prefix with a single preallocated append + strconv.AppendUint instead of fmt.Sprintf, halving allocations per broadcast message. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BJM9kF4JBhRHEqtcv3sasu * refactor(client): extract E2EEManager from livekitSession Move all client-side E2EE key-exchange logic (~550 lines) out of LiveKitSession into a new E2EEManager class in livekitE2EE.ts: ECDH keypair management, identity signing and TOFU pin verification, announce/offer handling, key-holder election, membership rekeying, and periodic key rotation. Dependencies are injected following the existing roomEventHandlers pattern. LiveKitSession keeps thin public delegates (handleE2EEAnnounce, handleE2EEOffer, handleParticipantLeft, rePinPeerIdentity) so the module-level bound exports and the public API are unchanged. livekitSession.ts shrinks from 1955 to 1409 lines. Adds focused unit tests for E2EEManager (key-holder setup, pending announce queue, offer resolution, clearState, rotation). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BJM9kF4JBhRHEqtcv3sasu * perf(server): hot-path and query optimizations Logging (biggest win): rewrite the admin log RingBuffer as a true ring (fixed array + head/count) instead of allocating a fresh 2000-entry slice + full copy per log line; gate the ring handler on a configurable level instead of unconditional DEBUG capture; move the broadcast debug log out of the seqMu critical section; drop the per-message slog.With clone in the WS handler. Database: new migration 019 adds idx_attachments_message (message pages no longer scan the attachments table), a covering role-leading index on channel_overrides (replacing a duplicate of the UNIQUE auto-index), a partial index for pinned messages, and narrows the FTS trigger to content changes only; ANALYZE runs after migrations. Rewrite GetChannelUnreadCounts and GetUserDMChannels to correlated subqueries that range-scan idx_messages_channel — O(unread) instead of O(all messages) per WS connect. New GetUserDMChannelIDs replaces the full DM query where only IDs are needed. CreateMessage/EditMessageContent use RETURNING, removing the re-read after every send/edit. Write-path contention: TouchSession throttled to once per minute per session (was one UPDATE per authenticated request); EventPersister flushes its batch in a single transaction with per-row fallback; revoked-session and stale-voice sweeps run off the hub dispatch goroutine with an in-flight guard, and session checks are batched into one IN query; the rate limiter is sharded into 32 buckets with allocation-free strconv key building (auth.Key). WS structural: voice E2EE channel fan-out goes through the existing pubsub voice topic instead of scanning every connected client under h.mu; channelReadAudience memoizes role lookups per call; hasChannelAccess drops its redundant duplicate permission check; voice_join batches SPEAK/VIDEO/SCREENSHARE checks via HasChannelPermBatch. Also: pubsub topic builders and NewAppMetrics stop allocating via Sprintf/global mutex. Verified with go test -race across all packages, go vet, gofmt, and sqlc generate idempotency. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BJM9kF4JBhRHEqtcv3sasu * perf(client): render-path, logging, and bundle optimizations Logging: the logger no longer runs permanently at debug — level is set from the environment at startup (debug in dev, info in prod), so every hot-path debug entry stops being serialized, buffered, consoled, and persisted to disk; per-URL debug logs in embed rendering removed. Render path: MessageList's store selector is scoped to the mounted channel, so messages in other channels no longer trigger re-renders, and a new incremental tail-append fast path appends rows instead of tearing down the whole window; Intl.DateTimeFormat instances are cached at module level; parseTimestamp memoizes epoch millis; media prefs (showEmbeds/inlineMedia/showLinkPreviews/animateGifs) are cached with pref-change invalidation; members store gains a roleRevision counter so MessageList stops rebuilding a role map on every presence/typing event. MemberList patches presence changes in place (status dot + offline class) via a row map instead of rebuilding every row, with single-pass role grouping. ChannelSidebar splits its voice subscription into a structural selector (excluding speaking) and a speaking-only patcher using a cached element map instead of per-user querySelector on every speaker event. Memory: GIF/media elements are unobserved before the message window discards them, fixing unbounded IntersectionObserver retention of detached DOM (including frozen-frame data URLs). Bundle: livekit-client (1.3 MB) moves to its own chunk via dynamic imports and manualChunks; the READY handler's stale-voice check reads the voice store instead of requiring the module synchronously. Adds 11 focused tests (different-channel no-rerender, append fast path, media release, presence patch, speaking patch). Full unit suite: 3606/3606 passing; typecheck, lint, and production build clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BJM9kF4JBhRHEqtcv3sasu --------- Co-authored-by: Claude <noreply@anthropic.com> * fix(ci): skip alloc test under deadlock tag; cut bcrypt cost in tests (#1284) The deadlock-tag CI pass failed on TestRingBuffer_WriteDoesNotAllocate: under -tags deadlock, syncutil.Mutex is the go-deadlock mutex whose Lock allocates, so the steady-state ring write measures 1 alloc/call. Extend the build constraint to !race && !deadlock — the test's guarantee is about the ring buffer itself, which the -race-less default pass covers. Make bcryptCost a var with an exported SetCostForTesting hook that also resets the dummy timing pad, and call it with bcrypt.MinCost from the api, auth, and admin TestMains. Password hashing at production cost 12 dominated those suites (~264 hashes): with the race detector the api package alone took ~860s; it now runs in ~33s. Nothing under test depends on hash strength, and no test asserts the cost. Hygiene in the same pass: migration 020 drops idx_sessions_token and idx_invites_code (exact duplicates of their UNIQUE auto-indexes, pure write overhead) with updated db_test assertions; remove the dead tar.TypeRegA comparison in the updater (stdlib normalises it to TypeReg since Go 1.11); gofmt storage/storage.go comment alignment. Claude-Session: https://claude.ai/code/session_01BJM9kF4JBhRHEqtcv3sasu Co-authored-by: Claude <noreply@anthropic.com> * perf(ws): route hot-path permission checks through the cached PermissionService (#1285) The ws package was the only major subsystem still doing live per-check permission queries (GetRoleForUser + GetChannelPermissions per check): a V2 voice join cost 9+ DB reads across its four gates, and every channel broadcast resolved one role query per connected client. Hub now holds svc.Permissions and the voice deps carry it (nil-safe: bare test fixtures fall back to the existing live path, fail-closed semantics preserved everywhere). Converted sites: the voice join and token-refresh permission gates, USE_VIDEO/SHARE_SCREEN controls, requireChannelAccess, channelReadAudience, and RefreshChannelVisibility. Caching these is revocation-correct: every permission-changing mutation already invalidates synchronously before hub fan-out (InvalidateUser on role change, InvalidateAll on override change), the 30s TTL is only a backstop, and the service's gen-counter guard prevents a populate that races an invalidation from caching stale data — the audience-resolution comments now document that invariant. The stale-voice sweeper's check deliberately stays live: it is the last-line backstop for revocations that might bypass an invalidation hook, runs once a minute for only in-voice clients, and its eviction test pins exactly that guarantee. requirePerm keeps its INTERNAL-vs-FORBIDDEN distinction by using the cache only for positive verdicts and falling through to the live path on denial. Adds perm_cache_test.go: role-change invalidation is immediate (no TTL wait), and a counting-store test proving the second check is served from cache. All pinning tests (authz, voice_perm_stale, channel visibility agreement, sweep eviction) pass unmodified. Claude-Session: https://claude.ai/code/session_01BJM9kF4JBhRHEqtcv3sasu Co-authored-by: Claude <noreply@anthropic.com> * perf + refactor: SQLite reader pool, async audits, real lazy-livekit, test splits, eslint 10 (#1286) * perf(db): batch audit writes through an async writer Audit inserts ran synchronously on the request path — including one INSERT per WebSocket connect — each an implicit transaction on the single SQLite connection. WriteAudit keeps its exact signature and D8 policy (never fail the caller, never silently discard): it now upgrades to an async path when the passed Auditor also implements AsyncAuditor. *DB implements that via an atomic pointer that main.go populates at server startup with an AuditWriter modeled on the event persister (bounded queue, batched single-transaction flush with per-row fallback, drain-on-stop, atomic counters, non-blocking enqueue that error-logs drops without leaking the detail field). The token CLI and tests never install a writer, so they keep today's synchronous behavior with zero call-site changes. The writer's Stop defer registers after database.Close's so the LIFO unwind drains the queue before the DB shuts. Adds audit_writer_test.go: batch flush, D8 drop logging, drain-on-stop, flush-failure accounting, poison-row fallback, concurrent enqueue, and seam tests pinning sync-without-writer vs async-with-writer behavior. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BJM9kF4JBhRHEqtcv3sasu * perf(client): actually defer livekit-client; honor saved log level at startup The manualChunks split was cosmetic: index.html modulepreloaded the 531 kB livekit chunk and the entry statically imported it. All four import chains from startup are now cut — auth.store's logout leaveVoice and ptt's setMuted go through dynamic imports, applyStoredAppearance moved to lib/appearance.ts so main.ts and ConnectPage stop pulling the settings tree (whose overlay now loads on first open), and MainPage itself is a dynamic import in renderPage, guarded against the destroy-before-mount race by a navigation-generation helper and pre-warmed once the socket connects. Entry chunk drops 387 kB -> 114 kB (gzip 36 kB); index.html has no modulepreload links; livekit/MainPage/SettingsOverlay/livekitSession load as lazy chunks. The logger now honors the Logs tab's saved minimum level at startup (applyStoredLogLevel with the legacy-key migration moved into lib/preferences.ts) and re-applies it live on pref changes. Dead code: remove unreachable VoiceChannel.ts (superseded by ChannelSidebar's renderer) and its test, plus all knip-flagged unused re-exports in message-list/renderers.ts and ConnectPage's unused form types — knip is now clean apart from pre-existing config hints. Tests: +12 (navigation guard incl. stale-mount discard; logger startup pref, migration, and live re-apply); ptt/stored-appearance updated for dynamic-import plumbing only. Full suite 3593 passing; typecheck, lint, and production build clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BJM9kF4JBhRHEqtcv3sasu * perf(db): split SQLite into single-writer + multi-reader connection pools The entire server serialized on one SQLite connection: every read queued behind every other read and every write, throwing away WAL's concurrent-reader capability. File-backed databases now open two pools from a DSN that carries all seven PRAGMAs as per-connection _pragma parameters (an Exec'd PRAGMA only configures one arbitrary pooled connection — moving them into the DSN is what makes >1 connection safe, foreign_keys included): a single-connection writer with _txlock=immediate, and a reader pool sized max(4, NumCPU). In-memory databases keep the exact historical single-connection behavior, which preserves every :memory: test site and the connection-scoped PRAGMA-toggle tests untouched. Routing lives in a dbtx router implementing sqlc's DBTX: statements go to the reader only when provably read-only (leading SELECT/PRAGMA after skipping comments — necessary because sqlc routes INSERT/UPDATE/DELETE ... RETURNING through QueryRowContext/QueryContext, which must stay on the writer); Exec, transactions, migrations, ANALYZE, VACUUM INTO, and the SQLDb() escape hatch all pin to the writer. Every former sqlDB reference across the package was re-pointed deliberately. New pool_test.go pins the properties the split must preserve on a file-backed DB: foreign_keys=1 across many reader connections, WAL journal mode, FK enforcement through both write paths, 8x8 concurrent reader/writer hammering with exact row counts, and a read completing against the pre-tx snapshot while a write transaction is open — the property this change exists to unlock. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BJM9kF4JBhRHEqtcv3sasu * test(client)+chore: split the two largest test files; eslint 10; audit clean Split tests/unit/ws.test.ts (3340 lines) into ws-cert / ws-reconnect / ws-messaging / ws-lifecycle plus a shared helpers/ws-mocks.ts module, and tests/unit/audio-pipeline.test.ts (2547 lines) into core / gain / vad-worklet / vad-fallback files. Test bodies moved verbatim; the suite count is unchanged at 3593 passing. Upgrade eslint 9 -> 10 (with @eslint/js 10; typescript-eslint's peer range already covers v10, flat config unchanged, zero new findings) and pin test-exclude ^8 via the existing overrides block so the coverage chain picks up patched glob/minimatch/brace-expansion. npm audit: 8 high -> 0 vulnerabilities. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BJM9kF4JBhRHEqtcv3sasu * refactor(server): split remaining large files; dependency hygiene notes Split ws/coverage_boost_test.go (2856 lines) into coverage_helpers / chat / voice / voice_lifecycle / misc test files — bodies verbatim, 746 passing tests before and after. Split service/message.go (781) into message_crud / message_reactions / message_query / message_perms with types and the constructor staying put, and ws/serve.go (754) into serve / serve_pumps / serve_auth / serve_ready. Dependency findings (no changes needed): coraza-coreruleset's stale Feb-2024 pseudo-version is unreachable from our code — it enters the module graph only through coraza's own internal tests, and our WAF uses inline directives, never the CRS (fresher rules would require adopting the /v4 module and rewiring the WAF config — deliberate follow-up, not hygiene); gogo/protobuf is likewise graph-only via the livekit SDK and never built into our binaries. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BJM9kF4JBhRHEqtcv3sasu * style: satisfy golangci-lint modernize/staticcheck in new pool and audit code CI's golangci-lint pass (not run locally until now) flagged the Phase 3/4 additions: range-over-int loops, interface{} -> any on the dbtx router, WaitGroup.Go in the pool tests, and a De Morgan simplification in isReadOnlySQL's identifier-boundary check. Pure style — verified against the same golangci-lint v2.11.3 binary CI uses (0 issues) and re-ran db/ws race + deadlock suites green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BJM9kF4JBhRHEqtcv3sasu --------- Co-authored-by: Claude <noreply@anthropic.com> * feat(waf) + fix(deps) + test(ws): OWASP CRS, Dependabot fixes, sleep-free ws tests (#1287) * fix(deps): clear quick-xml RUSTSEC advisories in Tauri lockfile cargo-audit identified the two Dependabot alerts on the default branch: quick-xml 0.37.5 and 0.38.4 both carry RUSTSEC-2026-0194 (quadratic runtime on duplicate-attribute checks) and RUSTSEC-2026-0195 (unbounded namespace allocation DoS), fixed in >=0.41. Both were transitive: plist 1.8.0 (via tauri) and tauri-winrt-notification 0.7.2 (via notify-rust). Semver-compatible updates fix both — plist 1.10.0 moves to quick-xml 0.41, and tauri-winrt-notification 0.7.3 drops quick-xml entirely. cargo-audit is now clean of vulnerabilities; the remaining 20 informational notices are the unmaintained GTK3-binding crates inherent to Tauri v2 on Linux. Verified plist compiles against quick-xml 0.41 (full Tauri build needs the GTK/WebKit system libs CI installs). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BJM9kF4JBhRHEqtcv3sasu * feat(waf): layer the maintained OWASP Core Rule Set onto the WAF The WAF previously ran six inline directives only — the CRS never loaded (the old coreruleset dep was a stale graph-only pseudo-version). A second Coraza engine now loads the embedded CRS from coraza-coreruleset/v4 (v4.25.0), layered on top of the inline rules, which stay byte-identical and keep blocking exactly as before. CRS ships in a new server.waf_crs_mode knob (off|detect|block), defaulting to detect: chat traffic is CRS-false-positive-prone (a new test pins that block mode rejects benign SQL-ish chat prose at the default threshold), so operators get rule-match visibility via structured logs first and opt into blocking after tuning. Setup mirrors the official connector: Host/Transfer-Encoding restored to the transaction (else 920280 fires on everything), phase 2 always runs so query-string attacks are scored, PUT/PATCH/DELETE added to the CRS method policy for this REST API, body limits matched to the app's 1 MiB cap with uploads excluded from body access and the content-type policy. Also fixes a latent middleware bug: the body was previously swapped for the buffered reader even when nothing was buffered, which would have handed body-access-off routes an empty body; now pinned by a test across all modes. Adds waf_crs_test.go (load, mode wiring, XSS/traversal detection without blocking, block-mode blocking + benign passthrough, upload body preservation); waf_test.go passes unmodified. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BJM9kF4JBhRHEqtcv3sasu * test(ws): replace fixed sleeps with condition-based waits The ws suite paced async hub effects with 537 fixed time.Sleep calls — slow at best, flaky under load at worst. They are now condition-based: a small waitFor/waitRegistered/waitClientCount/waitMsgOfType helper set (waitRegistered exploits the hub's in-order client-event processing), plus blocking decode-scans for the DM tests. The bulk deletion is grounded in verified production facts, unchanged by this commit: sendMsg is a synchronous buffered send (error replies are already buffered when the handler returns), the voice control / rollback / cleanup / sweep paths are synchronous, and serve.go registers the client before writing the ready frame. Absence assertions were deliberately NOT inverted into polling — they keep bounded windows, each commented. 20 sleeps remain, all justified in place: poll intervals inside condition loops, absence windows, clock-granularity pacing, and the event-pruner's inherently time-based no-prune-after-cancel assertion. Suite: 746 tests before and after; 62.6s -> 46.1s (30s of the remainder is GracefulStop's hard-coded production 5s drain, out of scope here); race flake check passes 3 consecutive iterations; deadlock pass and golangci-lint clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BJM9kF4JBhRHEqtcv3sasu --------- Co-authored-by: Claude <noreply@anthropic.com> * fix: audit-driven fixes — client leaks/lazy-load, WAF detect logging, audit shutdown race (#1288) * fix(waf,db): aggregate CRS detect-mode logging; make audit Stop await goroutine exit WAF detect mode wired logCRSMatch as the engine-level error callback, which fires one slog.Warn per matched rule on the request goroutine. In the default detect mode ordinary chat prose trips several CRS SQLi/XSS rules plus anomaly scoring, so each request logged a burst of Warn lines in the hot path. Aggregate per request from per-transaction state instead of the shared global callback: in the default detect path leave the engine error callback nil and, in the existing crsTx defer, emit at most one Warn per request that had matches (count + highest-severity rule), demoting the full rule-id list to Debug. Block mode keeps per-rule logging (blocked requests are rare and their detail is wanted), and a caller-supplied onCRSMatch callback keeps per-rule delivery so existing tests stay unmodified. Detection, interruption, and body handling are unchanged — only the detect-path logging shape. The audit writer's Stop selected between <-done and <-ctx.Done(); on a slow flush the 5s ctx could win, returning while run() was still flushing. main.go's LIFO defers then closed the DB pool under a live flusher, losing audits. Stop now always waits on done (the goroutine has stopped touching the store) while ctx bounds only the drain inside run() via a published stopCtxDone channel, so a slow store delays shutdown by at most one in-flight flush and the pool is never closed under a live writer. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BJM9kF4JBhRHEqtcv3sasu * fix(client): plug listener leaks, guard lazy livekit load, honor saved log level Follow-up audit of the recently-landed lazy-livekit and session wiring found three real issues: - clearAuth unconditionally dynamic-imported livekitSession to call leaveVoice on every logout, pulling the ~531 kB livekit chunk into the logout path even when no voice session was ever active. Guard the import on an active voice session (currentChannelId set and status not idle) and add a .catch so a failed teardown import can't reject unhandled. - The onStateChange handler unsubscribed session listeners only on the ready transition, not on disconnected; user_update and ready listeners registered per session were never collected for cleanup. Collect them into a sessionUnsubs array cleaned up on both ready and disconnected, preventing duplicate handlers accumulating across reconnects. - The Logs tab min-level select ignored the persisted log level when no explicit dropdown preference was saved. Add logger.getLogLevel() and default the select to it so the UI reflects the level actually in effect. Also add .catch to the ptt setMuted dynamic import. New unit tests cover the clearAuth guard, getLogLevel, and the LogsTab default. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BJM9kF4JBhRHEqtcv3sasu --------- Co-authored-by: Claude <noreply@anthropic.com> * fix(waf): load embedded OWASP CRS ruleset correctly on Windows (#1289) The CRS WAF engine failed to initialize on Windows, taking the whole api package's test suite red there. coraza's seclang parser resolves Include globs through path/filepath: for every match of `Include @owasp_crs/*.conf` it calls filepath.Join(currentDir, match), which on Windows rewrites the forward slashes to backslashes. It then feeds names like `@owasp_crs\REQUEST-901-INITIALIZATION.conf` back into the root fs.FS. That FS is the ruleset's embed.FS, which is always forward-slash and rejects a backslash name, so newCRSWAF returned "file does not exist" and no CRS rule under a subdirectory was ever loaded. Wrap coreruleset.FS in a small slash-normalizing fs.FS (Open/ReadFile/ReadDir/ Glob) that converts backslashes to forward slashes before delegating. This fixes CRS loading on Windows without patching coraza or the ruleset module and is a no-op where the separator is already "/". The Linux-only local verification for the CRS work missed this because coraza never emits backslashes there. The new test reproduces the failure mode on any OS by constructing the exact backslash name coraza produces on Windows: the raw ruleset FS fails to read it, the wrapper resolves it, and a forward-slash path still works. Claude-Session: https://claude.ai/code/session_01BJM9kF4JBhRHEqtcv3sasu Co-authored-by: Claude <noreply@anthropic.com> * fix(e2e): repair the Playwright suite so the CI job stops timing out (#1291) The Client E2E CI job never completed: every run hit its 25-minute cap and was cancelled. ~229 of the 255 web tests were failing, all cascading from the shared login helper, and 255 tests x 3 attempts x 20-45s of timeout burn on 1 worker deterministically exceeds the cap. Root cause: the e2e Tauri mock predates the Rust HTTP TOFU proxy. api.ts now awaits invoke("start_http_proxy") and builds REST URLs as http://127.0.0.1:{port}/api/v1/..., but the mock's invoke returned null for the unstubbed command, so every URL got a literal "null" port and Request construction threw before the mocked plugin:http transport was consulted. Login rejected, [data-testid='app-layout'] never mounted, and every logged-in test burned its full timeout. Stubbing start_http_proxy with any numeric port fixes the cascade because route matching is substring-based. The tail of failures after that fix were tests asserting behavior the app intentionally changed: - The ready payload can no longer pre-connect the local user to voice: the dispatcher treats "self in ready.voice_states while idle" as stale state from a reload and immediately leaves. MOCK_VOICE_STATE now seeds remote users only (2, 3), and widget tests join through the real click path via a new joinVoiceChannelByName helper. - The mock's voice_join reply no longer includes a voice_token: a token starts a real LiveKit session that deterministically self-destructs in the browser mock (E2EE key exchange timeout ~15s / connect-refused retries), tearing the widget down mid-test. These web tests validate the WS/UI layer only; real LiveKit is covered by the native suite. The reply also gained the full VoiceStatePayload shape — the sidebar renders user.username directly, and the omitted field broke the whole voice-user list render. - Message-load failure now renders an inline region error + Retry instead of a toast (UX spec 2), so the toast specs assert the inline UI and get their auto-dismiss vehicle from the delete-confirmation toast. CI hardening so a future systemic breakage can never burn the full cap again: maxFailures 20 and a 20-minute globalTimeout in CI (Playwright now self-terminates with a usable report instead of being SIGKILLed), with the workflow's timeout-minutes 25 as the outer backstop. The job stays continue-on-error until it has proven stably green across a few pushes; the ci.yml comment documents that flip trigger. Full suite: 255/255 passing locally (~7.5 min at 1 worker, ~4 min at 2). Unit tests (3598), typecheck, and prettier all clean. Claude-Session: https://claude.ai/code/session_01BJM9kF4JBhRHEqtcv3sasu Co-authored-by: Claude <noreply@anthropic.com> * feat(admin): first-run setup wizard with config.yaml write-back + LiveKit auto-download (#1290) * feat(admin): first-run setup wizard with config.yaml write-back Turn the single-screen owner-account setup into a guided multi-step wizard so non-technical operators never have to hand-edit YAML: - config: new comment-preserving config.Save (yaml.Node round-trip, atomic temp+rename write, verified loadable before replacing the file) plus a shared config.DefaultPath. Persists the runtime-generated LiveKit credentials so voice tokens survive restarts. - admin: POST /admin/api/setup accepts an optional "wizard" object (server name, MOTD, registration, port, TLS mode/domain, upload limit, voice quality). Values are validated before the account is created; DB settings and config.yaml are written after; failures downgrade to warnings so the created owner is never orphaned behind a 5xx. When a startup-only value changed the server restarts itself (reusing the backup/update restart machinery) and returns the new admin URL. - admin: GET /admin/api/setup/status now returns secret-free prefill defaults while setup is pending. - admin panel: six-step wizard UI (welcome, account, server basics, uploads & voice, access, review) with plain-language explanations, a restart/reconnect screen, and a "skip" path that keeps the legacy account-only flow byte-for-byte. - legacy payload {username,password} and all existing call sites keep working (SetupOptions is a trailing variadic parameter). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018iHyK5WtjSQgjubTegSrUB * fix(lint): satisfy modernize — any over interface{}, new(expr) over ptr helper Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018iHyK5WtjSQgjubTegSrUB * feat(voice): auto-download the LiveKit server binary Voice now works with zero manual setup: when voice.auto_download_livekit is enabled and no voice.livekit_binary is configured, the server fetches the pinned livekit-server release (v1.13.5, overridable via voice.livekit_version) from the official LiveKit GitHub releases in the background at startup, verifies it against the release's checksums.txt, extracts it into data/livekit/, and manages it as the existing companion process (crash recovery, health checks, graceful shutdown). - ws: new livekit_download.go — pinned version, per-platform asset mapping (linux/windows × amd64/arm64/armv7, matching LiveKit's goreleaser config), size-capped downloads, hash verification and extraction through one open handle (TOCTOU-safe), O_EXCL staging, atomic rename, stale-version cleanup. LiveKitProcess.Start resolves the binary asynchronously with retries so boot is never blocked. - config: voice.auto_download_livekit + voice.livekit_version; enabled in the generated default config so fresh installs get working voice out of the box, while the compiled-in default stays off for existing configs. config.Load now loads the default file it just wrote, so the first boot runs with exactly the configuration the file documents. - wizard: "Voice chat" toggle (on by default) in the Uploads & voice step; the choice is written to config.yaml and factored into the restart decision. - docs: livekit-setup, server-configuration, deployment, README. Verified end-to-end against the real v1.13.5 release: download, checksum match, extraction, and process spawn all succeed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018iHyK5WtjSQgjubTegSrUB * chore: remove stray server.log, ignore local run logs Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018iHyK5WtjSQgjubTegSrUB --------- Co-authored-by: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
58005c9c6f |
feat(auth): revocable API tokens, introspect MCP server, and a Go 1.26 idiom pass (#1266)
* feat(auth): add revocable API tokens (bot/service auth) Add long-lived, revocable API tokens so headless clients (the introspection MCP tool, bots, CI) can authenticate without a password. Presented as "Authorization: Bearer <token>", a token authenticates as a specific user, inheriting that user's role and permissions. - migration 018 + dedicated api_tokens table (kept separate from sessions so bulk logout and the per-user session cap never touch these); only the SHA-256 hash is stored, raw token shown once at creation - auth.ResolveTokenHash: one shared bearer resolver that both AuthMiddleware and adminAuthMiddleware now call. Sessions are matched first so existing login behavior is unchanged; API tokens are a fallback only on session miss. A DB outage is returned wrapped, never mistaken for a bad token. - `server token create|list|revoke` CLI: mints directly against the DB with no HTTP and no login — the password-free bootstrap path - tests: resolver (8 cases incl. outage-not-fallthrough), db queries (6), api middleware integration (valid + revoked token) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(tools): add owncord-introspect MCP server A local MCP dev tool that lets Claude Code introspect a running OwnCord instance: read its logs, query any REST endpoint, and tail the desktop client's log file. It is a thin wrapper over the existing API plus the client log — no new product surface. - tools/mcp-introspect/index.mjs (Node/ESM, one dep: @modelcontextprotocol/sdk) exposes api_request (full read-write passthrough), server_logs (admin SSE ring-buffer stream), client_logs (reads the desktop log file) - authenticates with an API token (OWNCORD_API_TOKEN); pins the self-signed cert and skips hostname checks (the cert has no SAN) - registered in .mcp.json (secret-free ${OWNCORD_API_TOKEN}) - un-ignore tools/mcp-introspect/ so this shared dev tool is committed, while tools/livekit-server.exe and node_modules stay ignored - docs/mcp-introspect.md: how it works, tool reference, setup, troubleshooting Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(dependencies): update and add various crate versions in Cargo.lock * feat(admin): manage API tokens from the admin panel Add Owner-gated HTTP endpoints and a UI card to create, list, and revoke API tokens from the web admin panel. Previously only the `server token` CLI could manage them, which requires shell access to the host. - POST|GET|DELETE /admin/api/tokens in admin/handlers_tokens.go, wired in admin/api.go. All three are Owner-only (ownerOnlyMiddleware, like backups/updates): an HTTP token-mint endpoint is a network-reachable credential-minting surface, and API tokens deliberately survive password change + bulk logout, so a hijacked admin session must not mint one. - Reuses the same db.*APIToken calls as the CLI; create sources the actor from request context (audits who clicked, not the bound user); the raw token is returned once in the 201 body, never stored. - Add json tags to db.APITokenListItem for snake_case wire consistency. - Admin panel: "API Tokens" nav item + create modal, show-once reveal, revoke confirm in admin/static/index.html. - Tests: 7 in admin/api_test.go (+api_tokens table in the in-memory schema). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor: modernize to Go 1.26 idioms + enable modernize linter Apply `golangci-lint modernize` autofixes across the server and enable the linter in .golangci.yml so these stop re-accumulating (they built up only because modernize was never in the config). Production code: slices.Contains for hand-rolled membership loops (api router, ws origin, db/account, plugin manifest); strings.SplitSeq for allocation-free line/segment iteration (db/migrate, updater, livekit_proxy); strings.Cut (config); fmt.Appendf (dm_handler); min() (event_pruner); any (ws client). Tests: range-over-int, t.Context(), WaitGroup.Go, slices.Sort, maps.Copy, new(expr), interface{}->any. - plugin/manifest.go parent-traversal check applied by hand: modernize skipped it (two conflicting rewrites); used the slices.Contains form. - Removed the now-dead ptr() test helper after newexpr inlined its callers. - Dropped dangling sort imports left by the sort.Slice->slices.Sort rewrite. No behavior change. All four tag variants build, full test suite is green, and golangci-lint (with modernize enabled) reports 0 issues. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
4fc21cb372 |
feat(server): logging & error-visibility hardening
Make server failures debuggable without leaking secrets: - configurable stdout log level (config.yaml logging.level + OWNCORD_LOGGING_LEVEL) - preserve the DB cause in ErrInternal wraps; log auth-DB failures distinctly from bad tokens; log the previously-silent expired-session cleanup goroutine - route HTTP handler panics through slog (was chi stderr-only, invisible to the admin log stream) - stackutil: argument-free panic stacks so key/token bytes never reach the admin ring buffer / SSE; slog.LogValuer redaction on VoiceConfig/GitHubConfig/ GIFConfig/Config and db.User/db.Session - logctx: req_id/trace_id correlation on ...Context log calls Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
6afa9e974c |
refactor(server): thread context.Context through the db layer and all callers
Fixes all 109 golangci-lint findings (106 contextcheck, 1 gocritic,
2 gosec) that accumulated after D2 wired dbgen (whose queries take ctx)
under ctx-less db.DB wrappers while CI lint was quota-dead. No nolint
comments added; every finding fixed by genuinely threading context.
- db: all 138 hand-written db.DB methods take ctx first; the dbCtx()
Background shim is deleted; raw Query/QueryRow/Exec/Begin use their
Context variants; the four redundant ctx-less passthroughs removed.
db.Auditor/WriteAudit gain ctx.
- Seams: permissions.Checker (DB iface, HasChannelPerm,
RequireChannelAccess) and the service.Store interface mirror the new
signatures (ws.EventStore and plugin.PluginStore already did).
- Callers: api/admin handlers use r.Context(); ws per-message paths use
the connection ctx via DispatchV2; hub loops and startup wiring use
context.Background(); service methods thread ctx where they have one
and Background where no ctx exists. Public service surface reached by
ctx-holding chains (PermissionService.HasChannelPerm/GetRoleForUser/
RequireChannelAccess, message/dm/block/invite/profile methods) is now
ctx-first.
- Detached (context.WithoutCancel) where cancellation would break an
invariant, found by a 3-lens adversarial review of the diff:
* voice-leave background retries (a dead webhook/connection ctx killed
retry 2 before it ran, leaving ghost capacity-holding voice rows)
* rollbackVoiceJoin's compensating delete (its trigger IS the cancel)
* post-2FA-change DeleteOtherSessions and logout DeleteSession (the
security tail of a committed change must not die with the request)
* all api/ws audit writes (a banned user could suppress their own
login_blocked_banned row by aborting the request mid-bcrypt)
* admin backup VACUUM INTO (an interrupt left a truncated .db that
the backup list presented as restorable)
* post-commit message/edit refetches (a committed message must still
fan out when the sender disconnects)
* hub settings-cache refresh (one dead connection could pin stale
values for the 30s TTL)
- gocritic rangeValCopy fixed (index iteration); gosec G306 excluded in
config with justification (generated source must stay world-readable)
instead of flipping genprotocol output to 0o600.
Verified: gofmt/vet, all four build-tag variants, full suite, deadlock
pass, full -race pass, golangci-lint 0 issues uncapped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
5f1d6fc287 |
refactor(server): remove the store abstraction layer (D3)
Deletes Server/store (SQLiteStore, MemStore, the composed Store interface) and collapses to a single sqlc-backed db package, executing the prior audit's P4 "single data layer" direction (finding #6). SQLiteStore was a pure pass-through to *db.DB, so consumers now depend on narrow interfaces that *db.DB satisfies directly: - service.Store (service/datastore.go, renamed from store/store.go) - ws.EventStore (ws/eventstore.go) - plugin.PluginStore (plugin/pluginstore.go) The event- and plugin-KV methods that lived in the store's SQLite implementation move into the db package (db/event_queries.go, db/plugin_queries.go), keeping their raw-SQL form. Tests: the MemStore-based unit tests now run against a real in-memory SQLite db opened per-test with migrations applied, via package-local seed helpers. Fault-injection tests embed a real *db.DB and override the single method under test, preserving error-path coverage. Full server suite and sqlc-verify are green. Docs: audit finding #6 and A-2026-07-06 marked resolved; decisions D3 updated; architecture server.md / data-model.md diagrams and prose updated to the api -> service -> db layering. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UA17KPvqGBX3XbXYnMf1rA |
||
|
|
0c093e8403 |
chore(server): delete unfinished Postgres scaffolding
PostgresStore was 86% stubs behind a build tag nothing enables, pgdbgen carried hand-added build tags that fought sqlc-verify, and the runtime never threaded store.Store through the handler boundary. Single-engine reality shrinks the W1-3 attachment-ownership fix and ends the pgdbgen churn. Removed: store/postgres.go, db/pgdbgen/, db/queries/postgres/, migrations/postgres/, the sqlc postgres block, pgx from go.mod, the startup-refusal branch, and the dead Postgres config surface. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
1f4a89ced1 |
audit: fix all C/H/M/L severity findings from Phase B+C PR review
Agent-Logs-Url: https://github.com/J3vb/OwnCord/sessions/2d415fd9-6138-495b-ac29-c54d08ae4ce4 Co-authored-by: J3vb <192430104+J3vb@users.noreply.github.com> |
||
|
|
d320a8b587 |
fix(review): address 11 Copilot review findings on PR #1132
Clean sweep of every actionable item from the two Copilot review passes
on head
|
||
|
|
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 |
||
|
|
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).
|
||
|
|
dede2c61a7 |
phase-a: scaffold postgres backend (schema, queries, store stub, config)
- migrations/postgres/: consolidated pg schema with tsvector FTS, CITEXT usernames, native CHECK constraints, native BOOLEAN/TIMESTAMPTZ types - db/queries/postgres/: 14 sqlc query files dialect-translated from sqlite ($N placeholders, NOW(), TRUE/FALSE, ON CONFLICT DO UPDATE, RETURNING id, :execrows for mutations needing row count) - sqlc.yaml: second engine entry -> pgdbgen package under pgx/v5 - Makefile: sqlc-verify covers both dbgen + pgdbgen - store/postgres.go: PostgresStore behind //go:build postgres, full Store interface (112 methods). Connection lifecycle real; query methods stub ErrPostgresNotImplemented awaiting pgdbgen wrappers - config: DatabaseConfig.Type/Host/Port/User/Password/Name/SSLMode/MaxConns - main.go: explicit dispatch on database.type; postgres errors with clear pointer at remaining work until pgdbgen + boundary refactor land - phase-a-foundation.md: implementation status + actionable TODO checklist including forward-only sqlite->postgres data migration design |
||
|
|
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 |
||
|
|
447a4543e7 |
chore: remaining server changes (code quality, go mod tidy)
Go mod tidy, minor server-side adjustments from security verification and code quality cleanup pass. |
||
|
|
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 |
||
|
|
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) |
||
|
|
3236918012 |
refactor: server hardening + client decomposition + protocol resilience
Server:
- Split monolithic voice_handlers.go into voice_join/leave/controls/broadcast
- Add metrics endpoint (admin-IP-restricted /api/v1/metrics)
- Add orphaned attachment cleanup in maintenance loop
- Add sentinel errors (db/errors.go, ws/errors.go)
- Add ring buffer for event replay on reconnect
- Add heartbeat monitoring with stale connection sweep
- Improve hub with panic recovery, graceful shutdown, seq tracking
- Typed message structs replace raw map[string]interface{}
Client:
- Decompose MainPage into ChatArea + SidebarArea controllers
- Add disposable.ts lifecycle management pattern
- Add member list right-click context menu (kick/ban/role)
- Tighten CSP (media-src, font-src, object-src, base-uri)
- Improve store with shallowEqual, 500-msg cap, batch updates
- Add search API endpoint wiring
- Fix LiveKit session cleanup and reconnection
Docs:
- Add CODEMAPS for architecture, backend, frontend, data, deps
- Add protocol-schema.json (machine-readable, 36 message types)
- Add platform research report
- Update PROTOCOL.md with seq/replay fields
|
||
|
|
df998386d9 |
feat: redesign admin panel, add live server logs and audit log filters
Admin panel redesign: - Rebuild frontend from mockup with Discord-style dark theme - Stat cards, section cards, role badges, modal system, toast notifications - All 7 sections: Dashboard, Users, Channels, Audit Log, Settings, Backups, Updates - Modals replace confirm()/prompt() for all destructive actions Live server logs (new): - RingBuffer + MultiHandler tees slog to stdout AND in-memory buffer - SSE endpoint at /admin/api/logs/stream streams logs in real-time - Log viewer with level filters (DEBUG/INFO/WARN/ERROR), search, auto-scroll, pause/resume, copy all, clear - Color-coded lines by level, source categorization from file paths Audit log improvements: - Search filter (actor, action, target, detail) - Action type dropdown filter - Copy All and Export CSV buttons - Instant client-side re-filtering Console output: - Switch from JSON to human-readable text format (slog.TextHandler) - Move startup banner before init logs so it appears first |
||
|
|
65a8403a92 |
fix: resolve 1 critical and 5 high security/reliability issues from go-review
- CRIT-2: Prevent HTTP header injection in Content-Disposition via mime.FormatMediaType - HIGH-1: Call hub.GracefulStop() on server shutdown to clean up WebSocket/voice state - HIGH-2: Remove double send on serveErr channel that caused goroutine leak - HIGH-3: Handle GetUserByID error after registration to prevent nil panic - HIGH-4: Return nil,nil on sql.ErrNoRows in GetAttachmentByID (matching codebase convention) - HIGH-5: Add voiceDone channel to bound RTP goroutine lifetime on voice teardown - Fix handleVoiceICE to return VOICE_ERROR when no PC (consistent with other voice handlers) - Update tests to match corrected behavior |
||
|
|
ce4326766a |
fix: resolve all golangci-lint issues blocking CI server build
Fix 70+ errcheck violations by adding explicit error discards (`_ =`) for unchecked return values across test helpers and deferred Close() calls. Remove unused `senderID` field from broadcastMsg and unused `defaultCleanupMaxWindow` const. Apply De Morgan's law, remove empty branch, and simplify redundant type declaration per staticcheck. |
||
|
|
9c1d99683c |
fix: address PR review findings (issues #9-#14)
- Fix capacity over-allocation and use strings.Builder in getReactionsBatch (#9) - Replace `any` types and cache Tauri invoke in window-state.ts (#10) - Remove custom `contains` helper, fix NilHub tests to pass nil (#11) - Add nil guards before hub method calls in admin handlers (#12) - Run golangci-lint v2: modernize interface{}/any, range-over-int loops, remove dead code, fix errcheck, add .golangci.yml config (#13) - Add 23 client unit test suites (694 tests), exclude Tauri-coupled files from coverage, achieve 80%+ threshold (#14) Closes #9, closes #10, closes #11, closes #12, closes #13, closes #14 |
||
|
|
049b58c183 |
fix: prevent stale "online" status for users not connected via WebSocket
- Remove premature UpdateUserStatus("online") from REST login handler;
the WebSocket serve.go already sets "online" on actual WS connect
- Add ResetAllUserStatuses() called at server startup to clear stale
statuses from previous runs or crashes (alongside ClearAllVoiceStates)
|
||
|
|
c1c25ed26c |
feat: implement full client UI from mockup — 10 phases, 331 tests
Client UI: - Design system: Colors, Typography, Controls resource dictionaries - Message actions: reply compose bar, hover edit/delete/reply buttons - Rich content: code blocks, attachments, system messages, content parser - Server strip: 72px sidebar with server icons, home button, add server - Status picker: popup for changing online/idle/dnd/invisible status - ConnectPage: server health check dots with auto-refresh - User popup: profile card with banner, avatar, roles, member since - Emoji picker: 6 categories, search, grid of Unicode emojis - Settings overlay: full-screen with sidebar navigation - Friends/DM view: sidebar + friends list with tabs (online/all/pending) - Toast notifications: auto-dismiss after 3s with fade animation Models & services: - Attachment model added to Message, ApiMessage, ChatMessagePayload - EditMessageAsync, DeleteMessageAsync, SendStatusChangeAsync APIs - MessageContentParser (code blocks, inline code, bold, italic) - EmojiData, ToastService, HealthStatusToBrushConverter Server (from prior session): - Voice room management, SFU, speaker detection - ACME/TLS support, config improvements - Protocol and schema updates Tests: 331 passing (61 converter + 24 voice service + 34 voice VM + 41 parser + 9 edit/delete + existing) |
||
|
|
6eba999233 |
feat: add Let's Encrypt ACME support, fix security issues, improve server UX
Server: - Add Let's Encrypt (ACME) TLS mode with autocert, HTTP-01 challenges on :80, and automatic certificate renewal (tls.mode: "acme" in config.yaml) - Add ASCII art startup banner with server info and endpoint URLs - Fix CSP blocking admin panel inline styles/scripts (per-route override) - Suppress TLS handshake error noise in console output - Fix TOCTOU race in invite consumption (atomic UPDATE with row-count check) - Fix sendMsg mutex race condition (hold lock for entire send) - Fix permission override formula (deny-first, allow-wins) - Fix voice join parsing channelID before permission check - Add session expiry check at WebSocket auth and periodic revalidation - Add message length limit (4000 chars) and emoji length validation (32 bytes) - Add file size enforcement in storage after io.Copy - Add checksum URL validation in updater - Add backup path traversal protection (BackupToSafe) - Add self-modification guard in admin handlePatchUser - Fix admin ownerOnlyMiddleware to use context user instead of re-auth - Remove redundant startup log lines (banner shows same info) - Add periodic expired session cleanup (15-min ticker) - Add permissions package with bitfield constants and EffectivePerms - Add rate limiter cleanup goroutine to prevent unbounded growth - Add auth helpers (IsEffectivelyBanned, IsSessionExpired) - Add WebSocket origin validation Client: - Add TOFU certificate trust service - Add receive loop error handling - Fix redundant else-if in OnChatMessage |
||
|
|
73a621ef0f | feat: implement server auto-update API endpoints with download, verify, and restart | ||
|
|
a1434ad07f |
feat: implement Phase 1 server skeleton with TDD
- Scaffold Go module (github.com/owncord/server) with all package dirs - config: koanf-based YAML loader with env var overrides, default generation - db: pure-Go SQLite (modernc, no CGO), WAL mode, FK enforcement, full 15-table schema from SCHEMA.md including FTS5 and idempotent migrations - auth/tls: ECDSA P-256 self-signed cert generation, LoadOrGenerate for all 4 TLS modes (self_signed, acme, manual, off) - api: chi router with request ID middleware, /health and /api/v1/info - main: graceful shutdown (30s timeout), structured slog JSON logging - Stubs for ws, storage, admin packages ready for Phase 2+ Test coverage: api 100%, auth 85.7%, db 82.4%, config 80.6% Binary: chatserver.exe 12MB, GOOS=windows GOARCH=amd64 |