Files
OwnCord/docs/audit-2026-08-19.md
J3vbandClaude 312ac4bbf4 docs: add the 2026-08-19 repo health audit (#1395)
Full-repo health check at eacba10: prior-finding closure verification
across all five dated audits, dynamic checks (all suites green, server
boots clean), doc/code drift for the three reference specs, and a static
sweep of the ws hub, reconnect/sync, and the REST/WS boundary. Findings
ranked BROKEN/FRAGILE/DEBT with a fix-order and alpha-exit roadmap.


Claude-Session: https://claude.ai/code/session_01HtkxwdqE4pUv82GQPRsTeQ

Co-authored-by: Claude <noreply@anthropic.com>
2026-08-20 02:33:07 +02:00

32 KiB
Raw Permalink Blame History

OwnCord — Repo Health Audit

Date: 2026-08-19 Branch: claude/repo-health-audit-s0xnyo (audited tree: eacba10 = main) Scope: full-repo health check — prior-finding closure verification, dynamic checks (build/test/boot), doc/code drift across the three reference specs, and a static sweep focused on the WebSocket hub, reconnect/sync handling, and the REST/WS boundary. Read-only; no code changes ship with this audit. Relationship to prior audits: successor to the 2026-08-04 pair (security, docs & coverage), which remain the closure trackers for their own findings. Every item those audits left open is re-verified in §2. Method: ten parallel analysis passes (3 prior-finding verification, 3 doc-drift, 4 static-sweep), every BROKEN/FRAGILE candidate then re-traced by an independent adversarial verifier instructed to refute it (7 confirmed, 1 downgraded, 0 refuted). Every dynamic-check number in §3 comes from an actual run in this session. Rank legend: BROKEN = fails or states falsehoods now · FRAGILE = works today, likely to break · DEBT = cleanup, no user impact.


1. Executive summary

The code is in the best shape any of the six audits has found it: every suite passes, the server boots clean, the bug-hunt ledger is fully closed (188 fixed / 2 declined / 1 duplicate / 0 open), and the heavily-audited subsystems — hub locking, replay tiers, permission funnels, plugin sandbox — all verified sound again (§7).

What has decayed is the documentation, again. Fifteen days after the 2026-08-04 spec refresh, the three reference docs carry ten newly wrong statements (§4): schema.md misses two migrations and documents the exact FK behavior migration 030 was written to remove; protocol.md claims DM chat events are unsequenced when they are stamped, ring-buffered and replayable, and documents five rate limits as "None" that are all enforced; api.md tells non-admins they can call an admin-only endpoint and documents three error codes the server never emits. The keep-docs-current-per-PR rule (A-2026-07-03) is not holding on its own.

On the code side the sweep found five FRAGILE items and no BROKEN ones — two silent-failure gaps in auth persistence, a role-change/handshake race that can leave a socket on stale subscriptions, dead-on-arrival client replay-dedup machinery, and one block-enforcement asymmetry (blocking a user does not evict them from a live 1:1 DM voice call) that continues the exact defect family the 2026-08-04 security review closed. One client unit test is a load-sensitive timeout flake. Everything else is debt.


2. Prior-finding closure verification

Every item left open by a prior audit, re-verified against eacba10.

2.1 Carried findings

Item Sev Verdict 2026-08-19 Evidence
A-2026-07-10 router god-constructor MED PARTIALLY FIXED NewRouter is down to 171 lines (Server/api/router.go:39-209) with 7 extracted helpers (routerTOTPKey :213 … routerMetricsRoutes :412), but remains the composition root: builds limiter/services/hub, spawns go hub.Run() (:161), and the returned cleanup (:204-206) still covers only limiterStopCh
A-2026-07-11 Hub Set* post-construction wiring MED STILL OPEN, hazard mitigated 6 setters remain (ws/hub_events.go:40,48,60,70, ws/hub_livekit.go:10,48); three are now atomic stores safe post-Run, the other three are guarded by rejectIfRunning (ws/hub.go:721-728) — a missed setter is no longer a race, but the temporal coupling stands
A-2026-07-14 scattered client constants LOW STILL OPEN, grew #5865F2 ×19 across 12 files (was 18/11); localhost:8443 ×3; setTimeout/setInterval at 114 call-site lines across 43 non-test files (was 64); src/lib/constants.ts still holds only 2 constants
2026-04-07 #9 auth handler bypasses service layer MED STILL OPEN Server/api/router.go:117 passes database *db.DB to MountAuthRoutes while siblings get svc; handlers call database.* directly (auth_handler.go:166,201,209,483,572,584,673,757)
2026-04-07 #5 plugin HTTP exfiltration accepted ACCEPTED RISK INTACT All four mitigations re-verified: defaults off + empty allowlist (config/config.go:351-357), empty allowlist denies all (plugin/host_http.go:140-158), WASI-only runtime with no host imports (plugin/sandbox_wazero.go:100-113), inert EventSink.Dispatch + no production Subscribe (plugin/host_events.go:122-131)
A-2026-07-06 residual: direct DB above service layer MED IMPROVED ~50% ~170 direct call sites in non-test api/admin/ws (was ~359), 30 files; top offenders ws/voice_join.go (14), admin/handlers_channel_perms.go (13), ws/hub_broadcast.go (11)
channelCanSend copy (permission-middleware plan disclosure) MED STILL OPEN Server/ws/serve_ready.go:142-160 still hand-mirrors MessageService.checkSendPermission (service/message_perms.go:78-102); guarded by the mirror test ws/can_send_test.go but synced by hand
audit-2026-08-04 §5: backup restore hardcodes DB path; 500-path serves a closed DB obs FIXED admin.SetDatabasePath wired from main.go:337 (pinned by handlers_backup_test.go:553-577); every restore failure branch now rolls back the safety copy and/or restarts (handlers_backup.go:276-329) — no branch leaves a closed DB serving
T-2026-07-25-11 residual: HandleLiveKitHealthForTest duplicate LOW STILL OPEN (test-only) All 7 legacy callers unmigrated (api/middleware_test.go:1026,1049,1075, api/coverage_push_test.go:327,350,373,642); production handler is single-sourced
T-2026-07-25 §4: logctx.WithGroup nesting decision LOW STILL OPEN by design Behavior unchanged (logctx.go:52-54), pinned by test; no production code opens a logger-level group; doc comment defers explicitly (note: stray word "ponytail" at logctx.go:49)
T-2026-07-25 backlog #8: gofmt drift in storage/storage.go LOW FIXED — but recurred elsewhere gofmt -l Server/ is clean except one new drifted file, admin/handlers_users_broadcast_test.go (§6); no gofmt gate exists in CI or .golangci.yml, which is why it drifted in
T-2026-07-25-17/-18/-19 (main.go/seed untested; MainPage.ts/main.ts excluded; no Go/Rust coverage floor) LOW UNCHANGED, documented vitest excludes still carry written justifications (+1 justified entry: noise-suppression.ts); the rest are recorded accepted positions
T-2026-07-25-20 ws flake under -coverpkg LOW CAN'T VERIFY Not reproduced this session (plain go test used); stays on watch
DC-14 reserved protocol entries (voice_speakers, member_leave) P3 STILL OPEN, deliberate Constants + schema entries exist, zero non-test emit sites; consistently documented "Reserved" in protocol.md:1506,1508; a protocol rev for the owner to schedule
admin-e2e soak graduation P2 STILL PENDING ci.yml:311 continue-on-error: true, graduation criterion recorded 2026-08-15 (~30 consecutive green main runs, ci.yml:302-307)
2026-07-19 §5: tauri-build never runs on push to main LOW STILL TRUE ci.yml:440-443 gates on pull_request + base_ref == main; only other desktop build is the v*-tag release workflow
livekitSession.ts monolith MED STILL OPEN, grew 1809 LOC (was 1719); satellites roomEventHandlers.ts/livekitDiagnostics.ts were added without shrinking the core. AccountTab.ts 1185, SidebarArea.ts 848, LoginForm.ts 784
TODO inventory 3 remain ws/voice_e2ee.go:167 (key-holder TOCTOU, documented accepted), ws/deps.go:271 (generics), SidebarArea.ts:659 (H16 DOM thrash). The update_handlers.go container TODO is resolved (503 CONTAINER_DEPLOYMENT via updater.RunningInContainer, update_handlers.go:49-53)

2.2 CI gates and pins (all verified holding)

.nvmrc = 20 matches all six setup-node pins; knip, client-e2e, client-e2e-parity genuinely blocking (no continue-on-error, no || true); -tags wazero ./plugin/... and -tags otel ./telemetry/... run in CI (ci.yml:87-89); rust-tests runs on every event; tests/e2e/E2E-ISSUES.md matches the live Playwright configs (293 tests vs the doc's dated 291 — normal growth, counts are labeled as a run snapshot).

2.3 Plan statuses (docs/plans/)

Plan Verdict Notes
security-hardening-remediation complete, verified W2-4/W3-3 closed with the named locking tests present
channel-visibility-unification, http-tofu-proxy, permission-middleware-consolidation, v2-dispatch-migration, audit-2026-07-19-decisions shipped, nothing reopened only cosmetic line-number drift in citations
security-scan-2026-07-22-remediation shipped; header stale in the good direction F3 follow-up 3 (getIdentityPin fail-open) is marked open but was fixed (identity.ts:158-175, tri-state lookup citing DC-08)
sqlc-adoption accurate residual raw queries (variable IN, FTS, tx) still deliberately raw as listed
tauri-capability-narrowing DNS-rebinding follow-up still open embeds.ts:8 still fetches user-posted URLs from the renderer via @tauri-apps/plugin-http; capabilities/default.json still grants https://* (:33,:36); no Rust og-meta command exists
infrastructure-roadmap shipped except 2 named leftovers TOTP/partial-auth persister seam (no persist code in auth/totp.go) and published capacity numbers (the load-baseline workflow exists to produce them)
discord-parity phases 1-6 shipped; named leftovers open role hoist/mentionable + @RoleName mentions, categories as real entities; its still-dead-code list is one item stale (sounds table was dropped by migration 029)
bug-detection-improvements header stale — partially implemented header says "not implemented" but Tier 1a (make fuzz, Server/Makefile:43-48) and Tier 2 (5 ESLint rules, delivered 2026-08-08 per its own body) exist; Tiers 3-4 open
slash-commands design-only, as stated no plugin_commands migration, no autocomplete surface

3. Dynamic checks (this session, at eacba10)

Environment: Linux container, 4 CPU / 15 GB; go 1.24.7 host + GOTOOLCHAIN=auto; Node 22.22.2 (CI pins 20), npm 10.9.7.

Check Command Result
Go vet cd Server && go vet ./... PASS
Go tests go test -count=1 ./... PASS — 15 test packages ok (ws 54s, db 14.5s)
Client typecheck npm run typecheck PASS
Client lint npm run lint PASS — exit 0 with 25 non-blocking no-underscore-dangle warnings, all in livekitSession.ts:1656-1759 (delegating-method section)
Client unit suite npm test 5044/5045 — one timeout failure, shown to be a load flake (§5, F-5); passes 3/3 in isolation
Server build go build . PASS
Server boot run binary in empty dir CLEAN — first-run config + certs generated, expected WARNs only; /health 200 {"status":"ok","uptime":1,"online_users":0}; /admin 200 serving the SPA (145,663 bytes); LiveKit auto-download + managed start; clean shutdown on SIGTERM
Client build npm run build PASS (chunk-size warnings only)
Protocol codegen gate go run ./scripts/genprotocol diffed against committed output CLEAN — byte-identical Go + TS
sqlc gate make sqlc-verify SKIPPED — sqlc binary absent in this container; CI covers it
knip npx knip exit 0 — zero unused files/exports/deps (4 cosmetic config hints)
Go module hygiene go mod tidy -diff CLEAN — empty diff

4. New findings — BROKEN (all documentation)

No code-level BROKEN findings. All ten are reference-doc statements that are false against the running code; a reader following them writes wrong clients or wrong ops procedures. All were independently re-verified.

schema.md

ID Finding Evidence Fix
B-01 Migration table ends at 029; attachments.message_id documented as ON DELETE CASCADE, the exact behavior migration 030 removed (ON DELETE SET NULL, to stop stranding uploaded files); 031 undocumented schema.md:44-74, :420 vs migrations/030_attachments_unlink_on_message_delete.sql:25 (+ header lines 1-16), 031_sessions_expiry_index.sql Add 030/031 rows; flip :420 to SET NULL with the unlink semantics
B-02 Indexes table lists 3 dropped indexes (idx_sessions_token, idx_invites_code, idx_channel_overrides_channel_role) and omits 7 live ones (idx_channel_overrides_role, idx_attachments_message, idx_messages_pinned, idx_api_tokens_user, idx_users_avatar, idx_channels_dm_group, idx_sessions_expires_at) schema.md:719,723,728 vs migrations/019_perf_indexes.sql, 020_drop_redundant_indexes.sql:6-7, 018:27, 027:28, 028:29, 031:13; the doc's own migration rows (:64-65) contradict its index table Rewrite the table from cumulative migration state
B-03 "The connection pool is pinned to a single connection" — false for file-backed (production) DBs, which run a 1-writer + up-to-64-reader pool split schema.md:27 vs db/db.go:223 (writer.SetMaxOpenConns(1)), :239-244 (reader pool max(4, NumCPU), database.max_readers) Describe the writer/reader split and the knob

protocol.md

ID Finding Evidence Fix
B-04 DM chat_message/chat_edited/chat_deleted/reaction_update documented as unsequenced and excluded from the ring buffer; they are seq-stamped, ring-buffered AND persisted (tier-1 replayable) protocol.md:98,263-264,1489-1494 vs ws/emit.go:23-26hub_broadcast.go:923-935 (nextSeq + replayBuf.Push + persistEvent); emit sites handlers_chat.go:102,123,146, handlers_reaction.go:47 Fix the seq tables + reconnection section
B-05 plugin_broadcast documented "Has seq? No"; it rides the sequenced, replayable channel-broadcast path protocol.md:1524 vs ws/event.go:347-357, hub_broadcast.go:939-964 Flip the row to Yes
B-06 RATE_LIMITED WS errors claimed to "include retry_after in seconds"; no WS error payload ever carries it (only REST 429 headers do) protocol.md:1399 vs ws/messages.go:332-358, ws/event.go:7-12; repo-wide grep negative Delete the clause (or implement the field)
B-07 Five c2s types documented rate limit "None" are all limited: channel_focus 5/s, mark_read 5/s, call_decline 1/3s, chat_command 5/s, ping 2/s protocol.md:1461,1462,1478,1479,1480,1535 vs ws/handlers_presence.go:19-22,107,135, handlers_call.go:15-18,75-77, handlers_command.go:29-36,49-51 (comment cites OC-0091 — limiter added, doc never updated), handlers_ping.go:14-16 Document all five incl. silent-drop vs RATE_LIMITED behavior
B-08 E2EE prose says "Both E2EE message types are rate limited at 5 per second", contradicting the doc's own table (offers 64/s) and the code; the inner 5/s-per-target offer cap is undocumented protocol.md:1112 vs :1433,1437-1439; ws/voice_e2ee.go:16-24,213-223 Fix the prose; document the per-target inner cap

api.md

ID Finding Evidence Fix
B-09 GET /api/v1/diagnostics/connectivity documented "any authenticated user"; code requires ADMINISTRATOR (H-8) — a non-admin following the doc gets 403. Also: limiter is per-IP not "per user"; livekit_url is sanitized to host:port, not the documented full URL api.md:2472,2473,2485 vs api/router.go:153-158, middleware.go:228-229, diagnostics_handler.go:54-58 Fix auth line, limiter scope, example
B-10 Error-code table documents SERVER_ERROR / INTERNAL / TOO_LARGE — none ever emitted by REST (real 500 code is INTERNAL_ERROR, ×60+ sites; oversize upload is 400 BAD_REQUEST); STORAGE_ERROR 507 missing entirely. A client switching on documented codes never matches api.md:57-58,119,181,229,310 vs greps (zero SERVER_ERROR/TOO_LARGE emissions), upload_handler.go:115-146, storage/storage.go:172, ws/errors.go:6 Replace with INTERNAL_ERROR, correct the 413 row, add STORAGE_ERROR

5. New findings — FRAGILE

F-1 — Blocking a user does not evict them from a live 1:1 DM voice call. BlockUser commits with zero side effects (service/block.go:24-54; api/dm_handler.go:83-88 takes no evictor). The only block re-checks for a user already in voice are client-volunteered (ws/voice_join.go:136-141 join gate; :579-590 token refresh). The minute-sweep — whose own invariant comment says "Revocation must evict a live session, not merely block the next join" (ws/hub_sweep.go:167) — re-checks only role CONNECT_VOICE (hub_sweep.go:185,328-354), which has no DM/block branch; LiveKit validates tokens at join only, so a client that stops calling voice_token_refresh keeps its SFU session indefinitely. Contrast: CloseDM evicts via dmVoiceEvictor (api/dm_handler.go:236-242). This is the same guard-asymmetry family as A-2026-08-03 (block check on rings), one sink over. Adversarially verified: CONFIRMED. Fix: have handleBlockUser evict via the existing dmVoiceEvictor for the pair's shared 1:1 DM channel (mirroring handleCloseDM), or add a DM-block re-check to the sweep.

F-2 — Role reassignment racing a WS handshake leaves the socket on old-role subscriptions. The handshake resolves permissions from the auth-time c.user snapshot (ws/serve.go:124-132; computeAllowedChannels via GetRoleByID(user.RoleID) at :670, used by both :306 and :736), and nothing revalidates handshake-time subscriptions (the OC-0024 recheck covers only applySetChannelID; the sweep re-checks CONNECT_VOICE only). The demotion's live revocation, revokeUnreadableChannels (hub_broadcast.go:797-879), early-returns if the user is not yet in h.clients (:818-822) and its Unsubscribe no-ops on a replaced client via the pubsub identity guard (pubsub.go:133-134) — so a role change landing in the handshake window is never applied to that socket, which then receives events for channels the new role cannot see until the next reconnect. RefreshChannelVisibility handles both hazards explicitly (hub_broadcast.go:443-444); the role path predates that pattern. Adversarially verified: CONFIRMED. Fix: re-read the user row inside reconnectPrecheck/handleFreshConnect, and make revokeUnreadableChannels re-resolve the live client before sendMsg/Unsubscribe, mirroring RefreshChannelVisibility.

F-3 — Auth lockout persistence failures are silent. All three lockout writes discard errors with no log: _ = r.store.UpsertLockout(...) (auth/ratelimit.go:172), _ = ...DeleteLockout (:234), _ = ...CleanupExpiredLockouts (:283) — while the load path got exactly this fix as ledger finding OC-0061 (the file's only slog call, :88). The doc comment at :162-164 says "The persist write must land once the lockout is decided", yet a failed write leaves zero trace and the lockout evaporates on restart. Adversarially verified: CONFIRMED (not an OC-0061 duplicate — this is the symmetric write-path gap). Fix: mirror OC-0061 — slog.Warn on all three error paths.

F-4 — Session-cap eviction failure is invisible (observability gap; downgraded from a stronger claim). CreateSession discards the H-6 eviction error (db/auth_queries.go:244 _ = d.q.EvictOldestSessions(...)) with no log anywhere in the chain. The verifier established the cap is not durably disabled — the DELETE trims to the newest 24 so the next successful login self-heals, and a persistent DB failure also fails the subsequent InsertSession — so the residue is: a transient failure lets one session exceed the cap, and no failure of a security control is ever logged. Adversarially verified: DOWNGRADED to this statement. Fix: slog.Warn on eviction error only.

F-5 — One client unit test is a load-sensitive timeout flake. tests/unit/message-list.test.ts "does not report success when renderWindow's own >30-in-2s breaker drops the rebuild" performs 30 synchronous full renderWindow rebuilds of a 100-row jsdom list inside the default 5s vitest timeout. In this session's full run (concurrent with go test on a 4-CPU box) it timed out — 5044/5045 — then passed 3/3 in isolation (~4.2s per whole-file run). Same class as the open T-2026-07-25-20 watch item. Fix: give that test an explicit larger timeout (or shrink the fixture) so CI-under-load cannot produce a spurious red.

F-6 — Client replay-dedup machinery can never engage (dead safety net + misleading tests). The server always writes auth_ok before the replay burst (ws/serve.go:531,537-544), but the client creates replayDedup in the socket-open handler and clears it when auth_ok is processed (ws.ts:463-465, :358) — so the dedup set and isReplaying() (ws.ts:788) are inactive for every real replayed frame; the dispatcher's gates at dispatcher.ts:580,592 never fire, and its own comment at :612-618 admits the timing. The pinning tests inject replay frames in an order a spec-compliant server never produces (ws-reconnect.test.ts:409-497). Meanwhile the server can legitimately duplicate a voice frame on resume (the voice supplement is re-read outside seqMu, serve.go:263-271,553-556), which is what the machinery was presumably for. Today's no-op behavior is actually correct for unread counts, making this inert-but-misleading rather than harmful. Adversarially verified: CONFIRMED. Fix: either delete the inert machinery (and fix comments + tests) or move the clear to end-of-burst — decide, don't leave it half-true.


6. New findings — DEBT

Grouped; every item carries its evidence at the cited lines.

Docs/comments

  • D-01 protocol.md error-code table missing BAD_PAYLOAD and NOT_KEY_HOLDER (emitted at ws/voice_e2ee.go:119-199).
  • D-02 protocol.md ready/member_join field lists incomplete: voice_states omits username/speaking/camera/screenshare, roles omits position/is_default, member_join omits its top-level status field (serve_ready.go:365, db/models.go:99-106,234-245, ws/messages.go:56-67).
  • D-03 api.md: body-cap exemption list wrong (3 exempt prefixes, not 1 — api/constants.go:177-184); identity_public_key accepted by PATCH /users/me but undocumented (profile_handler.go:29-37,100-114); plugin endpoints return plain-text errors contradicting the "all errors use the JSON envelope" claim, and the X-Plugin-Runtime header is undocumented (plugins_handler.go:51-136); /health's 503 degraded state + reason field undocumented (router.go:446-453,509-544); metrics/LiveKit endpoints have their own CIDR keys with admin fallback, not "admin CIDRs" (config/config.go:219-233); updates/apply can 409 RESTART_PENDING/UPDATE_IN_PROGRESS, absent from its table (admin/restart.go:104-112).
  • D-04 Stale comments: buildReady's claims "no slow_mode … voice_* extras" contradicted by readyChannelPayloads directly above (serve_ready.go:312-315 vs :216-224); tsconfig.e2e.json:2-3 + ci.yml:155-156 say "47 spec files + the three playwright configs" (52 and four); stray word "ponytail" in logctx.go:49.
  • D-05 Three plan headers understate shipped work (bug-detection-improvements, security-scan-2022-remediation F3-3, discord-parity's sounds-table row) — all drift in the direction that misdirects planning toward finished work.
  • D-06 Branch-flow docs contradict practice: README:230-232 and contributing.md:152-153 say branch from dev, PR to dev, "dev is merged to main for releases"; root CLAUDE.md:63 says branch from main, PR to main. Practice matches CLAUDE.md — the 5 most recent merged PRs (#1390-#1394) are base:main, and the sync direction is inverted (commit 40cba79 merges main into dev; dev is 4 graph-refresh commits ahead, 0 behind).

Dead code / hygiene (deadcode + knip corroborated)

  • D-07 Hub.hasChannelPerm is unreachable in production; its doc comment claims the sweep calls it, but the sweep uses hasChannelPermChecked (ws/handlers.go:286-292 vs hub_sweep.go:185,320-354); the F5 guarantee test pins the dead copy (voice_perm_stale_test.go).
  • D-08 UserService.SetCustomStatus/ClearCustomStatus: zero production callers; "Called on logout" comment false (logout writes the DB directly, auth_handler.go:584); duplicates the length validation HandlePresenceUpdate owns (service/user.go:201-215 vs channel.go:183).
  • D-09 Production-unused hub APIs: PubSub.PublishHigh (pubsub.go:181), Hub.BroadcastToAllLow (hub_broadcast.go:907), EventRingBuffer.EventsSince (ringbuffer.go:43), and the Hub.Register/Unregister clientEvents queue (hub.go:446-453, whose Run branch is test-only and would fail closed for a future production caller); TopicsForClient's "debugging and tests" doc is stale — it has a production caller (hub_broadcast.go:850).
  • D-10 gofmt drift: admin/handlers_users_broadcast_test.go (one misaligned field); no gofmt/gofumpt gate exists in CI or .golangci.yml.
  • D-11 25 non-blocking no-underscore-dangle lint warnings accumulated in livekitSession.ts:1656-1759.

Boundary asymmetries (all privileged-actor or minor; the next members of the audit-2026-08-04 defect family — worth sweeping in one pass)

  • D-12 typing_start requires only READ_MESSAGES on guild channels while the send it advertises requires SEND_MESSAGES (announcement: MANAGE_MESSAGES) — a send-muted member can still broadcast typing (service/channel.go:133 vs message_perms.go:93-99; the DM branch is fully gated).
  • D-13 getPermChannel answers a DM id with 400 "DM channels do not support permission overrides" while its sibling getAdminChannel deliberately answers 404 to avoid confirming which ids are DMs (A-2026-08-02) — same perimeter, one oracle left (admin/handlers_channel_perms.go:40-44 vs handlers_channels.go:44-61).
  • D-14 Admin channel create/patch has no length cap or sanitation on name/topic/category (only the 1 MiB body cap); the group-DM writer of the same column caps at 100 runes and its comment claims a channel-name parity that does not exist (admin/handlers_channels.go:104-116,171-183 vs service/dm.go:173-175,234-237).
  • D-15 Invite create/revoke are the only privileged mutation family with no audit row and no log line — an operator cannot see who minted an invite (service/invite.go:29-77; every sibling family audits: roles, bans, emoji, settings, channels, purge, auth).
  • D-16 channel_focus's read-state write is called "the load-bearing half" in its own comment, then discarded unlogged (service/channel.go:275-286); a persistent failure means unread badges never clear with zero trace.

7. Verified clean (read end-to-end and found sound)

Recorded so future audits don't re-spend the effort. Hub dispatch loop (panic breaker, stop idempotence, register/replacement ordering); every seqMu critical section (no lock held across real I/O; shed frames never burn a seq); lock ordering across seqMu/h.mu/ps.mu/rb.mu/c.mu/keyHolderMu with no reverse edge; pubsub index hygiene and identity guards; three-tier client queues + writePump anti-starvation; topic rate limiter and pruner/persister lifecycle (no goroutine leaks found); ring-buffer tier boundaries (both edges fall conservative to full ready) and the closed lost-update window (register-inside-seqMu); cold-tier contiguity probes including the correct off-by-one on the prune gap; replay authorization incl. the OC-0206 watermark re-check under seqMu; seq reseeding across restarts; client backoff/generation discipline (OC-0219) and optimistic-send reconciliation across reconnects; REST/WS message-family parity (send/edit/delete/reaction/ pin/purge all funnel through the same service gates); REST-mutation fan-out ordering (post-commit, context.WithoutCancel, watermark bumps); admin users/bans/roles/override families (hierarchy, audit rows, invalidation ordering); plugin sandbox mitigations (§2.1); protocol type inventory (27 c2s / 39 s2c identical across schema, Go, TS, and doc tables) and generated-code gates (protocol codegen byte-identical; go mod tidy clean; knip zero findings; no TS zombie modules; Rust unwraps confined to test modules); E2EE section of protocol.md including the eacba10 epoch change — fully documented and accurate.


8. Verdict

MUST fix (before the next feature work lands on top):

  1. The ten BROKEN reference-doc statements (§4) — one spec-refresh PR, same shape as 2026-08-04's C1. Docs are the record everything else keys off, and two of the ten are security-relevant reader traps (B-09 auth claim, B-07 undocumented limits).
  2. F-1 block/voice eviction — it is the live continuation of the guard-asymmetry family the security review closed; small, testable fix.
  3. F-3 + F-4 + D-16 silent-failure logging — three slog.Warn lines restoring visibility to security/UX controls.

CAN wait (scheduled, not urgent): F-2 (handshake race — real but narrow window; needs a careful hub change + deadlock-tag run), F-5 (test timeout bump), F-6 (delete-or-fix decision), all §6 DEBT including the D-12..D-15 asymmetry sweep and the D-06 contributing-docs correction, and every §2 carried architecture item (service-layer consolidation, hub constructor, client constants, monoliths).

Already done (no action): everything in §2 marked FIXED/intact — backup restore, CI gates, .nvmrc, plugin mitigations, DC-08, W2-4/W3-3, protocol codegen and ledger hygiene — plus §7's verified-clean list. The bug-hunt ledger has zero open findings.


9. Roadmap

9.1 Fix order (every BROKEN + FRAGILE, dependencies first, smallest verifiable step first)

  1. F-5 test-timeout bump (one line in one test) — protects the CI signal every later step relies on.
  2. §4 B-01..B-10 spec-refresh PR for schema.md/protocol.md/api.md, folding in D-01..D-05 while in the files — pure docs, zero code risk, ends the "docs are the record" debt before code changes add more drift.
  3. F-3 + F-4 + D-16 logging PR (three slog.Warn sites, mirrors the shipped OC-0061 pattern) — trivial, independently testable.
  4. F-1 block → DM-voice eviction (reuses the existing dmVoiceEvictor seam CloseDM already exercises; pin with a test like A-2026-08-03's) — smallest security-adjacent code fix.
  5. F-2 handshake/role-change race — hub-internal, needs the ci-check deadlock pass; do after 4 so the voice-eviction test infrastructure is fresh.
  6. F-6 replay-dedup decision (delete vs repair) — last because it needs an owner call; either branch also fixes the misleading tests/comments.

9.2 Alpha exit — what stands between here and a stable beta

(each item backed by a finding above or an existing plan)

  • Graduate the two non-blocking CI legs: admin-e2e (soak criterion already written in ci.yml:302-307) and a tauri-build push-to-main or nightly trigger (2026-07-19 §5, still true) — after which every surface is gated.
  • Add a gofmt gate (D-10 proved the gap; one CI line).
  • Close the DNS-rebinding follow-up (tauri-capability-narrowing): move link-preview fetches behind a Rust command and drop the https://* capability — the last open item from the F3 security track.
  • Finish the two infrastructure-roadmap leftovers: TOTP/partial-auth persister seam, and publish the capacity numbers the load-baseline workflow exists to produce.
  • Start the service-layer consolidation at MountAuthRoutes (2026-04-07 #9, the designated first step of backlog 12), then the hub constructor cleanup (A-2026-07-11 S-effort half).
  • Unify channelCanSend onto the permission checker — the last hand-mirrored visibility/permission copy.
  • Protocol rev decision (owner): emit-or-drop voice_speakers / member_leave (DC-14), and wire the plugin wire types into the client or document desktop non-support (D-02's sibling).
  • Docs process hardening: the 15-day re-drift (§4) says the per-PR rule alone doesn't hold; add the docs-check line to CI or the PR template checklist with teeth (a grep-able "docs reviewed" gate), plus the D-06 contributing-flow correction so new contributors aim at the right branch.

9.3 Ideas — opinion, not findings

  1. Slash-commands Phase A (plan exists, design-only today) — first user-visible plugin payoff.
  2. Role hoist/mentionable + @RoleName mentions (discord-parity leftover).
  3. Categories as real entities (discord-parity leftover).
  4. Linux ARM64 server binary + prebuilt Docker image (README platform-matrix gap).
  5. macOS PTT support (currently a stub, noted in discord-parity's dead-code list).