From sui-dev-agents
Implements zkLogin on SUI — OAuth login (Google, Facebook, Apple, Twitch) with zero-knowledge proofs for privacy-preserving authentication. Useful for social login without wallet extension.
How this skill is triggered — by the user, by Claude, or both
Slash command
/sui-dev-agents:sui-zkloginThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
**OAuth-based wallet authentication with zero-knowledge proofs.**
OAuth-based wallet authentication with zero-knowledge proofs.
Targets: @mysten/sui 2.17.0 (^2.0). Tested: 2026-05-21.
Compatibility notes: The zklogin API lives at @mysten/sui/zklogin. The old @mysten/zklogin package is deprecated and merged into @mysten/sui — if you see Cannot find module '@mysten/zklogin', install only @mysten/sui@^2. There is no ZkLoginProvider class; the API is functional.
zkLogin lets users:
(iss, aud, sub, salt)@mysten/sui/zklogin)import {
generateRandomness,
generateNonce,
getExtendedEphemeralPublicKey,
jwtToAddress,
computeZkLoginAddress,
genAddressSeed,
getZkLoginSignature,
decodeJwt,
} from '@mysten/sui/zklogin';
There is no ZkLoginProvider, no .getLoginUrl(), no .getProof(). You drive the OAuth redirect yourself and call Mysten's prover service over HTTP.
1. ephemeral keypair (Ed25519) + maxEpoch + randomness → nonce
2. redirect to OAuth provider with nonce in `nonce` param
3. receive JWT (id_token)
4. jwt + user salt → zkLogin address
5. POST {jwt, extendedEphemeralPublicKey, maxEpoch, jwtRandomness, salt, keyClaimName} → prover → ZK proof
6. sign tx digest with ephemeral keypair
7. getZkLoginSignature({inputs: {...proof, addressSeed}, maxEpoch, userSignature}) → serialized signature
8. submit tx with that signature
import { Ed25519Keypair } from '@mysten/sui/keypairs/ed25519';
import { SuiGrpcClient } from '@mysten/sui/grpc';
import {
generateNonce,
generateRandomness,
getExtendedEphemeralPublicKey,
} from '@mysten/sui/zklogin';
const suiClient = new SuiGrpcClient({
network: 'devnet',
baseUrl: 'https://fullnode.devnet.sui.io:443',
});
const ephemeral = Ed25519Keypair.generate();
const { systemState } = await suiClient.core.getCurrentSystemState();
const epoch = systemState.epoch;
const maxEpoch = Number(epoch) + 2; // valid for ~2 epochs
const randomness = generateRandomness(); // string
const nonce = generateNonce(ephemeral.getPublicKey(), maxEpoch, randomness);
// Persist these — you need them after the OAuth redirect.
sessionStorage.setItem('zk_ephemeral', ephemeral.getSecretKey());
sessionStorage.setItem('zk_maxEpoch', String(maxEpoch));
sessionStorage.setItem('zk_randomness', randomness);
// @check:skip
const params = new URLSearchParams({
client_id: GOOGLE_CLIENT_ID,
redirect_uri: 'http://localhost:3000/callback',
response_type: 'id_token',
scope: 'openid email',
nonce, // critical
});
window.location.href =
`https://accounts.google.com/o/oauth2/v2/auth?${params}`;
// @check:skip
import { jwtToAddress, decodeJwt } from '@mysten/sui/zklogin';
const jwt = new URLSearchParams(window.location.hash.slice(1)).get('id_token')!;
// Salt should be fetched from your salt service (per-user, secret).
// For demos a fixed salt is fine; production needs per-user salts.
const userSalt = await fetchSaltForUser(jwt); // string or bigint
const address = jwtToAddress(jwt, userSalt, /*legacy*/ false);
// @check:skip
const ephemeral = Ed25519Keypair.fromSecretKey(
sessionStorage.getItem('zk_ephemeral')!,
);
const maxEpoch = Number(sessionStorage.getItem('zk_maxEpoch'));
const randomness = sessionStorage.getItem('zk_randomness')!;
const extendedEphemeralPublicKey = getExtendedEphemeralPublicKey(
ephemeral.getPublicKey(),
);
const proofRes = await fetch('https://prover-dev.mystenlabs.com/v1', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
jwt,
extendedEphemeralPublicKey,
maxEpoch,
jwtRandomness: randomness,
salt: userSalt,
keyClaimName: 'sub',
}),
});
const partialZkLoginSignature = await proofRes.json();
// → { proofPoints, issBase64Details, headerBase64 }
// @check:skip
import { Transaction } from '@mysten/sui/transactions';
import { genAddressSeed, getZkLoginSignature } from '@mysten/sui/zklogin';
import { decodeJwt } from '@mysten/sui/zklogin';
const tx = new Transaction();
tx.setSender(address);
// ...tx.moveCall(...) etc.
const { bytes, signature: userSignature } =
await tx.sign({ client: suiClient, signer: ephemeral });
const decoded = decodeJwt(jwt);
const addressSeed = genAddressSeed(
BigInt(userSalt),
'sub',
decoded.sub!,
decoded.aud as string,
).toString();
const zkLoginSignature = getZkLoginSignature({
inputs: { ...partialZkLoginSignature, addressSeed },
maxEpoch,
userSignature,
});
const result = await suiClient.core.executeTransaction({
transaction: bytes,
signature: zkLoginSignature,
});
No special Move code is needed. zkLogin addresses are regular SUI addresses — tx_context::sender(ctx) returns them like any other.
public fun create_profile(name: String, ctx: &mut TxContext) {
let user = tx_context::sender(ctx); // works with zkLogin
// ...
}
randomness (and therefore nonce) per login attempt.maxEpoch passes.import { ZkLoginProvider } from '@mysten/zklogin' — both the symbol and the package are wrong.
@mysten/sui@^2, import from @mysten/sui/zklogin, use the functional API above.Skipping extendedEphemeralPublicKey when calling the prover.
getExtendedEphemeralPublicKey(ephemeral.getPublicKey()), not the raw key.Using jwt.sub directly as addressSeed.
genAddressSeed(salt, 'sub', sub, aud) — a Poseidon hash. Using the raw sub gives the wrong address.Forgetting to call tx.setSender(address) before signing.
Reusing maxEpoch past expiry.
maxEpoch, every signature fails. Refresh the ephemeral key + nonce + JWT.@mysten/sui/zkloginnpx claudepluginhub first-mover-tw/sui-dev-agents --plugin sui-dev-agentsProvides behavioral guidelines to reduce common LLM coding mistakes, focusing on simplicity, surgical changes, assumption surfacing, and verifiable success criteria.
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.