Commit Graph
684 Commits
Author SHA1 Message Date
J3vb 352b7c8cc1 feat(client): narrow Tauri HTTP capability scope
Add a deny list for https loopback literals (localhost, 127.0.0.1, both
with and without an explicit port) to `http:allow-fetch`. All legitimate
server traffic reaches loopback over http through the Rust TOFU proxy, so
an https loopback fetch from the renderer can only be an attempt to probe
some other local service. `deny` wins over `allow` in Tauri's scope check.

Drop the scope objects from `http:allow-fetch-send` and
`http:allow-fetch-read-body`, leaving bare identifiers. tauri-plugin-http
validates the URL exactly once, in the `fetch` command; both of these take
an already-validated ResourceId and never consult a scope, and a
permission declaring `commands.allow` contributes command scope only —
it never merges into the plugin's global scope. The blocks were inert
configuration that read like defence in depth. The capability description
now records why, so they are not re-added on reflex.

The `https://*` wildcard on `http:allow-fetch` stays: link previews fetch
arbitrary user-posted URLs by design, and Tauri scopes per command, not
per JS caller. See docs/plans/tauri-capability-narrowing.md.

Add tests/unit/capabilities-scope.test.ts as a regression guard on the
shape of the grant.
2026-07-20 14:51:14 +02:00
J3vb 2511af345e docs: design note for narrowing Tauri HTTP capabilities
Investigates whether the https wildcard on the three http:allow-fetch*
identifiers can be enumerated now that the HTTP TOFU proxy has landed.

Two findings change the answer:

- Only http:allow-fetch is URL-scoped. tauri-plugin-http validates the URL
  in the `fetch` command only; `fetch_send` and `fetch_read_body` take a
  ResourceId and never consult a scope, and Tauri's ACL resolver keeps a
  command-declaring permission's scope as command scope. The allow blocks
  on the other two identifiers are inert.
- The host set is not enumerable: api.ts, profiles.ts and attachments.ts
  are loopback-only (attachment URLs are always server-generated
  /api/v1/files/<id>) and media.ts hits one fixed YouTube oEmbed URL, but
  embeds.ts fetches arbitrary user-posted URLs by design.

Decision: keep the https wildcard with a loopback deny list, drop the two
inert scope blocks, and record the Rust-side link-preview command as the
follow-up that would actually make the set enumerable. Residual risk
(CSP connect-src already allows https:) stated explicitly.
2026-07-20 14:45:34 +02:00
J3vb 25716aaa1c Merge pull request #1197 from J3vb/docs/backlog-bookkeeping
docs: reconcile audit backlog table with closure status
2026-07-20 13:12:56 +02:00
J3vbandClaude Fable 5 2ec53863c2 docs: reconcile audit backlog table with closure status
Rows 1, 2, 4, 6, 7, 8, 9 were all closed in the closure table but never
struck in the section 6 backlog, making the remaining work look ~4x larger
than it is. Only rows 10 (partial) and 12 are still open.

Also drops the stale "V1/V2 dispatch" blurb from the architecture index,
which contradicted websocket.md after the V1 registry was deleted in #1196.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 12:47:34 +02:00
J3vb e4f5a9358e Merge pull request #1196 from J3vb/feat/audit-backlog-3-11
feat: unify channel visibility + finish V2 dispatch migration (audit backlog 3, 11)
2026-07-20 12:41:28 +02:00
J3vb a56d131add Merge branch 'main' into feat/audit-backlog-3-11 2026-07-20 12:41:21 +02:00
J3vb fbef95fcbe Merge pull request #1195 from J3vb/feat/logaudit-no-silent-discard
feat(audit): no-silent-discard LogAudit policy via shared best-effort helper
2026-07-20 12:39:49 +02:00
J3vb 868306a06d Merge pull request #1194 from J3vb/feat/dm-block-gating-updater-progress
feat(client): DM block composer gating + updater download progress
2026-07-20 12:39:35 +02:00
J3vb 723c1e591b Merge pull request #1193 from J3vb/feat/voice-e2ee-status
feat(voice): surface voice-session + E2EE status and freeze controls on WS reconnect
2026-07-20 12:39:11 +02:00
J3vb 44478617a4 Merge pull request #1192 from J3vb/fix/role-store-logout-claudemd
fix(client): unify role store, revoke session on logout, un-stale test-suite docs
2026-07-20 12:38:51 +02:00
J3vbandClaude Fable 5 b28fad5d4e docs: record Node 22+ jsdom localStorage trap and sweep remaining KNOWN RED claims
The suite was never red: on Node 22+ native Web Storage shadows jsdom's
localStorage, failing ~478 unrelated tests locally. Documents the workaround
where a future session will hit it, and corrects the stale KNOWN RED
assertions in docs/architecture/client.md and the ci-check skill that
contradicted the new green-and-must-stay-green rule.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 12:18:38 +02:00
J3vbandClaude Fable 5 3e42c4a4f0 docs: close audit items 3 and 11 (channel-visibility + V2 dispatch)
- audit-2026-07-19.md: A-2026-07-07 and A-2026-07-09 → RESOLVED
  2026-07-20 in the findings tables; §6 backlog rows 3 and 11 struck
  through as DONE (D9/D10)
- plans/audit-2026-07-19-decisions.md: D9/D10 status → Implemented
- plans/channel-visibility-unification.md, v2-dispatch-migration.md:
  status → implemented; v2 note records the applier-trigger shape the
  voice handlers actually landed with
- architecture/server.md: WS box "V1+V2 dispatch" → "typed command
  dispatch"
- architecture/websocket.md: intro + §D4c redrawn as the single typed
  path (no V1 fallback)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 11:44:32 +02:00
J3vbandClaude Fable 5 5c91d2492b feat(ws): finish the V2 dispatch migration and delete V1
Port the last three V1 message types to typed V2 handlers, then remove
the V1 registry and fallback path so handleMessage has a single dispatch
generation (audit A-2026-07-09 / backlog item 11). Server-internal only —
the envelope wire format is unchanged, no client/protocol edits.

- chat_command: ChatCommandCmd + constructor (empty/args guards) and a
  V2 handler returning an ephemeral Reply plus a channel-routed
  PluginBroadcastEvent gated by MessageService.CanPost; PluginDeps reads
  the registry live (wired post-construction)
- voice_join/voice_leave: V2 handlers gate parse/rate-limit and hand off
  to the still-hub-internal handleVoiceJoin/handleVoiceLeave routines via
  new Result.JoinVoice / Result.LeaveVoice appliers (those routines are
  also called un-throttled on disconnect and channel switch)
- delete HandlerRegistry.handlers/Register/Dispatch/RegisteredTypes/
  IsRegisteredV1/hasV2, the MessageHandler type, and the V1-shadowing
  guard; NewHub registers only V2
- tests: per-handler V2 tests + a parity guard asserting every command
  constructor has a V2 handler and vice versa (locks the migration shut)

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

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

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 11:28:03 +02:00
J3vbandClaude Fable 5 215a194325 docs: design notes for audit backlog 3 (channel-visibility) and 11 (V2 dispatch)
Greenlight backlog items 3 (A-2026-07-07) and 11 (A-2026-07-09) for
implementation. One design note each in docs/plans/ matching the existing
per-decision format (problem, approach, files touched, test plan, non-goals),
plus D9/D10 rows in the maintainer decisions doc dated 2026-07-20.

Design-only step of the audit-backlog PR; no code changes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 11:08:52 +02:00
J3vbandClaude Fable 5 4e27f95333 docs(audit): record D9 LogAudit best-effort error-handling policy
Add decision D9 (dated 2026-07-20) to the audit decisions doc capturing the
maintainer-approved policy: best-effort audit writes, never silently
discarded, routed through db.WriteAudit. Mark carried-over finding #10 in
docs/audit-2026-07-19.md as RESOLVED with the helper adoption.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 10:48:13 +02:00
J3vbandClaude Fable 5 b60bc8d04b feat(audit): route every LogAudit call through a best-effort WriteAudit helper
Audit writes stay best-effort — a LogAudit failure must never fail or abort
the request — but a failed write must no longer be silently discarded. Add
db.WriteAudit(auditor, actor, action, targetType, targetID, detail), which
logs a failed write with actor/action/target context (never the detail
string, which may be sensitive) and never propagates the error.

The Auditor interface is satisfied structurally by both *db.DB and the
service-layer Store, so api/admin/ws/service all reach the helper without an
import cycle. Converts all ~26 call sites from `_ = LogAudit(...)` (and the
two backup handlers' inline `if err` blocks) to db.WriteAudit. Pinned by
db/audit_test.go: failure logged and not propagated, success logs nothing,
detail never leaks.

Resolves the repo-wide LogAudit policy question flagged by the D8 note.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 10:48:05 +02:00
J3vbandClaude Fable 5 ca91d28561 feat(updater): surface download progress in the update banner
The Rust download callback was a no-op, so "Downloading update…" looked hung
for large binaries (settings-and-admin.md §5). download_and_install_update now
accumulates received bytes and emits an `update-progress` event
({ received, total }) to the webview. downloadAndInstallUpdate(serverUrl,
onProgress) listens for it and UpdateNotifier renders a percentage when the
total is known, falling back to bytes (MB) until Content-Length arrives.

Rust change is minimal and CI-gated only (not built locally per policy). Adds
TS tests for the formatter and the banner wiring.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 10:19:42 +02:00
J3vbandClaude Fable 5 a546af2d4f feat(dm): gate DM composer on block state with spec reasons
Wire DM block state into the existing disabled-with-reason composer mode
(channels-members-dms.md §3.2). A new blocks.store holds two directions:

- blockedByMe (from GET /blocks on every ready) -> "You've blocked this
  user. Unblock to send messages."
- blockedByThem (inferred from a refused DM send: ErrBlocked -> FORBIDDEN,
  cleared on the next ready) -> neutral "You can't message this user right
  now.", never revealing the block explicitly.

ChannelController reads dmComposerBlockReason(recipientId) and subscribes to
blocks.store so an unblock (shrunken GET /blocks) re-enables the composer
live; blockedByMe takes precedence when both apply. Adds api.listBlocks(),
threads an optional api into wireDispatcher, and covers both directions plus
un-gating in blocks-store / channel-controller / dispatcher tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 10:19:31 +02:00
J3vbandClaude Fable 5 b8ad9db489 test(voice): pin the reconnecting status write on unexpected room drop
setVoiceStatus("reconnecting") is written from the setReconnectAc callback fired
via roomEventHandlers.handleDisconnected on an unexpected room drop — the primary
reconnecting write, previously untested (the reconnect suite only asserted the
subsequent "connected"). Add a test that connects, captures the Disconnected
handler, fires it with a non-CLIENT_INITIATED reason, and asserts both the
"reconnecting" session state and the setVoiceStatus("reconnecting") write.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 09:56:43 +02:00
J3vbandClaude Fable 5 544360642e fix(voice): freeze sidebar voice join/leave row on WS reconnect
The WS-reconnect control freeze (spec item 6) only reached the VoiceWidget's
in-call controls. The actual join affordance — clicking a voice-channel row in
ChannelSidebar — stayed a plain clickable div with no disabled state, so a click
while the socket was reconnecting/disconnected was a silent no-op (only the
VoiceCallbacks socketLive() backstop stopped the send).

Gate renderVoiceChannelItem on ui.store.connectionStatus using the same
disabled-with-reason pattern as VoiceWidget: apply a .disabled class,
aria-disabled, and a "Reconnecting…" / "Not connected" title while not connected,
and make the click a no-op. Subscribe the sidebar to connectionStatus so the row
freezes/unfreezes reactively (mirrors the existing collapsedCategories selector).

Docs: README.md §3 callout now notes the sidebar join affordance takes the
disabled-with-reason state too; voice-and-e2ee.md lists ChannelSidebar.ts as a
source of truth for the freeze.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 09:53:48 +02:00
J3vbandClaude Fable 5 23bf34bbae docs(ux): close the voice/E2EE status gap callouts
Mark the voice-and-e2ee.md §1 (store-backed voiceStatus) and §2 (visible E2EE
securing/secured indicator) gaps as implemented, noting the client runs the ECDH
key exchange before the media connect. Close the remaining voice column of the
README.md §3 connection-status table: voice controls now freeze on WS reconnect.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 09:31:40 +02:00
J3vbandClaude Fable 5 ff55da7161 test(voice): pin voiceStatus transitions, widget indicators, and control freeze
- livekit-session: assert joining→securing→connected on join, idle on leave,
  connected after auto-reconnect
- voice-widget: assert each status label + secured badge visibility, and controls
  disabled with reason while the socket is down, re-enabled on reconnect
- voice-callbacks: assert join/leave/disconnect do not send over a down socket
- voice.store: assert join seeds joining, leave resets idle, setter writes status
- thread voiceStatus through existing full-state fixtures; keep an active-call
  socket live in widget fixtures so control-click tests still operate enabled

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 09:31:27 +02:00
J3vbandClaude Fable 5 827eea77ed feat(voice): surface voice-session + E2EE status and freeze controls on WS reconnect
Add a store-backed voice.store.voiceStatus (idle | joining | securing |
connected | reconnecting), written as the single source of truth from
livekitSession at each lifecycle transition: joining at connectAndSetup start,
securing when ECDH key exchange begins, connected on the connected transition
(initial join and auto-reconnect), reconnecting when the room drops, idle on
leaveVoice. joinVoiceChannel seeds joining optimistically on click.

VoiceWidget renders the phase in its header: 'Connecting…' / 'Securing…' (amber)
and a persistent '🔒 Secured' badge once the room key is ready, replacing the
log-line-only E2EE feedback. While ui.store.connectionStatus is not 'connected',
the widget disables its controls with a 'Reconnecting…' / 'Not connected' reason,
and the VoiceCallbacks join/leave paths refuse to send over a down socket.
LiveKit's own reconnection machinery is untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 09:31:17 +02:00
J3vbandClaude Fable 5 6e884d871d docs: mark client unit suite green, not KNOWN RED
The full vitest suite passes (verified locally, 3252/3252), but both CLAUDE.md
files, ci.yml's client-tests comment, and audit item A-2026-07-04 still called
it "KNOWN RED" pending a P2 triage. Update all four to say the suite is green
and must stay green, and close A-2026-07-04 (2026-07-20). Flipping client-tests
to blocking + the nightly Playwright gate remain tracked as backlog #10.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 08:39:24 +02:00
J3vbandClaude Fable 5 80903a4ccb fix(client): revoke server session on user-initiated logout
api.logout() (POST /auth/logout) existed but was never called, leaving the
bearer token valid server-side after a client-local logout. Add a small
logout() helper that fires the revocation best-effort — fire-and-forget with
its rejection swallowed — then runs clearAuth() synchronously, so a slow,
offline, or rejecting server can never block or delay the local logout. Wire
it into the settings Log Out button. Tests pin both paths: logout is called,
and local logout still completes when the request rejects or never settles.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 08:39:12 +02:00
J3vbandClaude Fable 5 93850689a2 fix(client): unify role lookups onto the dispatcher-updated store
SidebarMemberSection read role name->id from a parallel roles.store that
nothing ever wrote to — only channels.store.setRoles is updated by the
dispatcher on `ready`. Repoint the reader at channels.store and delete the
dead roles.store (its setRoles/getRoleIdByName coverage already lives in
channels.store.test.ts). Adds a regression test pinning that the member UI
resolves role ids from the store the dispatcher writes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 08:39:01 +02:00
J3vb 5de92b9510 Merge pull request #1191 from J3vb/claude/blueprints-architectural-audit-k927qb
feat(client): connection-status store + no-silent-failure batch
2026-07-19 21:43:53 +02:00
Claude 45e51cca1c fix(client): review follow-ups for the connection-status batch
Fixes from an adversarial review of the previous commit:

- MainPage banner: sync the banner with the current store status at mount.
  The selector subscription baselines on the current value and only fires
  on change, so a MainPage mounted mid-outage (status already
  "reconnecting") would never show the banner — the whole retry cycle maps
  to the same 3-state value. The status→banner dispatch is extracted to
  ServerBanner.applyConnectionStatus and unit-tested.
- History-fetch failure is no longer silent when the channel already has
  rows (live broadcasts / optimistic sends): the inline error region only
  renders in an empty channel, so loadMessages now also raises a toast in
  that case.
- Composer disable reason distinguishes "Reconnecting…" from
  "Not connected" per the spec §3 table (it previously showed
  "Reconnecting…" while disconnected, contradicting the banner).
- The single-writer wiring is extracted to
  dispatcher.wireConnectionStatus(ws) and pinned by a test (it was
  previously an untestable main.ts module-scope line — deleting it would
  have failed zero tests).
- Docs honesty: messaging.md's transport-drop diagram arm now shows both
  codes (channel full → NETWORK, closed/not-open → OFFLINE) instead of
  claiming NETWORK for both; README §3's callout now explicitly lists the
  voice column ("frozen" during reconnect) as a remaining gap instead of
  implying the section is fully closed; the composer table documents both
  offline reasons.
- New pinning tests: SidebarArea passes ws to UserBar (the production-bug
  fix was previously unasserted), ServerBanner.showDisconnected,
  applyConnectionStatus mapping, ChannelController onRetryLoad /
  onRetry-resend / onDeleteDraft, composer reason per status, and the
  history-failure toast fallback.

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

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 19:42:36 +00: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 7d0c7a61e0 Merge pull request #1190 from J3vb/claude/blueprints-architectural-audit-k927qb
feat(client): optimistic message send + composer permission gating
2026-07-19 20:31:46 +02:00
J3vb f8e5e538b4 Merge pull request #1188 from J3vb/claude/repo-setup-tooling-axqgp1
chore: Claude Code project config, git hooks, and session setup
2026-07-19 20:27:38 +02:00
Claude e0ab0744ee feat(client): optimistic message send + composer permission gating
Implements the two highest-impact gaps from the client UX spec.

Optimistic send:
- messages.store gains addOptimisticMessage / markSendFailed /
  removeOptimistic, and confirmSend now stamps the real id + "sent" on
  the ack. addMessage reconciles the broadcast by real id (idempotent,
  replay-safe) with a defensive author match, so an echo never
  duplicates. Message gains status/correlationId/errorCode.
- ChannelController.performSend renders a pending row immediately and
  supports retry / delete-draft (retry preserves attachments).
- MessageList renders pending (dimmed) and failed (reason + Retry /
  Delete) rows; the hover action bar is limited to confirmed rows.
- Failures are precise: the server echoes the request id on error
  replies (buildErrorMsgWithID), so the dispatcher maps SLOW_MODE /
  FORBIDDEN / RATE_LIMITED / BAD_REQUEST to the exact row instead of
  dropping the code. An offline send is shown failed, not silently lost.

Composer permission + connection gating:
- The server computes an authoritative per-channel can_send in the ready
  payload (channelCanSend mirrors MessageService.checkSendPermission:
  READ|SEND, MANAGE_MESSAGES for announcement, admin bypass, channel
  overrides). channels.store carries it as Channel.canSend.
- MessageInput gains a disabled-with-reason mode; ChannelController
  derives the reason from can_send + channel type + connection status and
  disables the composer reactively (announcement read-only, no-permission,
  reconnecting) rather than accepting a click and failing. Older servers
  that omit can_send default permissive.

Docs: the corresponding "Current gap" callouts in docs/architecture/ux
are updated to reflect the implementation.

Verified: full server suite + client tsc + 3204 unit tests + lint + gofmt.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UA17KPvqGBX3XbXYnMf1rA
2026-07-19 17:47:39 +00:00
J3vb 3e5ad35a99 Merge pull request #1189 from J3vb/claude/blueprints-architectural-audit-k927qb
docs: client UX specification (target-state flows + per-view state maps)
2026-07-19 19:09:34 +02:00
Claude 0c86264b8c docs: add client UX specification (target-state flows + per-view states)
Adds docs/architecture/ux/ — a prescriptive (to-be) behavior spec for the
Tauri client, complementing the as-built module map in
docs/architecture/client.md. Covers every view and how it should react to
server events, permission state, and failure:

  - README.md            — view-state vocabulary, feedback primitives,
                           connection-status contract, the global
                           event->reaction map, and the error/permission
                           reaction matrix
  - connection-and-auth  — boot, profiles/health, login, TOTP,
                           register-by-invite, connected handshake,
                           reconnect, cert-TOFU trust
  - messaging            — composer permission/connection gating, optimistic
                           send lifecycle, edit/delete, reactions,
                           attachments, pins, search, read/unread, slow-mode
  - channels-members-dms — channel list/switch/categories, member list +
                           presence + typing, DM open/close, blocking
  - voice-and-e2ee       — join/leave, mute/deafen/camera/screenshare, PTT,
                           active-speaker, and the E2EE securing/secured
                           indicators
  - settings-and-admin   — settings tabs, profile/password/2FA/delete,
                           theming, inline admin (ban/kick/roles, channel
                           CRUD, invites), updater

Each flow carries dated "Current gap" callouts where today's code diverges
from the target (grounded in file:line references), so the set doubles as a
UX improvement backlog. Notable gaps captured: non-optimistic send with a
dead pending-send path, no composer read-only/permission gating (incl.
announcement channels), silently-dropped WS error codes, no E2EE "securing"
indicator, no updater download progress, client-local logout that never
revokes the server session, and a duplicated role store.

All 12 Mermaid diagrams validated; intra-repo links checked. Indexed from
docs/architecture/README.md and the top-level Docs Index.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UA17KPvqGBX3XbXYnMf1rA
2026-07-19 16:58:43 +00:00
Claude fc0f86118c chore: add Claude Code project config, git hooks, and session setup
- CLAUDE.md (root + Server + client) documenting commands, generated-code
  rules, and the known-red client unit suite policy
- .claude/skills: ci-check (local CI mirror), protocol-change, db-change
  workflows for the sqlc/protocol codegen invariants CI enforces
- .githooks pre-commit/pre-push mirroring CI's fast gates (gofmt, go vet,
  oxlint, prettier, tsc, tag-variant builds, sqlc/protocol staleness),
  enabled via 'npm run hooks:install'
- .claude SessionStart hook installing client npm deps and warming the Go
  module cache so remote sessions can run tests/linters immediately
- .mcp.json with Playwright and Context7 MCP servers
- .gitignore: commit shared Claude config; keep local-only overrides ignored

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BDcVJqGjHcJVLoV4x1HFjW
2026-07-19 16:54:13 +00:00
J3vb 719c910330 Merge pull request #1187 from J3vb/claude/blueprints-architectural-audit-k927qb
Adopt sqlc as the query layer (D2) — revive dead dbgen across all domains
2026-07-19 18:39:18 +02:00
Claude 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
2026-07-19 16:33:58 +00:00
Claude 071426c0d8 feat(server,client): announcement channels (D1, closes A-2026-07-01)
Make 'announcement' a real channel type, resolving the contradiction where
it was documented and offered by the admin API but hard-rejected by the
migration-013 DB triggers.

Model: announcement channels are readable like text channels (same
READ_MESSAGES visibility), but posting is restricted to users with
MANAGE_MESSAGES — no new permission bit, migration, or client permission
plumbing needed.

Server:
- migrations/016: recreate the channel-type triggers to allow
  text/voice/announcement/dm.
- service/message.go: checkSendPermission now takes the channel type and
  rejects posts to announcement channels from users lacking MANAGE_MESSAGES
  (SendMessage + CanPost paths). Added a service test.
- Unread counts: ready-payload builder (ws/serve.go) and
  GetChannelUnreadCounts (db) now include announcement channels alongside
  text, so they track unread/last-message like text channels.

Client:
- ChannelSidebar renders announcement channels with a megaphone icon
  (added to the icon set) instead of the '#' text prefix; they otherwise
  behave like text channels (already typed in ChannelType).

Specs + trackers (api.md, protocol.md, schema.md incl. migration 016,
architecture/data-model.md, audit A-2026-07-01, decisions D1) updated.

Verified: go build ./...; go test ./service ./db ./ws ./api ./admin;
sqlc-verify; client tsc + oxlint + prettier clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UA17KPvqGBX3XbXYnMf1rA
2026-07-19 16:00:18 +00:00
Claude f66d6c5274 refactor(server/db): delegate messages + reactions to dbgen; finalize D2
messages/reactions: CreateMessage, GetMessage (messageFromGen mapper),
EditMessage (EditMessageContent), DeleteMessage (SoftDeleteMessage),
AddReaction, RemoveReaction, GetReactions (GetReactionCounts),
SetMessagePinned, UpdateReadState. Retired the obsolete scanMessage.

Kept raw by design (no clean sqlc mapping): FTS search, cursor-paginated
GetMessages/GetMessagesForAPI/GetPinnedMessages, getReactionsBatch,
GetChannelUnreadCounts, GetLatestMessageID (interface{} MAX result).

D2 status: 97 db.DB methods now delegate to dbgen across every domain;
43 raw d.sqlDB calls remain by design (db.go passthroughs, migrate.go,
variable-length IN(), FTS, multi-statement transactions, PRAGMA/VACUUM).
sqlc is no longer dead code — audit A-2026-07-05 resolved. Full rationale
+ the kept-raw list in docs/plans/sqlc-adoption.md.

Verified: go build ./...; go test ./db ./service ./ws ./auth; sqlc-verify;
gofmt + go vet clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UA17KPvqGBX3XbXYnMf1rA
2026-07-19 15:48:39 +00:00
Claude c7e6702c8c refactor(server/db): delegate voice, dm, channels, admin to dbgen (D2)
voice: all reads (GetVoiceState, GetChannelVoiceStates, GetAllVoiceStates)
and writes (join/leave/mute/deafen/camera/screenshare/clear, capacity +
camera-limit atomic guards); CountChannelVoiceUsers stays raw (no query).
dm: OpenDM, CloseDM, IsDMParticipant, GetDMParticipantIDs; GetOrCreateDMChannel
(serializable tx), GetUserDMChannels (aggregate), GetDMRecipient stay raw.
channels: List/Get (shared channelFromFields mapper), Create/Update/Delete,
slow-mode/max-users setters, permission overrides get/list-for-role/upsert/
delete; ListChannelRoleOverrides + GetChannelTypes (variable IN) stay raw.
admin: UserCount, GetServerStats counts (PRAGMA stays raw), ListAllUsers,
UpdateUserRole, ForceLogoutUser, GetUserSessions, AdminUpdateChannel,
AdminDeleteChannel, LogAudit, GetAuditLog, GetSetting, SetSetting,
GetAllSettings, CountUsersWithoutTOTP; AdminCreateChannel + Backup stay raw.

Added b2i64 and strToNullPtr mapper helpers; retired the obsolete
scanChannel/nullableString. Behavior and public signatures unchanged.

Verified: go build ./...; go test ./db ./service ./permissions ./admin;
sqlc-verify.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UA17KPvqGBX3XbXYnMf1rA
2026-07-19 15:43:46 +00:00
Claude d769b8bdb6 refactor(server/db): delegate invites + attachments to dbgen (D2)
invites: CreateInvite, GetInvite, UseInviteAtomic, RevokeInvite,
ListInvites. attachments: CreateAttachment, GetAttachmentByID,
GetAttachmentWithChannel, DeleteOrphanedAttachments. Added ptrI64toI /
ptrItoI64 helpers for the *int64<->*int narrowing (invite max_uses,
attachment width/height). LinkAttachmentsToMessage and
GetAttachmentsByMessageIDs keep raw SQL (variable-length IN() lists sqlc
can't express). Behavior and signatures unchanged.

Verified: go build ./...; go test ./db (Invite, Attachment).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UA17KPvqGBX3XbXYnMf1rA
2026-07-19 15:36:59 +00:00
Claude e46bd0e015 refactor(server/db): delegate users, sessions, profile to dbgen (D2)
Convert the auth_queries.go user + session reads/writes and
profile_queries.go to the sqlc-generated layer, adding shared
userFromGen/sessionFromGen mappers (db/mappers.go) for the
int64/*string -> int/bool/string domain-model narrowing.

Delegated: GetUserByID, GetUserByUsername, UpdateUserStatus,
UpdateUserTOTPSecret, ResetAllUserStatuses, BanUser, UnbanUser,
ListMembers, CreateSession (EvictOldestSessions + InsertSession),
GetSessionByTokenHash, GetSessionWithBanStatus, DeleteSession,
DeleteOtherSessions, DeleteExpiredSessions, TouchSession,
UpdateUserProfile, UpdateUserPassword, ListUserSessions,
DeleteSessionByID (query changed to :execresult so the RowsAffected
ErrNotFound check is preserved). Behavior and public signatures unchanged.

Verified: go build ./...; go test ./db ./service ./auth; sqlc-verify.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UA17KPvqGBX3XbXYnMf1rA
2026-07-19 15:34:27 +00:00
Claude 44323373e3 refactor(server/db): adopt sqlc as the query layer — phase 1 (D2)
Wire the sqlc-generated dbgen package into db.DB so it stops being dead
code (audit A-2026-07-05) and becomes the real, CI-verified query layer.

db.DB now holds a *dbgen.Queries (initialized in Open via dbgen.New).
Query method bodies delegate to it; sqlc owns the SQL text and parameter
binding (make sqlc-verify), while db keeps its stable public API and
domain model types so no caller in api/admin/ws/service changes. The
migration is incremental — a method either delegates to d.q.* or still
runs raw SQL — so both layers are correct during the transition.

Converted domains (now load-bearing through sqlc):
- blocks: BlockUser, UnblockUser, IsBlocked, IsEitherBlocked,
  ListBlockedUsers (added the query to blocks.sql + regenerated).
  Empty ListBlockedUsers now returns []int64{} instead of nil, matching
  the MemStore backend — a latent inconsistency fixed, not a regression.
- lockouts: UpsertLockout, LoadActiveLockouts, CleanupExpiredLockouts,
  DeleteLockout (RFC3339 time formatting/parsing kept in the wrappers).
- roles: GetRoleByID, ListRoles, GetRoleForUser via a shared roleFromGen
  mapper (int64 position/is_default -> int/bool). GetUserWithRole stays
  raw for now.

Remaining domains stay on raw SQL and are tracked in
docs/plans/sqlc-adoption.md; store/ event+plugin SQL is intentionally
excluded (that layer is removed in D3). Decisions doc + audit closure
updated (A-2026-07-05 -> in progress).

Verified: go build ./...; go test -race ./db ./service ./auth ./ws (api
green non-race, race run matches CI's -timeout 20m); make sqlc-verify and
protocol-verify pass with the regenerated output committed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UA17KPvqGBX3XbXYnMf1rA
2026-07-19 15:01:54 +00:00
J3vb 752c8f2bcc Merge pull request #1186 from J3vb/claude/blueprints-architectural-audit-k927qb
Client HTTP TOFU proxy: cert-pin the REST path (closes A-2026-07-02)
2026-07-19 16:28:16 +02:00
Claude dab4d73e09 feat(client): TOFU HTTP proxy for REST — close audit A-2026-07-02 (D5)
The REST path previously used tauri-plugin-http with
danger.acceptInvalidCerts, so it accepted ANY certificate while the WS
and LiveKit paths were TOFU-pinned in Rust — and the bearer token rides
every REST request. This routes REST through a new Rust loopback
TCP->TLS proxy that pins the server certificate to the same
trust-on-first-use fingerprint as the WS proxy.

Rust (src-tauri):
- New http_proxy.rs: per-host loopback tunnels (HttpProxyState map);
  per-connection TOFU via CaptureVerifier + tofu_check, sharing
  ws_proxy's cert store (cert_store_key) and emitting the same
  cert-tofu events (first-use banner / mismatch modal). First request's
  Host is rewritten and Connection: close injected so one request rides
  each connection. Mismatch returns a clean 502 to the loopback fetch.
- Register HttpProxyState + start_http_proxy/stop_http_proxy in lib.rs.
- Drop the dangerous-settings feature from tauri-plugin-http.

TypeScript (src):
- New lib/httpProxy.ts: ensureHttpProxy(host) (per-host cache +
  concurrent-start dedup) / stopHttpProxy(host).
- api.ts, profiles.ts (health), attachments.ts (image + download) resolve
  server URLs to http://127.0.0.1:{port}; remove the allowSelfSigned
  config field and every acceptInvalidCerts block. External hosts (CDNs,
  OG previews, YouTube) keep normal TLS validation.
- main.ts constructs the API client without allowSelfSigned.
- capabilities/default.json: allow http://127.0.0.1:* fetch scope.

Tests:
- New tests/unit/http-proxy.test.ts (cache, dedup, stop/restart).
- api.test.ts and attachments-render.test.ts: mock httpProxy, replace the
  acceptInvalidCerts assertions with proxy-origin assertions.

Verified: tsc --noEmit clean; new + affected vitest suites green
(176 tests); the http_proxy pure logic (host validation, header rewrite)
passes as standalone Rust unit tests; oxlint/eslint counts unchanged
from HEAD; prettier clean. The full Tauri build (cargo) requires GUI
system libs not present in this environment and runs on CI/real runners.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UA17KPvqGBX3XbXYnMf1rA
2026-07-19 14:26:25 +00:00
J3vb 4bb388c738 Merge pull request #1185 from J3vb/claude/blueprints-architectural-audit-k927qb
Audit follow-through: decisions, quick wins, protocol codegen, spec refresh, Solid removal
2026-07-19 16:07:38 +02:00
Claude 8c768bf4c6 docs: add design note for the client HTTP TOFU proxy (D5)
Pins the implementation approach for closing audit finding A-2026-07-02:
a loopback TCP-to-TLS tunnel reusing the livekit_proxy pattern and the
shared per-host fingerprint store, with ws_proxy-equivalent TOFU
first-trust/mismatch flows (required because the first TLS contact with
a server is the login HTTP request), per-host tunnel lifecycle for
multi-profile health polling, and removal of the acceptInvalidCerts
path plus the dangerous-settings feature flag as the ratchet.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UA17KPvqGBX3XbXYnMf1rA
2026-07-19 13:58:58 +00:00
Claude bf508c9e6b chore(client): remove abandoned SolidJS beachhead (D6)
The Solid.js migration was abandoned (per CHANGELOG); the 154-LOC
beachhead and its scaffolding remained in-tree, leaving two UI paradigms
for contributors. Removed:

- src/components/solid/ (Badge, ChannelListItem, PluginContainer — none
  imported by production code)
- src/lib/solidMount.ts and src/lib/solidAdapter.ts
- tests/setup-solid.ts and tests/setup-solid.test.tsx
- vite-plugin-solid from vite.config.ts and vitest.config.ts (and the
  now-unneeded tsx test include + setupFiles)
- jsx/jsxImportSource from tsconfig.json
- solid-js, @solidjs/testing-library, vite-plugin-solid from package.json

docs/client-architecture.md (which described the SolidJS design) is
retired to a pointer at docs/architecture/client.md; README links
updated. Audit A-2026-07-12 and decision D6 marked closed.

Verified: tsc --noEmit clean (previous 3 test-file errors were caused by
the Solid jsx config and are gone); oxlint/eslint error counts identical
to HEAD (pre-existing); vitest runner healthy on a sample suite.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UA17KPvqGBX3XbXYnMf1rA
2026-07-19 13:57:28 +00:00
Claude ca3b0fee3e docs: refresh api.md, protocol.md, schema.md against current code (D7)
One-PR spec refresh per decision D7, using the 2026-07-19 audit's
conformance matrix as the checklist:

api.md
- Remove the deleted version field from /health and /api/v1/info
  (anti-fingerprinting C-2) and document the removal.
- Document the profile surface (PATCH /users/me, PUT /users/me/password,
  GET/DELETE /users/me/sessions), the user-blocks surface
  (GET/PUT/DELETE /api/v1/blocks), and the plugin admin surface
  (/api/v1/admin/plugins).
- Correct GET /api/v1/files/{id}: auth is required and caching is
  'private, no-cache' (was documented as public/immutable, unauthenticated).
- Add search (30/min) and upload (10/min) rate limits; note announcement
  channel type as planned-only.

protocol.md
- Document auth_ok.replay_source and the 3-tier reconnect replay
  (ring buffer -> events table -> full resync) with the visibility
  watermark.
- Add the Voice End-to-End Encryption section (voice_e2ee_announce/offer
  in both directions, key-holder semantics, rate limits) and
  voice_token.is_key_holder.
- Document user_update; mark voice_speakers and member_leave as defined
  but not currently emitted; extend voice_config fields.
- Update the reference tables (19 client->server / 30 server->client)
  and point them at protocol-schema.json as the generated inventory.

schema.md
- Correct the migration history to the real 001-015 numbering.
- Document the previously missing tables: login_attempts, settings,
  emoji, sounds (dead schema), rate_lockouts, user_blocks, events,
  plugins, plugin_kv; add attachments.uploader_id and new indexes.
- Fix the channel-type list to text/voice/dm (013 triggers) and correct
  the permission formula to (base & ~deny) | allow to match
  permissions.EffectivePerms.
- Note the sqlc/dbgen layer and link the architecture data-model doc.

Also correct the audit_log_v6 claim (transient rename inside migration
003, not a coexisting table) in the audit and data-model blueprint, and
update the audit/decisions trackers (A-2026-07-03 closed).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UA17KPvqGBX3XbXYnMf1rA
2026-07-19 13:51:44 +00:00