SIWE Library
@lib/siwe orchestrates the Sign-In with Ethereum (SIWE) flow used by /api/gate-access. It handles nonce generation, message validation, and signature verification so routes only need to focus on business logic.
The modern stack pairs SIWE with the Cloudflare Durable Object state service:
/api/nonce→generateNonce()→stateWorker.nonce:set/api/gate-access→verifyNonce()+verifySiweMessage()→stateWorker.nonce:consumeisSiweEnabled()toggles the entire flow based onNEXT_PUBLIC_ENABLE_STATE_WORKERand the worker secrets.
When SIWE is disabled (e.g., purely local dev), routes fall back to the legacy timestamped signature path.
File Tree
- siwe.server.ts
- siwe.client.ts
- __tests__/siwe.server.test.ts
Configuration Checklist
NEXT_PUBLIC_ENABLE_STATE_WORKER=true
STATE_WORKER_URL=https://ritoswap-state-worker.worker.dev/state
STATE_WORKER_API_KEY=your_state_api_key
NEXT_PUBLIC_DOMAIN=ritoswap.com # Hosts allowed in SIWE messagesIf the flag is false, isSiweEnabled() short-circuits, /api/nonce returns 501, and /api/gate-access enforces the legacy signature flow.
Server Helpers
isSiweEnabled()
Returns true only when:
NEXT_PUBLIC_ENABLE_STATE_WORKERis setserverConfig.stateService.isActive(URL + API key present)isStateServiceEnabled()confirms the Cloudflare client initialized successfully
generateNonce(params)
export async function generateNonce({ identifier, ttlSeconds = 300 }) {
const value = randomBytes(NONCE_BYTES).toString(NONCE_ENCODING);
if (isSiweEnabled()) {
await getStateClient().storeNonce(identifier, value, ttlSeconds);
}
return { value, expiresAt: new Date(Date.now() + ttlSeconds * 1000), identifier };
}When SIWE is disabled it still returns a nonce so clients can continue their flow, but nothing is persisted server-side.
verifyNonce(params)
Consumes and verifies the nonce via the state worker. If the worker is disabled, it yields { isValid: true } to maintain backwards compatibility.
verifySiweMessage(params)
Wraps the official siwe package to validate:
- Domain (derived from
NEXT_PUBLIC_DOMAINor request headers) - Address (
msg.addressvs.params.address) - Nonce
- Time window (issuedAt vs. current time)
- Signature (via
SiweMessage.verify+ downstreamviem.verifyMessagein the route)
Parsed messages are returned so the caller can enforce additional policy (domain allowlists, host binding, etc.).
Client Helpers
siwe.client.ts exposes browser-safe utilities:
isSiweEnabled()– readsNEXT_PUBLIC_ENABLE_STATE_WORKERgetDomain()/getUri()– consistent domain + URI formattingcreateSiweMessage(params)– EIP-4361 formatter
Use them alongside the client-side signing helpers in @lib/client/signing to avoid drift between front end and back end.
Flow Summary
1. Request Nonce
GET /api/nonce → generateNonce → Durable Object persists value with 5-minute TTL.
2. Build SIWE Message
Client uses createSiweMessage and buildEnvelope to prepare the EIP-4361 payload.
3. Sign
Wallet signs the message; on mobile, the UI deep links into the WalletConnect session.
4. Exchange
/api/gate-access verifies the nonce, SIWE message, and on-chain ownership before delivering content and minting a JWT.
Every SIWE signature is also re-verified with viem.verifyMessage in the route for belt-and-suspenders protection.