Plan: Wallet Auth Sessions
Status: Shipped 2026-05-07. Source: docs/superpowers/plans/2026-05-07-wallet-auth-sessions.md. User-facing reference: Authentication.
Stand up server-side wallet authentication on pulse-server (nonce → EIP-191 personal_sign → opaque bearer session) so future private routes can identify the authenticated wallet without storing user secrets, custody, approvals, or signing power.
Architecture
Section titled “Architecture”Two-step EIP-191 personal_sign flow modeled on the existing comments slice. The server issues per-wallet single-use nonces, verifies signed login messages with go-ethereum/crypto.SigToPub, and mints opaque random session tokens whose SHA-256 hashes are persisted in PostgreSQL. A requireAuth middleware wrapper validates Authorization: Bearer <token>, looks up the live session by hash, and attaches the authenticated wallet to the request context. GET /api/auth/me exercises the middleware end-to-end.
| # | Title | Outcome |
|---|---|---|
| 1 | Auth Store Boundary And Migration | AuthNonce, AuthSession types; four Store interface methods; SQLStore, unavailableStore, and fakeStore impls; Knex migration 20260507000000_create_auth_tables.js. |
| 2 | Auth Nonce And Login Handlers | internal/pulse/auth.go with EIP-191 verification, opaque token generation, SHA-256 hashing; routes GET /api/auth/nonce and POST /api/auth/login. |
| 3 | Auth Middleware And Me Endpoint | requireAuth wrapper + AuthedWalletFromContext helper in middleware.go; GET /api/auth/me mounted; eight integration tests. |
| 4 | Final Verification And Root Commit | Full test suite green; binary builds; root submodule pointer + plan committed. |
Post-review fixes
Section titled “Post-review fixes”A code review flagged two items after the slice landed; both were fixed in commit 5729905:
crypto/rand.Readerrors were silently swallowed, which could have produced all-zero tokens. Errors now propagate as 500.- The login window check accepted reversed timestamps (
expiresAt < issuedAt) and far-futureissuedAt. The check now rejects both.
Files of interest
Section titled “Files of interest”server-arenaton/├── internal/pulse/│ ├── auth.go # handlers + helpers│ ├── auth_test.go # 10 tests (5 nonce/login, 3 me, 2 window-hardening)│ ├── middleware.go # requireAuth, AuthedWalletFromContext│ └── store.go # AuthNonce, AuthSession, SQLStore + unavailableStore impls└── db/migrations/ └── 20260507000000_create_auth_tables.jsNon-goals (intentional)
Section titled “Non-goals (intentional)”- No Flutter wiring. Sign-in from the app is a follow-up slice.
- No retroactive gating of existing routes. Only
/api/auth/meusesrequireAuthas a demonstrable protected route. - No SIWE library, no JWT secret, no logout endpoint, no automatic refresh.
- No raw-token persistence — only SHA-256 hashes.
What’s next
Section titled “What’s next”The natural follow-up is the Flutter sign-in flow: nonce fetch → Reown personal_sign → token storage in flutter_secure_storage → bearer attached to API client. That slice was brainstormed but paused in favor of the Polymarket deposit-wallet onboarding, which the user prioritized as a more pressing concern.