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>
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>
- 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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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
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
- 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
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
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
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
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
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
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
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
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
Protocol codegen (decision D4, audit A-2026-07-08):
- Add docs/protocol-schema.json as the real single source of truth for
WS message-type constants, making the long-standing 'generated from'
comment in both constant files true.
- Add Server/scripts/genprotocol, a generator emitting both
Server/ws/message_types.go and Client .../lib/protocolTypes.ts
(constants byte-for-byte value-identical to before; only headers,
ordering alignment, and provenance comments changed).
- Add make protocol-generate / protocol-verify and wire protocol-verify
into CI next to sqlc-verify.
Quick wins (decision D8):
- admin: log LogAudit write failures in the backup handlers instead of
discarding them (prior audit #10).
- api: fix self-contradictory upload Cache-Control to 'private,
no-cache' per remediation plan W3-4; drop the now-unused
fileCacheMaxAgeSeconds constant; update test.
- ws: route the hub settings cache through db.GetSetting instead of
inline SQL.
- ws: fix a latent data race — main.go wires SetEventPersister and
SetEventStore after NewRouter has already started the hub Run loop,
which reads those fields on the broadcast/replay paths. They (and
pluginSink, which one test sets post-Run) are now atomic pointers;
the remaining pre-Run-only setters reject late calls with an error
log instead of racing silently.
Update the audit closure table and decisions doc statuses accordingly.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UA17KPvqGBX3XbXYnMf1rA
Add docs/plans/audit-2026-07-19-decisions.md capturing the eight decisions
taken on the audit's open questions (announcement channels: implement;
data layer: adopt sqlc + remove store/; protocol constants: real codegen;
client HTTP TOFU pinning: next security work; Solid beachhead: delete;
specs: one refresh PR after codegen) with sequencing, and mark the greenlit
items (protocol codegen + quick-wins batch).
Update the closure table in docs/audit-2026-07-19.md from OPEN to DECIDED
for the seven findings these decisions cover.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UA17KPvqGBX3XbXYnMf1rA
Add docs/architecture/ — a curated blueprint set with 10 Mermaid diagrams
covering system context, deployment topology, server package map, REST
request lifecycle, WebSocket auth/replay/dispatch, the full data model
(migrations 001-015), voice/E2EE flow, and the client module map.
Add docs/audit-2026-07-19.md — successor to audit-2026-04-07.md:
re-verifies carried-over findings, catalogues spec-vs-code drift in
api.md/protocol.md/schema.md (incl. the announcement channel-type
contradiction and the undocumented voice-E2EE protocol surface), records
server/client/CI findings with file:line evidence, and closes with a
12-item prioritized improvement backlog.
Link both from the README docs index.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UA17KPvqGBX3XbXYnMf1rA
Servers reachable via both a LAN IP and a public IP could only serve voice
on one of them: config.yaml accepts a single voice.node_ip and OwnCord
regenerates data/livekit.yaml on every start, discarding manual edits.
LiveKit has no multi-IP list, but it does support advertising internal host
candidates alongside the external mapping.
- New voice.advertise_internal_ip (OWNCORD_VOICE_ADVERTISE_INTERNAL_IP):
emits rtc.advertise_internal_ip: true so LAN clients get a reachable
candidate while remote clients keep using node_ip.
- livekit.yaml escape hatch: if the file exists without the auto-generated
marker header, OwnCord leaves it untouched, giving operators access to
every LiveKit option (ips.includes, interfaces, stun_servers, ...). The
generated header documents how to take ownership.
Closes#111
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwtnpHAoSFr1ZibQgQkNQK
On Wayland sessions (notably GNOME + NVIDIA), WebKitGTK's DMABUF renderer
can crash or render a blank window, so the client failed to start. Set
WEBKIT_DISABLE_DMABUF_RENDERER=1 on Wayland unless the user has already set
it themselves.
device_query's global key state needs an X11/XWayland display and panicked
per poll on pure-Wayland setups. Use DeviceState::checked_new() so push-to-
talk degrades to inactive with a single warning instead.
Fixes#96
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwtnpHAoSFr1ZibQgQkNQK
- Replace static badge block with CI, release, status, Go, Tauri,
platforms, and license badges (release badge tracks the public
OwnCord-releases repo)
- Update platform support table for v1.1.0-alpha.2 assets (Linux x64
server, Linux x64/ARM64 client)
- Stamp build examples with the current version
- Point contributing docs at main now that dev is pruned
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- gitignore .serena/ and Client/tauri-client/.env; untrack the .env
(the KLIPY key it held is treated as burned; rotation + server-side
proxy tracked for P3)
- CI: verify generated sqlc output (make sqlc-verify) on the ubuntu leg
- CHANGELOG: honest reset narrative (v1.1.0-alpha series), remove
references to deleted roadmap files
- delete stale docs/phase-a-status.md; fix dangling ref in
docs/plans/slash-commands.md
- add root SECURITY.md (GitHub-surfaced policy; reporting works while
the source repo is private)
- docs/audit-2026-04-07.md: add maintained finding-closure table
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Publish every release to the public OwnCord-releases repo with a full
source snapshot (AGPL section 6 for binary recipients), and make the
updater's repo coordinates configurable (github.owner/github.repo),
defaulting to OwnCord-releases. Both the server self-update and the
/client-update chain follow the new default, so deployed servers keep
updating after the source repo goes private.
The publish step fails closed: a private source repo with no
RELEASES_REPO_TOKEN aborts the release instead of silently shipping
binaries with no public source or update feed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds docs/plans/security-hardening-remediation.md capturing the code-review
findings against fix/security-hardening-review and sequencing the fixes into
three waves (availability/backend-breaking, behavioral regressions, cleanup).
Each item names root cause, right-altitude fix, files, and verification.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
First of two phase-D parity plans. Grounded in the existing V2 command
dispatcher (Server/ws/command.go) and the dormant
plugin.Registry.DispatchCommand + host_commands.go — this is not a
green-field design, it's a wiring plan for code that already exists.
Covers:
- Wire format: command_invoke, command_autocomplete, command_reply,
command_autocomplete_result
- Manifest extension: commands[] with option types, default_member_permissions,
contexts, autocomplete flag
- Schema: migrations/016_plugin_commands.sql with a unique name index
so two plugins can't both own /ban
- Code surface: enumerated file-by-file touch list
- Permission model: server-enforces default_member_permissions BEFORE
the plugin is invoked, plugins never get to gate their own commands
- Built-in commands: /me + /shrug ship in-tree as reference handlers
- Concurrency: 3s deadline via context.WithTimeout passed to DispatchCommand
- Failure modes & UX: 6-row table from "unknown command" through panic
auto-disable
- Testing: unit + integration + contract round trip
- Telemetry: 3 new counters + OTel span
- 4-stage rollout, each step independently shippable
- Open questions: bot identity for broadcasts, component v2 reservation,
cross-plugin imports, DM-context handling
Plan #8 (E2EE DMs + DAVE voice) and PHASE_D_PARITY_TODO.md items 2-7
to follow in a subsequent commit.
https://claude.ai/code/session_01UsBsQW2YiA2usk9pnJjAWk
Resolves the modify/delete conflict with dev (which removed
phase-a-foundation.md in a1e8970). The Implementation Status and
Actionable TODOs sections are preserved under docs/ alongside the
other project docs, matching the existing docs/*.md convention.
The original phase-a-foundation.md design brief is gone per dev's
intent; only the post-implementation status and follow-up checklist
survive.
- deployment.md: new Docker section with quick-start, config.yaml notes,
data persistence, upgrade, and LiveKit reference
- livekit-setup.md: new Docker section with .env / livekit.yaml setup,
node_ip explanation, and firewall table; companion process section
retitled for clarity