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
1
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.2
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.3
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.4
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.5
Link the account
The callback creates (or links) the Pinaivu account, binding the email
and Sui address, then redirects into the console.
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:
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).
Dependencies:
@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