ZKAuth
Get started

Reference

SDK reference

The published zkauth-client@2.0.0-beta.5 package exportsZKAuthSDK and runs Argon2id, Poseidon, and Groth16 proof generation in the client process. The Proof V2 path uses explicit development artifacts, is not production approved, and keeps the returned session in memory.

Client and framework helpers

The checked-out zkauth-client beta is the crypto-capable JavaScript client for Proof V2 registration, login, request-bound QR pairing, and the Access Pass experiment.

Server helpers: @zkauth/node, @zkauth/nextjs, @zkauth/express, and @zkauth/hono.
UI helper: @zkauth/react for app-owned React flows.
Use direct API mode from trusted server code, or hosted proxy mode for browser-hosted flows that should not receive a project API key.

Constructor

Pick one trust boundary: direct API mode with a server-side project key, or hosted proxy mode with a project slug and public client ID. Both modes keep password-derived proof generation on the client.

ts
import { ZKAuthSDK } from 'zkauth-client'
const zkauth = new ZKAuthSDK({  apiKey: loadServerOnlyProjectKey(), // direct API / server-side mode  baseUrl: 'https://api.zkauth.dev', // optional  timeout: 30000, // optional, ms  debug: false,   // optional})
Hosted/passkey golden pathts
import { ZKAuthSDK } from 'zkauth-client'
const zkauth = new ZKAuthSDK({  hostedProxy: {    baseUrl: 'https://zkauth.dev',    projectSlug: 'your-project-slug',    clientId: 'your_public_client_id',  },})
const options = await zkauth.webAuthnAuthenticationOptions({ userId })// Your browser UI converts options and runs navigator.credentials.get(...).const login = await zkauth.webAuthnAuthenticationVerify({  userId,  response: credentialResponse,  deviceInfo,})
Proof V2 Lab golden pathts
import { preloadProofArtifacts, ZKAuthSDK } from 'zkauth-client'
const artifacts = {  wasm: process.env.AUTH_V2_WASM_URL!,  zkey: process.env.AUTH_V2_ZKEY_URL!,}
const zkauth = new ZKAuthSDK({  apiKey: process.env.ZKAUTH_API_KEY!,  baseUrl: process.env.ZKAUTH_BASE_URL ?? 'http://127.0.0.1:3000',  experimentalProofV2Artifacts: artifacts,})
await preloadProofArtifacts(artifacts)await zkauth.register({ email, password, deviceInfo })// Complete email verification before login.await zkauth.login({ email, password, deviceInfo })
server callback handoffts
import { redeemZKAuthHostedHandoff } from '@zkauth/node'
export async function GET(request: Request) {  const url = new URL(request.url)  const code = url.searchParams.get('code')  const handoff = await redeemZKAuthHostedHandoff({    apiKey: loadServerOnlyProjectKey(),    baseUrl: loadServerOnlyApiBaseUrl(),  }, code)
  // Store handoff.sessionToken in your own HttpOnly, Secure, host-only cookie.  // Do not expose it in the URL or client-side storage.  return Response.redirect(new URL('/dashboard', request.url))}
server machine introspectionts
import { introspectZKAuthMachineToken } from '@zkauth/node'
const machine = await introspectZKAuthMachineToken(  {    apiKey: loadServerOnlyProjectKey(),    baseUrl: loadServerOnlyApiBaseUrl(),  },  {    audience: 'zkauth:client:00000000-0000-4000-8000-000000000001',    issuer: 'https://api.zkauth.dev',  },)
if (!machine.active) {  throw new Error('ZKAuth machine token is not active')}
OptionTypeNotes
apiKeystringRequired for direct API mode. Keep it server-side and omit it when hostedProxy is configured.
baseUrlstringEngine URL for direct API mode. Defaults to the hosted engine.
timeoutnumberRequest timeout in ms (default 30000).
debugbooleanVerbose logging.
clientIdstringOptional; fetched automatically if omitted.
hostedProxy{ projectSlug, clientId, baseUrl?, proxyPath? }Browser-safe mode that routes engine calls through ZKAuth hosted proxy routes. The proxy injects the project key server-side.
experimentalAccessPassPublicClientIdstringEnables keyless public-client Access Pass challenge, proof submission, and introspection. It cannot be combined with an API key or hosted proxy and cannot enroll credentials or fetch paths.
experimentalAccessPassArtifacts{ wasm, zkey }Explicit development-only Access Pass artifacts. The proof path fails closed when they are omitted.
experimentalProofV2Artifacts{ wasm, zkey }Required for password-proof login and QR pairing. Development/research only; the package does not ship proving keys and fails closed when these artifacts are omitted.

Hosted proxy boundary

The hosted proxy does not support Access Pass. It cannot preserve the relying application's exact Origin, so adding Access Pass routes to that allowlist would bind the ceremony to the wrong audience.

Hosted proxy mode is an SDK transport option, not full hosted UI parity. ZKAuth-hosted sign-up/sign-in helper pages can register accounts and verify engine login, including MFA, MFA enrollment-required, or device-approval-required states.

For project-required MFA on an unenrolled account, hosted sign-in can use the short-lived enrollment token only for TOTP setup and verification.

Hosted sign-in can return a single-use handoff code to an allowlisted callback; redeem it from trusted server code with @zkauth/node, then create your own app session cookie.

The hosted account security helper can use the bearer token in component memory for profile, account stats, recent activity, sessions, devices, recovery, TOTP, MFA, and hosted-origin passkey registration, safe metadata inventory, and revocation. Passkey mutations require a recent verified sign-in. Revocation attempts a credential-free email after deletion without making deletion depend on provider availability.

Recovery-code export uses shown-once values already in memory.
Session-wide termination of other active sessions requires a fresh first-factor engine session.
Hosted recovery and reset completion remain narrower surfaces. Hosted passkey sign-in can create a policy-bound engine session and the same single-use application handoff as hosted proof sign-in.

Server machine tokens

@zkauth/node exposes introspectZKAuthMachineToken() and client.introspectMachineToken() for trusted server code.

  • They introspect the configured project API key as an opaque machine token.
  • They return sanitized client and key metadata.
  • They accept only optional issuer/audience assertions.

Opaque-only today

Machine-token support is project API-key self-introspection. It is not OAuth client credentials, JWT bearer-token issuance, local JWT verification, or user-created API keys.

Integration quickstarts

Choose the package for the boundary you own. Browser code uses app or hosted proxy routes. Server code can verify sessions and inject project keys without exposing them to users.

# Direct SDK or server workernpm install zkauth-client@betaimport { ZKAuthSDK } from "zkauth-client"
boundaryServer-side direct API key, or browser hosted proxy mode.

Authentication

register(params)

Derives a salt and commitment from the password on the client and registers the user. deviceInfo is required.

ts
const res = await zkauth.register({  email: 'ada@example.com',  password: 'SecurePassword123!',           // min 8 chars, stays on device  deviceInfo: { deviceName: 'Chrome on Mac', deviceType: 'desktop' },})// res.data -> { userId, email, emailVerified, verificationToken?, deviceId }

login(params)

Requests a short-lived V2 ceremony, generates a Groth16 proof in the client process, and exchanges it for a session through an atomic one-time consume. The token is returned and also stored on the instance.

ts
const res = await zkauth.login({  email: 'ada@example.com',  password: 'SecurePassword123!',  deviceInfo: { deviceName: 'Chrome on Mac', deviceType: 'desktop' },})// res.data -> { user, session, mfaRequired?, deviceApprovalRequired? }const token = res.data.session.token

Experimental proof artifacts

The V2 proving keys are zero-contribution development artifacts. Keep password-proof login and QR pairing disabled in production until independent cryptographic review and a production phase-2 ceremony are complete. Prefer passkeys as the primary production method.

Session helpers

ts
await zkauth.getCurrentUser()  // -> User | nullzkauth.isAuthenticated()       // -> booleanzkauth.getSession()            // -> Session | nullawait zkauth.logout()          // clears the in-memory session

verifyEmail(token) / getSalt(email)

ts
// register() may return data.verificationToken in devawait zkauth.verifyEmail(token)

Request-bound QR pairing

A pending QR request is exchanged for a short-lived Proof V2 ceremony bound to that exact request. Completion verifies the client proof and atomically consumes both one-time records before creating device and session state. It does not fall back to the legacy proof flow.

ts
const ceremony = await zkauth.issuePairingChallenge({  pairingToken,  email,})
const pairing = await zkauth.completePairing({  pairingToken,  email,  password,  deviceInfo: { ...deviceInfo, deviceFingerprint },  ceremony,})
if (pairing.outcome !== 'authenticated') {  // Continue with pairing.token and pairing.availableMethods.  // Pairing never marks MFA verified by itself.}

Pairing is a first factor

An authenticated pairing result still has mfaVerified: false. Handle mfa_required andmfa_enrollment_required as continuations, never as an authenticated application session.

Experimental ZK Access Pass

Access Pass is a separate anonymous-authorization experiment. It proves current tenant Merkle membership and a hidden role threshold for one exact origin and action, then returns a short-lived opaque capability and pairwise pseudonym without returning identity fields.

ts
// Trusted issuer backend: the issuer controls the role and provisions// issuerGeneratedFieldSecret to the holder through its secure workflow.const projectSdk = new ZKAuthSDK({  apiKey: loadServerOnlyProjectKey(),  clientId: publicClientId,  baseUrl: 'http://127.0.0.1:3000',})const enrollment = await projectSdk.enrollAccessCredential({  organizationId,  credentialSecret: issuerGeneratedFieldSecret,  roleTier: issuerAssignedRoleTier,  credentialVersion: 1,})const context = await projectSdk.requestAccessPassProveContext({  organizationId,  credentialId: enrollment.credentialId,})
// Separate browser/public client: no project API key.const publicSdk = new ZKAuthSDK({  baseUrl: 'http://127.0.0.1:3000',  experimentalAccessPassPublicClientId: publicClientId,  experimentalAccessPassArtifacts: { wasm: wasmUrl, zkey: zkeyUrl },})const ceremony = await publicSdk.requestAccessPassChallenge({  organizationId,  audience: window.location.origin,  action: 'reports:read',  minimumRoleTier: 2,})const proof = await publicSdk.generateAccessPassProof({  organizationId,  credentialId: enrollment.credentialId,  credentialSecret: issuerGeneratedFieldSecret,  roleTier: issuerAssignedRoleTier,  credentialVersion: 1,  ceremony,  context,})const capability = await publicSdk.verifyAccessPass({  organizationId,  audience: window.location.origin,  ceremony,  ...proof,})

Issuer and deployment boundary

The credential issuer must control the role embedded in the enrolled leaf. Never accept a holder-selected role during enrollment.

This surface is included in the published SDK beta and still requires a development engine plus development-only artifacts. It is not deployed, production approved, independently audited, or a replacement for login, MFA, federation, recovery, or application sessions.

Password reset

A reset can require approval depending on your project’s policy.

ZKAuth can start the request from a project-bound hosted forgot-password page and complete the token-based reset from the hosted reset-password page without exposing project API keys to browser code.

  • Hosted reset completion derives verifier material in the browser and does not create an application session.
  • Your app can still handle reset callbacks and call resetPassword directly when you own the reset UI.
ts
await zkauth.forgotPassword({ email: 'ada@example.com' })await zkauth.verifyResetToken(token)await zkauth.resetPassword({ token, newPassword: 'NewSecret123!', email })

Devices

List, trust, verify, and remove devices for the authenticated user.

ts
await zkauth.getDevices()                       // -> Device[]const device = await zkauth.registerDevice({ deviceInfo }) // keep device.deviceSecret securelyawait zkauth.verifyDevice({ deviceId, deviceSecret })await zkauth.removeDevice(deviceId)

MFA

getMFAStatus() -> { mfaEnabled, totpEnabled, backupCodesGenerated, availableMethods }.

ts
import { ZKAuthError, ZKAuthErrorCode } from 'zkauth-client'
try {  await zkauth.login({ email, password, deviceInfo })} catch (e) {  if (e instanceof ZKAuthError && e.code === ZKAuthErrorCode.MFA_REQUIRED) {    await zkauth.verifyMFA({      token: e.details.token,      code: '123456',      codeType: 'totp', // or 'backup'    })  }}

TOTP setup, backup-code generation, session management, and the transparency endpoints aren’t wrapped by the client yet; call them over the HTTPS API with your session token.

OPAQUE & WebAuthn helpers

OPAQUE remains a thin protocol transport. The WebAuthn helpers support policy-bound sign-in for a known user ID and authenticated passkey registration through your proxy:

  • opaqueStatus(), opaqueRegistrationResponse(), opaqueRegistrationFinish(), opaqueLoginStart(), opaqueLoginFinish()
  • webAuthnRegistrationOptions(), webAuthnRegistrationVerify(), webAuthnAuthenticationOptions(), webAuthnAuthenticationVerify()

Call webAuthnRegistrationOptions() and webAuthnRegistrationVerify() only after authentication; the SDK sends the in-memory bearer session, and the engine ignores body-supplied user or tenant identity. A successful webAuthnAuthenticationVerify() stores the new session in SDK memory and preserves MFA, device-approval, and enrollment continuations as typed errors.

Events

Subscribe to lifecycle events: login, logout, register, session_expired, mfa_required, device_approval_required, error.

ts
const off = zkauth.on('login', (user) => console.log('signed in', user.email))zkauth.on('session_expired', () => redirectToLogin())off() // unsubscribe

Errors

Failures throw a ZKAuthError with a code from ZKAuthErrorCode (e.g. AUTHENTICATION_ERROR, VALIDATION_ERROR, SESSION_EXPIRED, UNAUTHORIZED).

ts
import { ZKAuthError, ZKAuthErrorCode } from 'zkauth-client'
try {  await zkauth.login({ email, password })} catch (e) {  if (e instanceof ZKAuthError && e.code === ZKAuthErrorCode.AUTHENTICATION_ERROR) {    // wrong credentials / failed proof  }}

Wire it into a framework in Examples.