refactor: cleanup stories #87-92 (perf, idiomatic, infra, WS-token security) #107

Merged
icub3d merged 11 commits from feature/cleanup-87-92 into main 2026-07-11 03:14:25 +00:00
Owner

Bundles the six cleanup stories (#87–#92) into one PR, per request.

Stacked on #106. This branch is based on feature/82-gateway-event-payloads (PR #106) because #90's bot-SDK rewrite builds on that branch's bot.rs. Merge #106 first, then this; the diff vs main collapses to just the cleanup once #106 lands.

#89 — Server idiomatic cleanup

  • StorageError now derives Display/Error via thiserror (identical strings; the branching From<sqlx::Error> stays manual).
  • new_id no longer panics on ULID monotonic overflow — falls back to Ulid::new(); the generator lock recovers from poisoning.
  • Gateway registry switched to parking_lot locks (removed 9 .expect("…poisoned")).
  • event_json logs a warn! on serialization failure instead of silently returning None.
  • Blanket #![allow(dead_code)] replaced with targeted per-item allows.
  • Skipped (behavior-preserving): pagination-envelope / check-ordering unification and the after cursor — left untouched to avoid response changes.

#88 — Tauri core cleanup

  • ed25519-dalek zeroize feature on; intermediate mnemonic strings wrapped in Zeroizing.
  • Collapsed the byte-identical get_signing_key_for/get_seed_for into one load_seed_for.
  • ACTIVE_ACCOUNT switched to parking_lot::Mutex (no poisoning).
  • load_accounts_index surfaces a distinct error on a corrupt index instead of silently treating it as empty.

#90 — Bot SDK cleanup

  • The 21 handlers (struct fields ×2, with_config init, 21 builder methods, 21-arm dispatch) collapse into one define_handlers! op↔event table; a single generic Handler<T> replaces 15 aliases.
  • Bot { config, handlers } — no more field-by-field copy in run().
  • Example bots embed decentcom_bot::Config via #[serde(flatten)] + apply_env_overrides(); fixes modbot/auditbot silently dropping seed_hex.
  • Removed unused futures-util.

#87 — Frontend performance

  • App/AppShell use useShallow narrow selectors (no more whole-store re-renders).
  • React.memo(MessageItem); message + member lists virtualized with @tanstack/react-virtual.
  • Member sort memoized; MessageItem thread-fetch effect depends on primitives, not object identity.

#91 — Frontend cleanup + WS-token security

  • Session token out of the WebSocket URL (was ?token=, landing in proxy/access logs): server reads an Authorization: Bearer header (Rust SDK) or an access_token.<token> WebSocket subprotocol (browser — the one header browsers can set), echoing back decentcom.v1; the URL query path is removed. SDK sends a bearer header; web client sends the subprotocol. Tokens are hex, so subprotocol-safe.
  • Shared utils/pubkey.ts + utils/address.ts (deduped truncatePubkey/shortPubkey/normalizeAddress); ProfilePopover's distinct truncation left intact.
  • serverStore: extracted loadServerData(); MESSAGE_CREATE does a sorted insert instead of a full re-sort.
  • Shared extractErrorMessage() reused by apiRequest and uploadFile.
  • KeySafety/Setup clear the in-memory seed phrase (and best-effort clipboard) on completion.

#92 — Build/infra

  • Hoisted anyhow, toml, url, bip39, reqwest, tokio-tungstenite, wiremock into [workspace.dependencies].
  • Slimmed tokio for the SDK + bots (rt-multi-thread, macros, sync, time, net, not full).
  • Dockerfile: the client-member sed now fails loudly via a grep -q guard instead of silently no-oping.
  • Moved the cargo audit --ignore RUSTSEC-… chain into .cargo/audit.toml (one per line, with rationale); CI just runs cargo audit.
  • Added cargo fmt --all -- --check to CI + a workspace-wide format pass (isolated in its own style: commit).
  • Replaced a stray npm package-lock.json with an updated pnpm pnpm-lock.yaml (what CI installs from).
  • arm64 release (needs your infra): it's built under QEMU emulation on the k8s runner. I can't provision a native arm64 runner or safely verify a CI change, so I documented the faster options (native runner / cross-compile / drop arm64) inline in release.yml rather than ship an unverifiable break. Your call.

Verification

  • cargo build --workspace, cargo clippy --workspace --all-targets -- -D warnings, cargo fmt --all --check — all clean
  • cargo test --workspace204 passed
  • Frontend (pnpm): build ok, lint clean, 89 vitest tests pass

Closes #88, #89, #90, #92. Part of #87, #91 (the pagination-unification sub-item of #89 and the arm64-runner sub-item of #92 are intentionally left, noted above).

🤖 Generated with Claude Code

Bundles the six cleanup stories (#87–#92) into one PR, per request. > **Stacked on #106.** This branch is based on `feature/82-gateway-event-payloads` (PR #106) because #90's bot-SDK rewrite builds on that branch's `bot.rs`. **Merge #106 first**, then this; the diff vs `main` collapses to just the cleanup once #106 lands. ## #89 — Server idiomatic cleanup - `StorageError` now derives `Display`/`Error` via **thiserror** (identical strings; the branching `From<sqlx::Error>` stays manual). - `new_id` no longer panics on ULID monotonic overflow — falls back to `Ulid::new()`; the generator lock recovers from poisoning. - Gateway registry switched to **parking_lot** locks (removed 9 `.expect("…poisoned")`). - `event_json` logs a `warn!` on serialization failure instead of silently returning `None`. - Blanket `#![allow(dead_code)]` replaced with targeted per-item allows. - *Skipped (behavior-preserving):* pagination-envelope / check-ordering unification and the `after` cursor — left untouched to avoid response changes. ## #88 — Tauri core cleanup - `ed25519-dalek` **zeroize** feature on; intermediate mnemonic strings wrapped in `Zeroizing`. - Collapsed the byte-identical `get_signing_key_for`/`get_seed_for` into one `load_seed_for`. - `ACTIVE_ACCOUNT` switched to `parking_lot::Mutex` (no poisoning). - `load_accounts_index` surfaces a distinct error on a corrupt index instead of silently treating it as empty. ## #90 — Bot SDK cleanup - The 21 handlers (struct fields ×2, `with_config` init, 21 builder methods, 21-arm dispatch) collapse into one `define_handlers!` op↔event table; a single generic `Handler<T>` replaces 15 aliases. - `Bot { config, handlers }` — no more field-by-field copy in `run()`. - Example bots embed `decentcom_bot::Config` via `#[serde(flatten)]` + `apply_env_overrides()`; **fixes modbot/auditbot silently dropping `seed_hex`**. - Removed unused `futures-util`. ## #87 — Frontend performance - `App`/`AppShell` use `useShallow` narrow selectors (no more whole-store re-renders). - `React.memo(MessageItem)`; message + member lists virtualized with `@tanstack/react-virtual`. - Member sort memoized; `MessageItem` thread-fetch effect depends on primitives, not object identity. ## #91 — Frontend cleanup + WS-token security - **Session token out of the WebSocket URL** (was `?token=`, landing in proxy/access logs): server reads an `Authorization: Bearer` header (Rust SDK) or an `access_token.<token>` **WebSocket subprotocol** (browser — the one header browsers can set), echoing back `decentcom.v1`; the URL query path is removed. SDK sends a bearer header; web client sends the subprotocol. Tokens are hex, so subprotocol-safe. - Shared `utils/pubkey.ts` + `utils/address.ts` (deduped `truncatePubkey`/`shortPubkey`/`normalizeAddress`); `ProfilePopover`'s distinct truncation left intact. - `serverStore`: extracted `loadServerData()`; `MESSAGE_CREATE` does a sorted insert instead of a full re-sort. - Shared `extractErrorMessage()` reused by `apiRequest` and `uploadFile`. - `KeySafety`/`Setup` clear the in-memory seed phrase (and best-effort clipboard) on completion. ## #92 — Build/infra - Hoisted `anyhow, toml, url, bip39, reqwest, tokio-tungstenite, wiremock` into `[workspace.dependencies]`. - Slimmed `tokio` for the SDK + bots (`rt-multi-thread, macros, sync, time, net`, not `full`). - Dockerfile: the client-member `sed` now **fails loudly** via a `grep -q` guard instead of silently no-oping. - Moved the `cargo audit --ignore RUSTSEC-…` chain into **`.cargo/audit.toml`** (one per line, with rationale); CI just runs `cargo audit`. - Added **`cargo fmt --all -- --check`** to CI + a workspace-wide format pass (isolated in its own `style:` commit). - Replaced a stray npm `package-lock.json` with an updated pnpm `pnpm-lock.yaml` (what CI installs from). - **arm64 release** (needs your infra): it's built under QEMU emulation on the `k8s` runner. I can't provision a native arm64 runner or safely verify a CI change, so I documented the faster options (native runner / cross-compile / drop arm64) inline in `release.yml` rather than ship an unverifiable break. Your call. ## Verification - `cargo build --workspace`, `cargo clippy --workspace --all-targets -- -D warnings`, `cargo fmt --all --check` — all clean - `cargo test --workspace` — **204 passed** - Frontend (pnpm): `build` ok, `lint` clean, **89 vitest tests** pass Closes #88, #89, #90, #92. Part of #87, #91 (the pagination-unification sub-item of #89 and the arm64-runner sub-item of #92 are intentionally left, noted above). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
refactor(shared): define gateway event payloads once, kill server/bot drift
Some checks failed
Security Audit / Gitleaks (Secrets) (pull_request) Successful in 6s
Security Audit / Trivy (SCA & IaC) (pull_request) Failing after 16s
Security Audit / PNPM Audit (JS SCA) (pull_request) Failing after 23s
CI / Client — test & lint (pull_request) Successful in 45s
Security Audit / Semgrep (SAST) (pull_request) Failing after 1m8s
CI / Server — test & lint (pull_request) Successful in 2m50s
Security Audit / Cargo Audit (Rust SCA) (pull_request) Failing after 3m12s
1aed69fd5d
Completes the last checkbox of #82. The server broadcaster and the bot-sdk
each defined their own gateway event payload types, and they had silently
drifted: the bot-sdk's `MessageEvent`/`ChannelEvent`/`RoleEvent`/`MemberEvent`
were trimmed subsets that dropped fields, and worse, several bot-sdk decode
types did not match the server's wire shape at all — `MEMBER_LEAVE`,
`MEMBER_BAN`, `MEMBER_UPDATE`, and `THREAD_MESSAGE_CREATE` would fail to
decode in any bot, because serde only ignores *extra* fields, not missing
required ones.

Define each gateway payload once in `shared` and have both ends use it:
- New payload structs in `shared::gateway`: `ThreadMessageData`,
  `MessageDeleteData`, `DeletedId`, `MemberLeaveData`, `MemberKickData`,
  `MemberBanData`, `MemberUpdateData`. The resource-carrying events reuse the
  `shared::rest` types (`Message`, `Channel`, `Role`, `Member`).
- Server handlers now construct these typed structs instead of ad-hoc
  `serde_json::json!` literals and local one-off structs, so the broadcast
  shape is checked at compile time.
- bot-sdk's `Event` enum, handler types, `Context::reply`, and the example
  bots (auditbot/modbot/welcomebot) consume the shared types; bot-sdk no
  longer keeps any local copies. Member join/leave/kick/ban/update now carry
  distinct, correct payload types.

The server's outgoing JSON is byte-identical to before (verified against the
current React/Tauri client's field usage), so this is wire-compatible; the
change only fixes the bot-sdk decode side. A new `shared` test locks the wire
field names of the small event payloads.

Also removes a stray unused `serde::Serialize` import in profiles/handlers.rs
that surfaced from the #104+#105 merge interaction.

Closes #82

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- storage: derive StorageError Display/Error/From via thiserror, keeping
  identical variants and Display strings; retain the custom From<sqlx::Error>
  branching (unique-violation -> Conflict, RowNotFound -> NotFound).
- sqlite::new_id: recover from a poisoned id-generator mutex and fall back to
  a fresh Ulid::new() on monotonic overflow instead of panicking.
- gateway::registry: use parking_lot::RwLock to drop the 9 "...poisoned"
  expects; behavior unchanged (guards returned directly).
- gateway::events::event_json: log a tracing::warn! before returning None on
  serialization failure.
- Replace blanket #![allow(dead_code)] in permissions.rs and storage/mod.rs
  with targeted #[allow(dead_code)] on the specific unused items
  (MENTION_EVERYONE, VIEW_AUDIT_LOG, MemberUser::is_read_only, ThreadFollower,
  SqliteStorage::in_memory); drop a stale unused serde::Serialize import.

Part of #89

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Enable ed25519-dalek `zeroize` feature so SigningKey is zeroized on
  drop; wrap intermediate mnemonic phrase strings in `Zeroizing` so
  secret words don't linger in freed heap.
- Collapse the byte-identical `get_signing_key_for`/`get_seed_for` into
  a single `load_seed_for` helper; `get_signing_key_for` now derives the
  key from it and the export path uses it directly.
- Switch `ACTIVE_ACCOUNT` to `parking_lot::Mutex` to avoid poisoning and
  the `.lock().unwrap()` panic paths.
- `load_accounts_index` now surfaces a distinct error on a corrupt
  on-disk index instead of silently treating it as empty (which could
  clobber existing accounts); a genuinely absent entry still yields an
  empty index.

Part of #88

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Replace the 21-handler triplication (`struct Bot` fields, `with_config`
  init, `struct Handlers`, 21 builder methods, 21-arm dispatch) with one
  `define_handlers!` op↔event table that generates the `Handlers` struct, the
  `on_*` builder methods, and the `dispatch` match. A single generic
  `Handler<T>` alias replaces the 15 per-type aliases. Adding an event is now
  one table row + one `Event` variant.
- Fold `Bot`'s handler fields into the `Handlers` struct it owns
  (`Bot { config, handlers }`), removing the field-by-field copy in `run()`.
- Example bots embed `decentcom_bot::Config` via `#[serde(flatten)]` and call
  the new `Config::apply_env_overrides()` instead of re-implementing the
  env-override block. This fixes modbot/auditbot silently dropping `seed_hex`
  (they hard-coded `seed_hex: None`).
- Remove the unused `futures-util` dependency from bot-sdk.

Part of #90

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
# Conflicts:
#	server/src/profiles/handlers.rs
Issue #87 (performance):
- AppShell/App: replace whole-store subscriptions with useShallow slices
- MessageItem: wrap in React.memo; depend the thread-fetch effect on
  primitive thread_id/reply_count instead of the thread object identity
- MessageList/MemberList: virtualize with @tanstack/react-virtual
- MemberList: memoize the member sort with useMemo
- serverStore: insert new MESSAGE_CREATE messages in sorted position
  instead of re-sorting the whole array

Issue #91 (cleanup):
- Extract shared truncatePubkey/shortPubkey (utils/pubkey) and
  normalizeAddress (utils/address); remove copy-pasted local copies
- serverStore: extract loadServerData() shared by connect()/revalidate()
- api: extract extractErrorMessage(); reuse in apiRequest and media upload
- KeySafety/Setup: clear generatedIdentity/seed state on completion and
  best-effort clear the clipboard after copy

Part of #87, #91

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The gateway session token was passed as a WebSocket URL query param
(`?token=…`), so it landed in access/proxy logs and browser history. Move it
out of the URL on every client:

- Server (`gateway/handler.rs`): accept the token from the `Authorization:
  Bearer` header (non-browser clients) or, for browsers — which can't set WS
  headers but can offer subprotocols — from an `access_token.<token>`
  WebSocket subprotocol. The server echoes back the `decentcom.v1` subprotocol
  so the browser handshake completes cleanly and never reflects the token. The
  URL query path is removed entirely. Added unit tests for token extraction.
- Rust SDK (`gateway.rs`): send the token in an `Authorization: Bearer` header
  on the handshake request instead of the URL; `build_ws_url` no longer takes
  or appends a token.
- Web client (`services/gateway.ts`): pass `["decentcom.v1",
  "access_token.<token>"]` as the WebSocket subprotocols instead of a URL
  query param.

Session tokens are hex, so they are valid WebSocket subprotocol tokens.

Part of #91

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Hoist repeated third-party deps into `[workspace.dependencies]` and switch
  member crates to `{ workspace = true }`: anyhow, toml, url, bip39, reqwest,
  tokio-tungstenite, wiremock.
- Slim tokio for the non-server crates (SDK + bots) to
  `rt-multi-thread, macros, sync, time, net` instead of `full`.
- Dockerfile: the `sed` that drops the Tauri client workspace member now fails
  loudly (via a `grep -q` guard) if the member line isn't found, instead of
  silently no-oping when the manifest is reformatted.
- Move the long inline `cargo audit --ignore RUSTSEC-...` chain into
  `.cargo/audit.toml`, one advisory per line with a rationale; CI just runs
  `cargo audit`.
- Add `cargo fmt --all -- --check` to CI (the formatting pass itself is the
  next commit) and note the arm64/QEMU release tradeoff + faster options inline.
- Replace the stray npm `package-lock.json` (accidentally introduced) with an
  updated pnpm `pnpm-lock.yaml`, which is what CI installs from.

Closes #92

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
style: cargo fmt --all across the workspace
Some checks failed
Security Audit / Gitleaks (Secrets) (pull_request) Successful in 7s
Security Audit / PNPM Audit (JS SCA) (pull_request) Failing after 18s
Security Audit / Trivy (SCA & IaC) (pull_request) Failing after 18s
CI / Client — test & lint (pull_request) Successful in 40s
Security Audit / Semgrep (SAST) (pull_request) Failing after 1m3s
CI / Server — test & lint (pull_request) Successful in 2m48s
Security Audit / Cargo Audit (Rust SCA) (pull_request) Failing after 3m43s
17a8e21fb1
Format the entire workspace so the new `cargo fmt --all -- --check` CI gate
(added in the previous commit) passes. Pure formatting — no logic changes.

Part of #92

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Merge branch 'main' into feature/cleanup-87-92
Some checks failed
Security Audit / Gitleaks (Secrets) (pull_request) Successful in 7s
Security Audit / PNPM Audit (JS SCA) (pull_request) Failing after 20s
Security Audit / Trivy (SCA & IaC) (pull_request) Failing after 19s
CI / Client — test & lint (pull_request) Successful in 42s
Security Audit / Semgrep (SAST) (pull_request) Failing after 1m24s
CI / Server — test & lint (pull_request) Successful in 2m51s
Security Audit / Cargo Audit (Rust SCA) (pull_request) Failing after 3m58s
885db704b4
icub3d merged commit 4bcf24b2c3 into main 2026-07-11 03:14:25 +00:00
icub3d deleted branch feature/cleanup-87-92 2026-07-11 03:14:26 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
icub3d/decentcom!107
No description provided.