Skip to content

feat(server): wallet-authenticated SEP-10 sessions - #147

Merged
moises-cisneros merged 11 commits into
mainfrom
feat/136-wallet-sessions-01-server
Oct 9, 2026
Merged

moises-cisneros merged 11 commits into
mainfrom
feat/136-wallet-sessions-01-server

Conversation

@TOMOKI977

Copy link
Copy Markdown
Contributor

Refs #136

size:exception

Server half of wallet-authenticated sessions. Flutter and the Freighter check stay in a stacked PR. This is over the 400-line review budget on purpose: ten work-unit commits, one behavior each.

Summary

SEP-10 challenges sign a wallet in and Serverpod issues the session. Hire methods require that session and resolve the wallet from wallet_account. The template email sign-in is gone. config.json publishes the public auth values (G… signing address, domains, network), never the S… secret.

Work units

  1. Models, migration and the AuthUser relation.
  2. Config and secrets, fail closed at startup.
  3. SEP-10 codec and the known vector.
  4. authorityOf and the account-entry RPC read.
  5. Challenge store and its contracts.
  6. Verifier.
  7. WalletIdp find-or-create.
  8. Endpoint, service and rate limit.
  9. WalletSessionWallet, requireLogin, and the EmailIdp removal.
  10. api.md and AppConfigRoute.

Acceptance criteria

  • A wallet that signs the challenge gets a session, and HireEndpoint.createHire works for it (integration test).
  • An expired, reused, wrong-wallet or wrong-network challenge is rejected with a typed error.
  • A bad signature is rejected, and no session is created.
  • A caller cannot act on another wallet's hire.
  • No password, email or wallet secret is stored or transmitted by this server flow.
  • In Chrome on testnet, connecting Freighter creates a session. GIF attached. (PR 2)
  • flutter analyze / flutter test pass. (PR 2)

Verification evidence

cd puls3_server && dart analyze --fatal-infos
# No issues found

cd puls3_server && dart test
# Two unrelated concurrency tests timed out at 30s under parallel load
# (chain_submission_store REL-002, escrow preparation race).
# Re-run alone: both files passed.

cd puls3_server && dart format --set-exit-if-changed .
# Clean after reflowing relay_protocol_test.dart

Notes for reviewers

  • The endpoint is WalletAuthEndpoint (client.walletAuth). A generated Client.auth would shadow the auth extension.
  • Expiry is 900 seconds, not 5 minutes. Verification is a SEP-10 transaction signature (signedChallengeXdr), not signMessage.
  • A wrong-network client signature leaves the body unchanged, so the reason is noClientSignature.
  • Sign-out revokes the refresh token. The access JWT stays valid until it expires; the next hire call with no session is HTTP 401.
  • FailClosedSessionWallet remains a test double. Production uses WalletSessionWallet.

TOMOKI977 and others added 10 commits October 9, 2026 15:44
SEP-10 sign-in needs a client challenge, a server-only challenge row, and one auth user per wallet before verification can land.

Co-authored-by: Cursor <cursoragent@cursor.com>
Production must not serve challenges until the SEP-10 signing key and auth domains are set, and that secret must never come from the environment or an error message.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@TOMOKI977 TOMOKI977 added type: feat New functionality area: backend Serverpod endpoints and persistence labels Oct 9, 2026
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

Deploying puls3 with  Cloudflare Pages  Cloudflare Pages

Latest commit: 6443b0a
Status: ✅  Deploy successful!
Preview URL: https://dd88311d.puls3-4lw.pages.dev
Branch Preview URL: https://feat-136-wallet-sessions-01.puls3-4lw.pages.dev

View logs

@moises-cisneros moises-cisneros left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Summary of Review

Reviewed and validated against PR #77 (docs/architecture/api.md) and OpenSpec Change #136 (specs/wallet-auth/spec.md, design.md, tasks.md).

Verification & Test Execution

  • Static Analysis: dart analyze --fatal-infos passes cleanly with 0 issues across puls3_server, puls3_client, and puls3_domain.
  • Formatting: dart format --set-exit-if-changed . clean (0 files changed).
  • Test Suite: Executed the full test suite against PostgreSQL and Redis test instances (756 tests passed, 0 failures). Integration suites wallet_auth_endpoint_test, wallet_idp_test, wallet_session_hire_test, and challenge_store_test all run clean.
  • Scenarios: Formal scenarios W1 through W31 are fully verified and covered by tests.

Architecture & Design Alignment

  1. Rate Limiting & Challenge Lifecycle:
    • createChallenge rate limiting (5 attempts/min per target wallet) strictly follows OpenSpec D14 / Scenario W5.
    • Deferring cleanup of unconsumed/expired challenges was deliberately scoped out in design.md:157 and tasks.md:30. Recommend a non-blocking follow-up issue for a background cleanup job and evaluating IP-level rate limiting at the reverse proxy layer.
  2. Signature Verification & Atomicity:
    • Sep10Verifier correctly enforces that all client signatures belong to authorized signers on chain (Scenario W11 / rule 5).
    • Atomic consumption in ServerpodChallengeStore.consume checks expiresAt > now inside the database transaction, preventing race conditions and replay attacks.
  3. Session & Endpoint Integration:
    • Clean transition to WalletSessionWallet resolving the bound wallet from wallet_account. Replaces the template email IDP cleanly.

Minor Non-blocking Follow-ups

  • puls3_server/README.md: Contains minor stale documentation referencing FailClosedSessionWallet as active in production and emailSecretHashPepper (lines 77-80, 407). Can be updated in a follow-up doc polish.
  • WalletAuthEndpoint._service: Rebuilds TokenManager per request; consider reusing a shared instance in a future optimization pass.

@moises-cisneros
moises-cisneros merged commit 9438435 into main Oct 9, 2026
10 checks passed
@moises-cisneros
moises-cisneros deleted the feat/136-wallet-sessions-01-server branch October 9, 2026 20:27
moises-cisneros added a commit that referenced this pull request Oct 10, 2026
…e with challenge checks, session on the Serverpod client) (#149)

* feat(flutter): sign in with the wallet (SEP-10) before a real hire

Since #147 the server only accepts hire calls from a wallet-bound session.
The app only read the Freighter address, so with HIRE=server every hire
ended in "Sign-in with your wallet is not available yet". It also could not
sign the challenge: FreighterWallet.signTransaction requires every source to
be the connected account (#69), but a SEP-10 challenge's source is the
server.

Wallet:
- WalletPort.signChallenge(SignInChallenge), with assertSep10Challenge:
  sequence 0 (never submittable), only manage_data operations, the first one
  "<home domain> auth" from the connected account, every other one from the
  server (the transaction source, never the account), time bounds that
  include now, and the server key and home domain when the app knows them.
  After signing, the body must be unchanged and carry a new signature from
  the connected account (shared with signTransaction).
- MockWallet and WalletController implement it.

Session:
- WalletSession: createChallenge, wallet signature, verifyChallenge, then
  the AuthSuccess is stored by the client's FlutterAuthSessionManager, which
  sends it on every call. A session is reused only for the wallet that
  opened it; a challenge issued for another wallet is never signed.
- main.dart sets client.authSessionManager and reads serverSigningKey and
  homeDomain from config.json auth when the server publishes them.

Hire flow:
- HireGateway.ensureSignedIn / forgetSession (no-op in the demo). The flow
  signs in after connecting and before createHire (new step signingIn). A
  session the server refuses (NotAuthenticated, AuthenticationUnavailable)
  or a refused sign-in (ChallengeExpired, ChallengeConsumed,
  ChallengeNotFound, InvalidWalletSignature: HireSignInFailed) is dropped,
  so Try again signs in afresh. ChallengeRateLimited maps to "too many
  sign-in attempts".

Tests: SEP-10 challenges built like the server's codec, signed by a real
key through the fake Freighter bridge (valid, and every refusal); session
reuse, wallet switch, wrong-wallet challenge, declined and refused
sign-ins; the sheet signs in before createHire and recovers after a refusal;
readAuthConfig. 267 app tests pass, web release build succeeds, and
breaking each of the 10 guards fails a test.

Refs #136

* feat(flutter): hire detail screen (S06) that polls getHire and shows the agent's result (#150)

- HireGateway.getHire returns HireProgress (from HireDetail): escrow
  status, run status, failure reason, result and the fund tx. HireStage
  derives Funded/Queued/Running/Result ready/Run failed/Submitted/
  Completed/Rejected (Cancelled from open)/Expired; a succeeded run that
  is still `running` with a result reads as Result ready.
- ServerHireGateway maps HireNotFound/HireNotOwned/InvalidHireId to
  HireNotFound and takes the payment link from the fund submission only.
- /hires/:id (HireDetailScreen + HireDetailView): polls every 4 s until
  the hire is final, keeps the last status with a Retry banner when a
  refresh fails, asks to connect or sign in (SEP-10) when needed, copies
  the result, opens the payment on StellarExpert.
- The hire sheet's confirmed state gets "View hire".
- Pages build: PULS3_HIRE sets the HIRE dart-define (server or demo).
- Approve and Reject are not offered: they need the server-signed
  submit (#97).

Refs #28

* fix(env): document PULS3_HIRE read by the Pages build script

---------

Co-authored-by: Moises Cisneros <110249336+moises-cisneros@users.noreply.github.com>
Co-authored-by: moises-cisneros <moisescisnerosdl@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: backend Serverpod endpoints and persistence type: feat New functionality

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants