pinaivu-api/dashboard) authenticates
users with Sui zkLogin. Instead of a seed phrase or a browser wallet, a
developer signs in with their existing Google account and receives a real,
self-custodied Sui address — derived from the OAuth identity through a
zero-knowledge proof. The OAuth provider never learns the user’s Sui
address, and Pinaivu never sees a private key.
This page documents how that flow is wired end-to-end, the exact files and
endpoints involved, and the security caveats worth knowing before you ship
it.
Why zkLogin
A developer console needs an on-chain identity (so usage, keys, and payments can bind to a Sui address) but the audience is developers who do not want to manage a wallet just to create an API key. zkLogin closes that gap:- Seedless — the address is a function of the OAuth
sub+ a salt, proven in zero knowledge. There is no mnemonic to store or lose. - Self-custodied — every transaction is still authorised by an ephemeral key the browser generates and holds; the proof binds that ephemeral key to the user’s address for a bounded number of epochs.
- Private — the link between the Google account and the Sui address is hidden behind the ZK proof. The chain sees a normal Sui address.
Where it lives
response_type=id_token).
The flow
Initialize a session (/login)
ZkLoginService.initializeSession() mints a fresh ephemeral
Ed25519 keypair, sends its public key to the Enoki nonce endpoint
(NEXT_PUBLIC_ENOKI_NONCE_URL) with additionalEpochs: 2, and stores
the returned { nonce, randomness, maxEpoch } plus the ephemeral
private key in localStorage under pinaivu-zklogin-session.maxEpoch is the Sui epoch after which both the nonce and any proof
bound to this ephemeral key stop being valid — the practical session
lifetime.Redirect to Google
The browser is sent to Google’s OIDC endpoint requesting an
id_token (a JWT) whose nonce claim is the value Enoki returned.
The nonce cryptographically commits the JWT to this ephemeral key, so
a stolen JWT can’t be replayed against a different key.Fetch the ZK proof (/auth/callback)
Google redirects back with
#id_token=<JWT> in the URL fragment. The
callback recreates the ephemeral keypair from the saved session and asks
Enoki to produce the zkLogin proof, passing the JWT in the
zklogin-jwt header.Derive the address and persist the proof
The zkLogin Sui address is derived from the JWT + salt (or from the
proof’s
addressSeed). The full proof bundle is cached in
localStorage under pinaivu-zklogin-proof with a 24-hour TTL.Signing Sui transactions
When the user later authorises an on-chain action, the ephemeral key signs the transaction bytes andZkLoginService.createTransactionSignature
wraps that signature together with the ZK proof and the addressSeed into a
single zkLogin signature that Sui validators accept:
addressSeed comes straight from the proof when present, otherwise it is
recomputed with genAddressSeed(BigInt(userSalt), "sub", sub, aud).
Session & route guarding
SessionManager (lib/zklogin/session.ts) keeps two localStorage keys:
| Key | Contents | Lifetime |
|---|---|---|
pinaivu-zklogin-session | ephemeral private key, randomness, maxEpoch, nonce | until the callback completes (then cleared) |
pinaivu-zklogin-proof | zkProof, JWT, address, salt, email | 24 h TTL, auto-expired on read |
DashboardShell treats /login and /auth/callback as public and
redirects every other route to /login when no valid cached proof exists.
“Sign out” calls SessionManager.clearAll().
Environment variables
These are required by the dashboard for zkLogin to work. TheNEXT_PUBLIC_ prefix means they are exposed to the browser (Enoki API keys
used here are publishable client keys, not server secrets).
| Variable | Purpose |
|---|---|
NEXT_PUBLIC_GOOGLE_CLIENT_ID | Google OAuth client id (OIDC provider) |
NEXT_PUBLIC_ENOKI_API_KEY | Enoki publishable key (Bearer auth to Enoki) |
NEXT_PUBLIC_ENOKI_NONCE_URL | Enoki nonce endpoint (step 1) |
NEXT_PUBLIC_ENOKI_ZKP_URL | Enoki ZK proving endpoint (step 3) |
NEXT_PUBLIC_SUI_NETWORK | testnet (default) / mainnet |
@mysten/sui (/zklogin + /keypairs/ed25519) and
jwt-decode.
Troubleshooting
- “Google Client ID not configured.” —
NEXT_PUBLIC_GOOGLE_CLIENT_IDis unset at build time.NEXT_PUBLIC_vars are inlined during the build, so rebuild after setting it. Enoki nonce error/Enoki ZKP error— check theNEXT_PUBLIC_ENOKI_API_KEYand that thenetworkmatches the key’s configured network. The thrown error includes Enoki’s response body.- Redirect loops back to
/login— the cached proof expired (24 h) or was cleared; sign in again.DashboardShellredirects wheneverSessionManager.getProof()returnsnull. No active session.in the callback — the user reached/auth/callbackwithout first going through/login(the ephemeral session inlocalStorageis missing).
Why Sui
Why zkLogin and on-chain settlement are built on Sui specifically
Glossary
zkLogin, ephemeral key, salt, maxEpoch, and other terms