docs: server-side crypto invariants, nonce lifecycle, treasury/wallet components and hooks reference - #599
Merged
codebestia merged 1 commit intoAug 31, 2026
Conversation
…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.
|
@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! 🚀 |
This was referenced Aug 31, 2026
Closed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
devand 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.tsandlib/signalInvariants.ts.findForbiddenSessionStateField, the fullFORBIDDEN_SESSION_STATE_FIELDStable with why each name is forbidden, theenvelopessub-scan, and the deliberate use ofhasOwnPropertyso a field present but empty is still a rejection.send_messageandedit_messagehandlers, returning before destructuring, before the membership check, and before anydb.querycall — and why that ordering matters (no existence oracle, constant rejection cost, nothing partially written)..strict(): Zod's default object mode silently strips an unknown key, so a non-strict schema would accept a payload carryingratchetState, drop the field, and return200, 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.security-ciregression job: both theregressionjob (ciphertext-only guard assertions plus the source scan over every backend.tsfile for declared secret field names, with the actual match pattern) and thedependency-auditjob, plus guidance for changing the invariants safely.apps/backend/docs/concepts-nonce-lifecycle.md(#563)Covers
lib/nonce.ts.Map.set./auth/challengeendpoint a way to permanently break a victim's device linking. Notes the reinforcing separation at the rate-limit layer, wheredevice_link_*buckets mirror theauth_*limits so hammering one flow cannot lock out the other.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.apps/web/docs/components-treasury-wallet.md(#570)Covers
src/components/treasury/andsrc/components/wallet/.Proposalshape and the distinction between the backend rowidand the displayproposalId), and the action it triggers.signMessageover`${type}:${proposalId}`, thenPOST /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.ProposalCarddoes not checkpublicKeybefore 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.apps/web/docs/hooks.md(#571)Covers all six hooks in
src/hooks/.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, anduseMessageSearchIndexkeying its effect on array identity rather than contents.useLocalSearch, entirely local state over a module-level worker singleton) and which assume a single owner (useInboundPipeline,useMessageHistory,useMessageSearchIndex, and effectivelyusePushSubscription), with the concrete cost of violating each. Notes thatuseSocketopens an independent connection per call — which the app does today in three places — and the shared-resume-cursor consequence.useSocketanduseInboundPipelineacross all three axes: the data dependency, the effect-ordering guarantee that lets the pipeline attach its listeners beforeconnectfires and replay begins, and the teardown ordering that makes disconnect the socket owner's job alone.useSocketas the one case that constructs its client during render inside auseMemo— 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.tsfromapps/backend— 4 files, 76 tests passed, confirming the documented guard and nonce behaviour.prettier --checkclean on all four new files.conversations/%5Bid%5D/page.tsx).