Skip to content

feat: move the SIWE challenge below the facade - #943

Merged
FSM1 merged 2 commits into
mainfrom
feat/910-siwe-challenge-below-facade
Aug 2, 2026
Merged

feat: move the SIWE challenge below the facade#943
FSM1 merged 2 commits into
mainfrom
feat/910-siwe-challenge-below-facade

Conversation

@FSM1

@FSM1 FSM1 commented Aug 1, 2026

Copy link
Copy Markdown
Owner

Problem

facade.siweLogin(message, signature) existed, but nothing exposed the challenge an EIP-4361
message must embed. The engine's API client already had siwe_challenge() against
POST /auth/siwe/challenge; it was unreachable from the facade.

So #804 left apps/web/src/auth/siweNonce.ts doing a plain fetch against the API's public
challenge endpoint. That was the only direct API call in apps/web, and it contradicted
blueprint/web-client.md — the app's only vault-facing dependency is packages/client.

Change

  • Engine. Engine::siwe_challenge over the existing ApiClient::siwe_challenge, returning the
    nonce. ApiClient::siwe_challenge now validates it against EIP-4361's alphanumeric class before
    constructing a SiweNonce: the nonce lands verbatim in the text a wallet signs, so anything
    outside that class would let a hostile challenge response splice extra fields into the signed
    message. That fail-closed check is what moved with the code out of the deleted siweNonce.ts.
  • Threading. The crates/wasm binding, then packages/client: worker/protocol.ts request +
    result union, worker/engineWasm.ts, worker/engineHost.ts, worker/serve.ts, transport.ts,
    correlatedTransport.ts, broadcast.ts WireRead, broadcastTransport.ts, leaderRelay.ts,
    engineClient.ts, facade.ts. The nonce crosses as a plain string, so no new descriptor type is
    minted.
  • Web. apps/web/src/auth/siweNonce.ts and its test are deleted. WalletLoginButton takes a
    requestNonce prop fed by useAuth().siweChallenge() and no longer takes apiBaseUrl.
  • leaderRelay's read dispatch becomes a switch behind an annotated return type, so a fourth
    WireRead kind fails to compile instead of leaving a follower's request unanswered.
  • The API pins the nonce class it issues, and the contract suite's !nonce.is_empty() becomes the
    class assertion it was standing in for. A decode-side hard reject needs its producer pinned at the
    producer, or a change of alphabet breaks every wallet login with an opaque decode error visible
    only to the live-stack suite.

Settling the issue's Note

The issue asked whether the wallet method belongs on the cold login page at all. It does not.
Command::SiweLogin is refused with NotStarted before facade.start and cannot be otherwise —
SIWE authenticates the account against the API and cannot produce the Core Kit login secret a vault
cold start needs; blueprint/engine.md and blueprint/web-client.md both call it a secondary
method. So siweChallenge is gated identically, and the refusal now lands before the wallet
signature prompt instead of after it.

The placement half could not land here: App.tsx has only / and /files/:nodeId?, so there is no
started-engine surface to move the button to. Filed as #938.

Deferred, each with a depends-on edge back to #910

One informational delta, no action: the challenge POST now rides FetchHttp, which sets
credentials: 'include', so it carries the HTTP-only refresh cookie where the page-level fetch
sent none. No CSRF surface — the nonce is session-unbound and side-effect-free — and the read is
gated on started, so the session is already correlated.

#914 rebase note

This shares a facade-threading surface with #914, which is not being worked yet. The threading is
deliberately minimal and mechanical: one new member per file, placed next to the existing
snapshot/download reads, no shared machinery reshaped. correlatedTransport's
request/dispatch/settle core is untouched — it is already result-type-agnostic. The one
non-additive edit is leaderRelay's read dispatch, which #914 does not touch since it adds an event,
not a read. #942 is filed to sequence the real generalization after #914.

Tests

  • crates/engine/src/api/client.rssiwe_challenge_refuses_a_nonce_outside_the_eip4361_class
    covers short, hyphenated, newline-bearing, spaced, empty, over-long, and unicode-confusable nonces
    (Arabic-Indic digits and fullwidth Latin, which char::is_alphanumeric would have accepted), and
    asserts the refusal never echoes the value it rejected. siwe_challenge_accepts_the_class_boundaries
    pins 8 and 128. Reverting is_eip4361_nonce fails both, plus the pre-existing
    siwe_challenge_returns_a_nonce_with_no_request_body, whose fixture had to become a real nonce.
  • apps/apiissues a nonce inside the EIP-4361 class the engine enforces. Change the alphabet in
    challenge.service.ts and it fails.
  • crates/engine/tests/facade.rsa_siwe_challenge_before_start_is_rejected_not_started (drop the
    gate and it fails) and a_started_engine_serves_the_nonce_from_its_api_client, which asserts the
    request reaches /auth/siwe/challenge.
  • packages/client — facade delegation, the serveEngine worker round-trip, and a follower
    challenge served off the leader engine. Remove the serve.ts case and the round-trip hangs to
    timeout; remove the leaderRelay arm and it fails to compile.
  • apps/webuseAuth reads the nonce from the facade and never from the API. The deleted
    siweNonce.test.ts's validation assertions live on in the Rust client test above.

Verification

All exit 0 on this branch:

  • cargo fmt --all
  • cargo clippy --workspace --all-targets -- -D warnings — clean
  • cargo check --workspace --all-targets
  • cargo check -p cipherbox-wasm --target wasm32-unknown-unknown --all-targets
  • cargo test -p cipherbox-engine -p cipherbox-wasm — 554 lib + 11 facade + the rest, 0 failed
  • pnpm -r --if-present run typecheck — api, client, web all clean
  • pnpm -r --if-present run test — client 114, api 178, web 64, all passed
  • pnpm --filter @cipherbox/client test:browser — 19 passed
  • pnpm exec eslint . — clean

Review gates run on git diff main...HEAD: /security-review (no findings), /simplify (four
angles), /crypto-privacy-review (no must-fix). Findings folded into the second commit; the rest
filed as #938 / #939 / #942.

Closes #910

Summary by CodeRabbit

  • New Features
    • Wallet sign-in now retrieves SIWE challenges through the engine before creating a signature.
    • SIWE challenge requests are supported across local, worker, and broadcast connection modes.
  • Bug Fixes
    • SIWE nonces are now validated against the EIP-4361 format: 8–128 ASCII alphanumeric characters.
    • Challenge requests fail safely when the engine is not started or the service returns an error.

FSM1 added 2 commits August 1, 2026 11:29
The nonce an EIP-4361 message embeds now comes from the engine, over the
API client's existing siwe_challenge, instead of a plain fetch in apps/web
against the public challenge endpoint. That was the only direct API call in
apps/web and it contradicted blueprint/web-client.md: the app's only
vault-facing dependency is packages/client.

The engine validates the nonce against EIP-4361's alphanumeric class at the
trust boundary, so a hostile challenge response cannot inject extra fields
into the text a wallet signs. That fail-closed check moves with the code
from the deleted apps/web/src/auth/siweNonce.ts.

siweChallenge is gated on start exactly like the siweLogin that spends it:
SIWE is a secondary method, so a nonce the engine could not spend is refused
before the host prompts a wallet for it rather than after.

Closes #910
Review-gate follow-ups on the same change.

The engine hard-rejects a nonce outside EIP-4361's alphanumeric class, so the
API that produces it must be pinned to the same invariant: challenge.service
asserts the class it issues, and the contract suite's vacuous non-empty check
becomes the class assertion it was standing in for. Without that, changing the
API's alphabet breaks every wallet login with an opaque decode error that only
the live-stack suite can see.

Also: unicode-confusable and boundary cases on the client-side check, an
assertion that a refusal never echoes the nonce it rejected, the wasm host
module header refreshed to list the new read, and the nonce fetched before the
signing phase latches so the label stops promising a wallet prompt that has not
happened yet.

Drops the redundant second NotStarted guard, the duplicated gate rationale, and
a hand-copied nonce literal.
@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

The SIWE nonce request moved from the web API layer into the engine facade. The request now traverses WASM, worker, local, correlated, and broadcast transports before reaching wallet login. Nonce validation now enforces the EIP-4361 format.

Changes

SIWE challenge flow

Layer / File(s) Summary
SIWE nonce validation
apps/api/src/auth/services/challenge.service.test.ts, crates/contract/tests/contract.rs, crates/engine/src/api/client.rs
API and engine tests enforce 8–128 ASCII alphanumeric nonce values.
Engine facade and WASM binding
crates/engine/src/facade.rs, crates/engine/tests/facade.rs, crates/wasm/src/host.rs
The engine exposes siwe_challenge(), maps API errors, and exports siweChallenge to JavaScript.
Client worker and transport plumbing
packages/client/src/worker/*, packages/client/src/transport.ts, packages/client/src/correlatedTransport.ts, packages/client/src/engineClient.ts
Worker protocols, hosts, transports, and serving logic carry the challenge request and string response.
Facade and broadcast routing
packages/client/src/facade.ts, packages/client/src/broadcast.ts, packages/client/src/broadcastTransport.ts, packages/client/src/leaderRelay.ts, packages/client/src/testkit.ts
Client facades expose the challenge, and follower requests route through the leader.
Web authentication integration
apps/web/src/auth/useAuth.ts, apps/web/src/components/auth/WalletLoginButton.tsx, apps/web/src/routes/LoginPage.tsx, apps/web/src/auth/siweNonce.ts, apps/web/src/auth/siweNonce.test.ts
Web authentication uses the facade callback for nonce retrieval. The direct nonce module and its tests were deleted. Tests add facade challenge coverage.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant WalletLoginButton
  participant AuthHook
  participant EngineFacade
  participant ClientTransport
  participant WasmHost
  participant ApiClient
  WalletLoginButton->>AuthHook: requestNonce()
  AuthHook->>EngineFacade: siweChallenge()
  EngineFacade->>ClientTransport: siweChallenge()
  ClientTransport->>WasmHost: siweChallenge()
  WasmHost->>ApiClient: POST /auth/siwe/challenge
  ApiClient-->>WasmHost: validated nonce
  WasmHost-->>ClientTransport: nonce string
  ClientTransport-->>EngineFacade: nonce string
  EngineFacade-->>AuthHook: nonce string
  AuthHook-->>WalletLoginButton: nonce string
  WalletLoginButton->>WalletLoginButton: create SIWE message
Loading

Possibly related PRs

  • FSM1/cipher-box#911: This PR replaces the direct siweNonce implementation and rewires wallet login through the engine facade.
  • FSM1/cipher-box#728: This PR extends the worker and facade transport surface introduced there.
  • FSM1/cipher-box#733: This PR extends the broadcast transport and leader relay protocol with SIWE challenge messages.

Suggested labels: comp:engine

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes satisfy issue #910 by moving SIWE challenge retrieval through the engine facade, client layers, WASM, and wallet login flow.
Out of Scope Changes check ✅ Passed The changes remain within issue #910 scope, including nonce validation and tests for the new facade-based SIWE challenge flow.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes moving SIWE challenge retrieval into the engine facade.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/910-siwe-challenge-below-facade

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@FSM1
FSM1 marked this pull request as ready for review August 1, 2026 11:49
@FSM1
FSM1 marked this pull request as draft August 1, 2026 14:53
@FSM1
FSM1 marked this pull request as ready for review August 1, 2026 14:54
@FSM1
FSM1 marked this pull request as draft August 1, 2026 17:15
@FSM1
FSM1 marked this pull request as ready for review August 1, 2026 17:15
@FSM1
FSM1 marked this pull request as draft August 1, 2026 17:19

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/client/src/worker/engineHost.ts`:
- Around line 110-112: Update EngineHost construction in onBootstrap() to pass
config.apiBaseUrl alongside config.profile, then update the EngineHost
constructor and its EngineHandle creation to accept and forward apiBaseUrl as
api_base_url. Preserve siweChallenge() behavior while ensuring requests target
the configured origin.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 2e78cd65-0aa6-49b0-b881-854e53e85b39

📥 Commits

Reviewing files that changed from the base of the PR and between afb3887 and 8cf97b6.

📒 Files selected for processing (30)
  • apps/api/src/auth/services/challenge.service.test.ts
  • apps/web/src/auth/siweNonce.test.ts
  • apps/web/src/auth/siweNonce.ts
  • apps/web/src/auth/useAuth.test.tsx
  • apps/web/src/auth/useAuth.ts
  • apps/web/src/components/auth/WalletLoginButton.tsx
  • apps/web/src/routes/LoginPage.tsx
  • apps/web/src/test/authFakes.tsx
  • crates/contract/tests/contract.rs
  • crates/engine/src/api/client.rs
  • crates/engine/src/facade.rs
  • crates/engine/tests/facade.rs
  • crates/wasm/src/host.rs
  • packages/client/src/broadcast.ts
  • packages/client/src/broadcastTransport.test.ts
  • packages/client/src/broadcastTransport.ts
  • packages/client/src/correlatedTransport.ts
  • packages/client/src/engineClient.ts
  • packages/client/src/facade.test.ts
  • packages/client/src/facade.ts
  • packages/client/src/leaderRelay.ts
  • packages/client/src/testkit.ts
  • packages/client/src/transport.ts
  • packages/client/src/worker/engineHost.ts
  • packages/client/src/worker/engineWasm.ts
  • packages/client/src/worker/protocol.ts
  • packages/client/src/worker/serve.test.ts
  • packages/client/src/worker/serve.ts
  • packages/client/test/browser/fakeEngine.worker.ts
  • packages/client/test/browser/journalEngine.worker.ts
💤 Files with no reviewable changes (2)
  • apps/web/src/auth/siweNonce.test.ts
  • apps/web/src/auth/siweNonce.ts

Comment thread packages/client/src/worker/engineHost.ts
@FSM1
FSM1 marked this pull request as ready for review August 2, 2026 17:55
@FSM1
FSM1 merged commit 76c9e52 into main Aug 2, 2026
24 checks passed
@FSM1
FSM1 deleted the feat/910-siwe-challenge-below-facade branch August 2, 2026 17:56
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.

web: move the SIWE challenge below the facade

1 participant