Skip to content

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.

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.

#TitleOutcome
1Auth Store Boundary And MigrationAuthNonce, AuthSession types; four Store interface methods; SQLStore, unavailableStore, and fakeStore impls; Knex migration 20260507000000_create_auth_tables.js.
2Auth Nonce And Login Handlersinternal/pulse/auth.go with EIP-191 verification, opaque token generation, SHA-256 hashing; routes GET /api/auth/nonce and POST /api/auth/login.
3Auth Middleware And Me EndpointrequireAuth wrapper + AuthedWalletFromContext helper in middleware.go; GET /api/auth/me mounted; eight integration tests.
4Final Verification And Root CommitFull test suite green; binary builds; root submodule pointer + plan committed.

A code review flagged two items after the slice landed; both were fixed in commit 5729905:

  • crypto/rand.Read errors 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-future issuedAt. The check now rejects both.
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.js
  • No Flutter wiring. Sign-in from the app is a follow-up slice.
  • No retroactive gating of existing routes. Only /api/auth/me uses requireAuth as a demonstrable protected route.
  • No SIWE library, no JWT secret, no logout endpoint, no automatic refresh.
  • No raw-token persistence — only SHA-256 hashes.

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.