Commit Graph
8 Commits
Author SHA1 Message Date
J3vbandClaude Fable 5 03fcb7d518 fix: execute the 2026-08-19 audit fix order (docs refresh + five FRAGILE fixes) (#1396)
* docs(plans): phased remediation plan for the 2026-08-19 audit

Executes the audit's §8 MUST-fix verdict and §9.1 fix order: one phase per
finding group, statuses updated in place as phases land.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HtkxwdqE4pUv82GQPRsTeQ

* test(client): give the renderWindow-breaker test its own timeout (audit F-5)

30 synchronous 100-row jsdom rebuilds can exceed vitest's default 5s on a
loaded runner; the test timed out once under CI-like load and passes in
isolation, so it now carries an explicit 20s budget.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HtkxwdqE4pUv82GQPRsTeQ

* docs: fix the ten wrong reference-doc statements from audit 2026-08-19 (B-01..B-10)

schema.md: migrations 030/031 documented, attachments ON DELETE SET NULL
(matching 030's rebuild), index inventory rewritten from cumulative migration
state, writer/reader pool split described, default-roles table made a
consistent post-migration snapshot, dbgen preamble updated.

protocol.md: DM chat events documented as sequenced/ring-buffered/replayable
(they are), plugin_broadcast seq flipped to Yes, retry_after claim removed
(no WS error carries it), the five enforced-but-documented-as-None rate
limits added (channel_focus, mark_read, call_decline, chat_command, ping),
E2EE announce/offer budgets corrected incl. the per-target inner cap,
BAD_PAYLOAD and NOT_KEY_HOLDER added to the error table, ready voice_states/
roles field lists completed, member_join top-level status documented.

api.md: diagnostics endpoint is ADMINISTRATOR-only (H-8) with a per-IP
limiter and host:port livekit_url, error-code table now matches emitted codes
(INTERNAL_ERROR, STORAGE_ERROR 507; oversize upload is 400), body-cap
exemptions listed, identity_public_key documented on PATCH /users/me, plugin
endpoints' plain-text errors + X-Plugin-Runtime header documented, /health
503 degraded state documented, metrics/LiveKit CIDR keys named, updates/apply
restart-conflict 409s added.

Also folds in the audit's D-04/D-05 comment and plan-header staleness fixes
(buildReady comment, e2e spec-count comments, logctx stray word, three plan
status headers).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HtkxwdqE4pUv82GQPRsTeQ

* fix(server): log the five silently-discarded persistence errors (audit F-3/F-4/D-16)

Lockout Upsert/Delete/Cleanup failures (auth/ratelimit.go), the H-6
session-cap eviction failure in CreateSession (db/auth_queries.go), and the
channel_focus read-state write failure (service/channel.go) all discarded
their errors with no trace — a brute-force lockout could silently fail to
survive a restart. In-memory behavior is unchanged (warn-and-continue); the
lockout write paths are pinned by tests mirroring OC-0061's load-path test.
The session-cap and read-state sites are log-only additions on seams the
existing suites already exercise on the success path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HtkxwdqE4pUv82GQPRsTeQ

* fix(dm): blocking a user evicts them from the pair's live 1:1 DM voice call (audit F-1)

The block gate ran only at voice_join and voluntary voice_token_refresh, so
a blocked user already in the shared 1:1 DM call kept their session
indefinitely — the same guard-asymmetry family as A-2026-08-03. handleBlockUser
now severs the call through the dmVoiceEvictor capability handleCloseDM
already exercises, using a new find-only FindDMChannelIDBetween lookup
(sqlc-generated; mirrors GetOrCreateDMChannel's is_group=0 clause so group
DM calls stay exempt, matching requireDMNotBlocked). Pinned by three handler
tests: shared-DM eviction, no-DM no-op, group-only no-op.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HtkxwdqE4pUv82GQPRsTeQ

* fix(ws): close the role-reassignment/handshake race (audit F-2)

A role reassignment landing mid-handshake was invisible for the socket's
whole life: both handshake paths resolved permissions from the auth-time
c.user snapshot, revokeUnreadableChannels early-returns for a user not yet
in h.clients, and its Unsubscribe no-ops on the pubsub identity guard once a
reconnect replaced the client.

Three coordinated fixes: (1) refreshUserSnapshot re-reads the user row (and
role name) in reconnectPrecheck and handleFreshConnect, fail-closed; (2) the
resume-fallback path re-reads the role once more after registerNow and runs
the revocation pass when it moved, so the reassignment-vs-registration
orderings meet in the middle; (3) revokeUnreadableChannels re-resolves the
live client immediately before acting, mirroring RefreshChannelVisibility.
Pinned by four tests driving real WS handshakes through the existing race
hooks plus a new pre-register/pre-act hook pair; ws suite green under the
default and deadlock builds.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HtkxwdqE4pUv82GQPRsTeQ

* refactor(client): remove the inert replay-dedup machinery (audit F-6)

The server writes auth_ok before the replay burst, so replayDedup — created
on socket-open and cleared when auth_ok is processed — could never be active
for a real replayed frame, and the dispatcher's isReplaying() unread gates
never fired. Their no-op behavior is the correct behavior (a buffer/db
resume has no ready payload, so replayed frames must count as unread), so
the machinery, the gates, and the misleading comments are removed rather
than repaired. The pinning tests injected replay frames in an order a
spec-compliant server never produces; they are replaced by a test pinning
the real contract (frames after auth_ok are dispatched verbatim; duplicate
handling belongs to the stores). Client suite green: 5036/5036.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HtkxwdqE4pUv82GQPRsTeQ

* docs(plans): mark remediation phases 1-6 done

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HtkxwdqE4pUv82GQPRsTeQ

* fix(ws): nolint the context-less revoke call golangci-lint flags

revokeUnreadableChannels takes no context by design (admin HubBroadcaster
interface); annotate the one call site inside a ctx-taking function, matching
the RefreshChannelVisibility precedent. golangci-lint v2.11.3: 0 issues.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HtkxwdqE4pUv82GQPRsTeQ

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-08-20 03:49:13 +02:00
J3vbandClaude Opus 4.8 1485e13f9a fix(security): gate TLS trust-on-first-use behind explicit confirmation (F4/F8)
The http and ws proxies accepted ANY certificate on first use and silently
pinned it, forwarding login credentials and the bearer token before the user
ever saw the fingerprint — an on-path attacker at first contact captured them.
The three proxies also duplicated the TLS verifier and TOFU logic verbatim.

- Extract the shared verifier, cert-store helpers, and a pure `decide` function
  into src-tauri/src/tofu.rs (used by the http/ws/livekit proxies).
- Split the trust decision from persistence: a first-use cert is no longer
  pinned or forwarded to. The proxy rejects (ws: Err; http: 502) and emits a
  cert-tofu "first_use" event; the only writer of a pin is the explicit
  accept_cert_fingerprint command.
- Frontend: a global cert-tofu listener (active during the connect page's health
  checks, before any WS connect) surfaces an SSH-style first-use confirmation
  modal. On accept the fingerprint is pinned and the server re-checked; nothing
  is sent to an unconfirmed host.

Closes security-scan F4 (http proxy) and F8 (ws proxy). Verified: client
typecheck/lint/format clean, full unit suite 3311/3311 green (incl. new ws
first-use routing + modal tests). Rust compiles in CI (cargo clippy) per the
client CLAUDE.md; pure tofu logic covered by #[cfg(test)] unit tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 12:47:30 +02:00
Claude fc5a94cb50 feat(client): connection-status store + no-silent-failure batch
Implements the next four gaps from the client UX spec (docs/architecture/ux).

Connection status as single source of truth (spec §3):
- main.ts registers the one writer: ws.onStateChange → toConnectionStatus
  (new 5→3 state mapper exported from ws.ts) → ui.store.connectionStatus.
- Consumers now subscribe to the store instead of ad-hoc ws wirings: the
  MainPage reconnect banner (which also gains a "Disconnected" state via
  ServerBanner.showDisconnected instead of going stale), ChannelController
  composer gating + per-click send guard, and the UserBar presence picker.
- Fixes a latent production bug: SidebarArea never passed ws to UserBar, so
  the status picker was permanently disabled and its presence_update path
  dead. It now gates on the store and receives the ws send path.
- The one-shot connected-overlay wiring stays on ws.onStateChange by design
  (it needs the exact internal transition); LiveKit voice reconnection stays
  independent ("retrying underneath").

Transport backpressure surfaced (spec §5):
- ws.ts sendRaw no longer drops local send failures silently: send() passes
  the envelope id, and failures notify a new onSendFailure(id, code)
  listener — channel full → NETWORK, closed/not-open → OFFLINE (deferred a
  microtask on the not-open path so the optimistic row registers first).
- The dispatcher fails the matching pending row via markSendFailed, exactly
  like a server error reply; id-less sends (heartbeat) and fire-and-forget
  sends (typing, presence) stay silent by design. MessageList renders the
  new NETWORK reason ("Connection problem — message not sent").

uploadFile honors global 401 handling (spec §5):
- api.uploadFile now calls onUnauthorized and throws ApiClientError(401)
  like every other REST call; main.ts sets the "Your session expired — sign
  in again." transient error so the connect page shows the reason.

History fetch loading/error states (messaging.md §1):
- messages.store gains per-channel historyLoadState (loading/error, absent
  = idle) with setChannelLoading/setChannelLoadError; setMessages and
  clearChannelMessages clear it.
- MessageController.loadMessages sets loading synchronously before the
  fetch and marks error inline instead of a toast; MessageList renders an
  in-region spinner placeholder or an inline error + Retry (onRetryLoad
  re-invokes loadMessages via ChannelController).

Also fixes two pre-existing eslint errors in api.ts (redundant assertions).

Docs: the corresponding gap callouts in docs/architecture/ux are updated
(README §3/§5, messaging.md §1/§3/§6, channels-members-dms.md block-gating
note no longer claims the composer lacks a read-only mode).

Verified: tsc + full client unit suite (3225 tests) + oxlint/eslint +
prettier all green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 19:08:19 +00:00
J3vb 839b07e8c6 fix: show notification banner on TOFU first-use cert trust (BUG-133)
The cert-tofu event listener now handles "trusted_first_use" status
and shows a visible notification banner with the server hostname and
SHA-256 fingerprint. Adds onCertFirstTrust callback to the WS client
API. First-use certificate trust is no longer silent.
2026-04-02 12:47:07 +02:00
jevb 867041b094 test: client test updates — fix failures + align with security hardening
Update 111 test files to match security hardening changes:
- acceptInvalidCerts now conditional on allowSelfSigned
- Credential store no longer returns passwords over IPC
- File type validation uses strict MIME allowlist
- Search rate limiter timing adjustments
- Dispatcher cleanup mock additions
- Audio elements screenshare mute preservation

2962 tests passing across 110 test files.
2026-04-01 11:40:31 +02:00
jevb 4c4526e539 fix: security hardening — 45 issues from full-project Copilot audit
Critical (6):
- C1: SQL injection in VACUUM INTO backup path — strict character allowlist
- C2: Unlimited binary download in updater — 500MB LimitReader
- C3: JSON injection in SSE log stream — json.Marshal instead of concat
- C4: CSS injection via custom themes — reject () and {} in values
- C5: Silent DM message loss — error response on participant lookup failure
- C6: LiveKit URL credential leak — strip creds from diagnostics endpoint

High (11):
- H1: DB errors no longer trigger login rate-limit lockout
- H2: Permission fetch failure returns 500, not empty channel list
- H3: TOCTOU race on duplicate WS — atomic check-and-register in hub
- H5: LiveKit webhook verifies voice channel match (already implemented)
- H7: Server host address validated before storage (hostname regex)
- H8: WS message deduplication on reconnect replay (1000-entry Set)
- H9: Admin setup endpoint rate limited (5/min/IP)
- H10: Backup responses return filename only, not full path
- H11: Update binary recovery failure now alerts admin

Medium (17):
- M1: MIME type from magic bytes, not client header
- M3: Nil guard on DM broadcast recipient
- M5: LiveKit process run-done channel race fixed
- M6: Backup restore calls fsync before close
- M7: Partial download file cleaned up on error
- M8: Admin CSP uses nonce instead of unsafe-inline
- M9: Client rate limiter enforced for presence_update
- M10: Voice joinedAt not reset on double-join
- M11: Unread count skips increment during reconnect replay
- M13: Category type uses exact match, not substring
- M14: Storage LimitReader off-by-one fixed
- M15: GitHub token only sent to GitHub hosts
- M16: Content-parser ReDoS regex replaced with split approach
- M17: Audio device switch error handling added

Low (11):
- L1: CORS uses configured origins instead of wildcard
- L2: HSTS header added when TLS enabled
- L3: Consistent JSON error responses across all endpoints
- L4: File modtime from stat, not time.Now()
- L5: Malformed invite JSON returns 400
- L6: TouchSession failure logged at warn
- L8: MessageInput timers cleared on destroy
- L9: Log persistence flush errors caught
- L10: Credential save failure surfaced to user
- L11: Case-insensitive asset name matching in updater

Found by GitHub Copilot full-project review (claude-sonnet-4.6 + claude-haiku-4.5).
2026-03-29 12:35:04 +02: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
jevb 3d022b68e4 test: add test helpers and integration tests for dispatcher + stores
- Create mock-ws helper with simulateMessage/simulateStateChange
- Create fixture factories for messages, members, channels, reactions
- Create resetAllStores utility for clean test isolation
- Add 16 integration tests covering:
  - Ready payload hydration (channels, members, voice)
  - Chat message flow with unread tracking
  - Message edit and delete
  - Reaction add/remove via reaction_update
  - Chat send confirmation via chat_send_ok
  - Typing indicator updates
  - Member ban handling
  - Voice config and speakers state

385 tests passing across 22 test files.
2026-03-15 20:46:38 +01:00