Skip to content

docs: server-side crypto invariants, nonce lifecycle, treasury/wallet components and hooks reference - #599

Merged
codebestia merged 1 commit into
codebestia:devfrom
BigManly4:docs/crypto-invariants-nonce-frontend-reference
Aug 31, 2026
Merged

docs: server-side crypto invariants, nonce lifecycle, treasury/wallet components and hooks reference#599
codebestia merged 1 commit into
codebestia:devfrom
BigManly4:docs/crypto-invariants-nonce-frontend-reference

Conversation

@BigManly4

Copy link
Copy Markdown

Summary

Adds four reference documents requested in #562, #563, #570 and #571. Documentation only — no source, schema, or CI configuration was changed. Every statement was written against the current code on dev and verified by reading the implementations and running the relevant test suites.

closes #562
closes #563
closes #570
closes #571

apps/backend/docs/concepts-crypto-invariants.md (#562)

Covers lib/ciphertextInvariant.ts and lib/signalInvariants.ts.

  • States both invariants plainly: the server stores ciphertext only, and never accepts session state, ratchet state, or private keys on any inbound payload — including the reasoning for why rejecting is stronger than accepting-and-ignoring (request logs, error reports, crash dumps).
  • Documents findForbiddenSessionStateField, the full FORBIDDEN_SESSION_STATE_FIELDS table with why each name is forbidden, the envelopes sub-scan, and the deliberate use of hasOwnProperty so a field present but empty is still a rejection.
  • Documents that rejection happens before any database lookup — the guard is the first statement of the send_message and edit_message handlers, returning before destructuring, before the membership check, and before any db.query call — and why that ordering matters (no existence oracle, constant rejection cost, nothing partially written).
  • Explains why the relevant Zod schemas are .strict(): Zod's default object mode silently strips an unknown key, so a non-strict schema would accept a payload carrying ratchetState, drop the field, and return 200, leaving a reintroduced plaintext or key-bearing field permanently invisible. Includes the table of every strict schema that gates crypto-relevant input, and notes that .extend() composites re-apply .strict() explicitly.
  • Documents the security-ci regression job: both the regression job (ciphertext-only guard assertions plus the source scan over every backend .ts file for declared secret field names, with the actual match pattern) and the dependency-audit job, plus guidance for changing the invariants safely.

apps/backend/docs/concepts-nonce-lifecycle.md (#563)

Covers lib/nonce.ts.

  • Documents creation, single-use consumption, and TTL for both nonce kinds, opening with a side-by-side comparison table (mint, consume, store, key, TTL, issuing route, burning route, whether the caller is authenticated, what success grants).
  • Covers the burn-on-read semantics in detail, including that a wrong nonce still deletes the entry — which forces a guessing run through the challenge rate limit rather than the cheaper verify path — and the overwrite semantics of Map.set.
  • Explains why device linking uses a separate namespace: shared storage would let the two flows silently overwrite each other, and would give anyone able to reach the unauthenticated /auth/challenge endpoint a way to permanently break a victim's device linking. Notes the reinforcing separation at the rate-limit layer, where device_link_* buckets mirror the auth_* limits so hammering one flow cannot lock out the other.
  • Documents where nonces live (in-process Maps, not Redis or Postgres) and the behaviour across a restart (all outstanding nonces lost; fails safe but user-visible, so clients should retry with a fresh challenge) and in a multi-node deployment (challenge and verify must hit the same process; otherwise intermittent failures that look like spurious nonce rejections). Includes the Redis migration path that would remove both limitations.
  • Explains the replay resistance provided (within-flow and cross-flow replay prevention, bounded freshness, expensive online guessing, application-scoped signatures) and its limits (no protection against a controlled wallet or an in-flight attacker, no transport or client binding, per-process only, and no statement about the linked device's trustworthiness).

apps/web/docs/components-treasury-wallet.md (#570)

Covers src/components/treasury/ and src/components/wallet/.

  • Each component documented with a props table, the data it expects (including the full Proposal shape and the distinction between the backend row id and the display proposalId), and the action it triggers.
  • For every action, states where it goes and in what order: voting is Freighter first, then backend REST (signMessage over `${type}:${proposalId}`, then POST /treasury/proposals/:id/{approve|reject}); proposal creation is backend REST only, with no wallet interaction and no on-chain cost; wallet connect is Freighter only, with no backend call and no session creation.
  • Documents the states a user can be in — not installed, not connected, wrong network, signature rejected — describing what the code actually does in each case, including two honest gaps: ProposalCard does not check publicKey before voting and relies on Freighter's own prompt, and message signing carries no network passphrase, so voting cannot fail on a network mismatch and no component renders a network warning.
  • Cross-links the contract docs for on-chain semantics (proposal lifecycle, proposals API, token transfer flow and API, token transfer storage, deployment and invocation) plus the backend Treasury API doc for the TTL-to-ledger conversion behind the modal's duration options.

apps/web/docs/hooks.md (#571)

Covers all six hooks in src/hooks/.

  • Each hook documented with arguments, return shape, side effects, and cleanup — including the details that bite: useInboundPipeline's two-ref join for out-of-order ciphertext and metadata, its sync loop having no cancellation on a conversation switch, useLocalSearch's run-counter for discarding stale results and its non-memoized return object, and useMessageSearchIndex keying its effect on array identity rather than contents.
  • Documents which hooks are safe to mount more than once (useLocalSearch, entirely local state over a module-level worker singleton) and which assume a single owner (useInboundPipeline, useMessageHistory, useMessageSearchIndex, and effectively usePushSubscription), with the concrete cost of violating each. Notes that useSocket opens an independent connection per call — which the app does today in three places — and the shared-resume-cursor consequence.
  • Explains the ordering dependency between useSocket and useInboundPipeline across all three axes: the data dependency, the effect-ordering guarantee that lets the pipeline attach its listeners before connect fires and replay begins, and the teardown ordering that makes disconnect the socket owner's job alone.
  • Notes the SSR constraint with a per-hook table, and calls out useSocket as the one case that constructs its client during render inside a useMemo — safe today only because server renders carry no token, with the guidance not to pass a server-resolved token in.

Verification

  • npx vitest run src/__tests__/security.regression.test.ts src/__tests__/nonce.test.ts src/__tests__/ciphertextInvariant.test.ts src/__tests__/signalInvariants.socket.test.ts from apps/backend4 files, 76 tests passed, confirming the documented guard and nonce behaviour.
  • prettier --check clean on all four new files.
  • Every relative link in the four documents was resolved against the working tree; none are broken. Dynamic-route links use the repository's existing percent-encoded convention (conversations/%5Bid%5D/page.tsx).

…s references

Adds four reference documents:

- apps/backend/docs/concepts-crypto-invariants.md covering
  lib/ciphertextInvariant.ts and lib/signalInvariants.ts, the forbidden
  field lists, the pre-database rejection ordering in the WebSocket
  handlers, why the Zod schemas are .strict(), and the security-ci
  regression job that keeps all of it enforced.
- apps/backend/docs/concepts-nonce-lifecycle.md covering lib/nonce.ts:
  creation, single-use consumption and TTL for both the sign-in and the
  device-link challenge, why the two use separate namespaces, where the
  stores live across restarts and multiple nodes, and the replay
  resistance this provides and its limits.
- apps/web/docs/components-treasury-wallet.md documenting ProposalCard,
  ProposeWithdrawalModal and WalletConnectButton with props, expected
  data, which actions hit the backend REST API versus Freighter and in
  what order, the wallet states a user can be in, and cross-links to the
  contracts docs for on-chain semantics.
- apps/web/docs/hooks.md documenting useSocket, useInboundPipeline,
  useMessageHistory, useLocalSearch, useMessageSearchIndex and
  usePushSubscription with arguments, return shapes, side effects and
  cleanup, the single-owner versus multi-mount rules, the ordering
  dependency between useSocket and useInboundPipeline, and the SSR
  constraint on hooks touching window, IndexedDB or WebCrypto.

Documentation only. No source or configuration changes.
@drips-wave

drips-wave Bot commented Aug 31, 2026

Copy link
Copy Markdown

@BigManly4 Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants