client.auth
Authentication is exposed through client.auth.zk. See the
Authentication guide for usage.
interface AuthUser { username: string; /** Decimal-string Poseidon commitment — the user's stable id. */ commitment: string;}Methods
Section titled “Methods”register(params)
Section titled “register(params)”register(params: { username: string; password: string; email?: string | null; login?: boolean; // default: true}): Promise<AuthUser>Derives the identity, registers the commitment, and (unless login: false)
signs in. Throws if the username is taken.
login(username, password, opts?)
Section titled “login(username, password, opts?)”login(username: string, password: string, opts?: { rememberMe?: boolean }): Promise<AuthUser>Re-derives the identity, proves it against a fresh server challenge, and
establishes the session and identity. rememberMe: true requests a
longer-lived session. Throws on bad credentials / expired challenge.
restore()
Section titled “restore()”restore(): Promise<AuthUser | null>Validates a persisted session token (from the sessionStore). Returns the user
when valid, or null (clearing a stale token). Does not unlock the identity
— call unlock() for encryption/messaging.
unlock(password)
Section titled “unlock(password)”unlock(password: string): Promise<void>Re-derives the identity for an already-authenticated (restored) session. Verifies the password by matching the derived commitment to the session’s; throws on mismatch.
enrollPasskey(opts?)
Section titled “enrollPasskey(opts?)”enrollPasskey(opts?: { rpId?: string; rpName?: string; label?: string }): Promise<void>Adds a WebAuthn passkey (PRF) that wraps the master seed. Signed-in only.
loginWithPasskey(username, opts?)
Section titled “loginWithPasskey(username, opts?)”loginWithPasskey(username: string, opts?: { rememberMe?: boolean }): Promise<AuthUser>Passwordless sign-in via an enrolled passkey. Unwraps the seed and re-derives the identity, establishing session and identity.
passkeyAvailable()
Section titled “passkeyAvailable()”passkeyAvailable(): booleanWhether WebAuthn is usable in this browser (sync).
passkeyPrfAvailable()
Section titled “passkeyPrfAvailable()”passkeyPrfAvailable(): Promise<boolean | null>Whether the authenticator supports the required PRF extension. null means it
can’t be determined. Gate the passkey UI on a true result.
enrollRecoveryPhrase()
Section titled “enrollRecoveryPhrase()”enrollRecoveryPhrase(): Promise<string>Returns a 24-word BIP39 phrase to display once — it is the seed, nothing is stored server-side. Deterministic, so it returns the same words each call. Signed-in only.
recoverWithPhrase(username, mnemonic)
Section titled “recoverWithPhrase(username, mnemonic)”recoverWithPhrase(username: string, mnemonic: string): Promise<AuthUser>“Forgot password” path: re-derives the identity from the phrase and signs in.
Follow with changePassword() to set a new password.
enrollEmailFactor(email)
Section titled “enrollEmailFactor(email)”enrollEmailFactor(email: string): Promise<{ email: string; confirm: (code: string) => Promise<void>;}>Add an email as a recovery + login factor. Sends a one-time code to the
address and returns a confirm continuation — call it with the code from the
inbox to finish enrolling:
const { confirm } = await client.auth.zk.enrollEmailFactor("me@example.com");await confirm(codeFromInbox);Signed-in only (the seed must be held). Verification-gated: the server only releases the split-key OPRF eval after the address is OTP-verified. Disclose the custody trade-off (recoverability vs. custody) in your enroll UI.
recoverWithEmail(username, opts?)
Section titled “recoverWithEmail(username, opts?)”recoverWithEmail(username: string, opts?: { rememberMe?: boolean }): Promise<{ confirm: (code: string) => Promise<AuthUser>;}>“Forgot password” path via an enrolled email factor. Sends a code to the
stored address (the response never reveals whether one exists), then
confirm(code) unwraps the seed and signs in. Follow with changePassword().
const { confirm } = await client.auth.zk.recoverWithEmail("alice");const user = await confirm(codeFromInbox);enrollGoogleFactor(idToken)
Section titled “enrollGoogleFactor(idToken)”enrollGoogleFactor(idToken: string): Promise<void>Link a Google account as a recovery + login factor. idToken is a fresh Google
ID token obtained via Google Identity Services (the SDK never renders Google UI).
Signed-in only.
loginWithGoogle(username, idToken, opts?)
Section titled “loginWithGoogle(username, idToken, opts?)”loginWithGoogle(username: string, idToken: string, opts?: { rememberMe?: boolean }): Promise<AuthUser>Sign in with Google — verifies the ID token, unlocks the seed via the gated
split-key eval, and establishes session and identity. The account must have
linked the factor first (enrollGoogleFactor). Serves both “Sign in with
Google” and “recover with Google”.
registerWithGoogle(username, idToken)
Section titled “registerWithGoogle(username, idToken)”registerWithGoogle(username: string, idToken: string): Promise<AuthUser>Register a brand-new, passwordless account whose only factor is Google. A random master seed is generated and wrapped under the Google-gated key; the user can add a password / passkey / phrase later. ZK registration is identical to a password signup.
changePassword(newPassword)
Section titled “changePassword(newPassword)”changePassword(newPassword: string): Promise<void>Re-wraps the unchanged seed under a new password. Identity and commitment never move. Signed-in only.
listFactors()
Section titled “listFactors()”listFactors(): Promise<Array<{ id: string; type: "password" | "passkey" | "phrase-marker" | "email" | "google"; label?: string; createdAt?: number; masked?: string;}>>Lists the user’s enrolled login methods (metadata only). Gated factors
(email / google) carry a masked display hint (e.g. "m•••@gmail.com");
the full value is never returned.
removeFactor(id)
Section titled “removeFactor(id)”removeFactor(id: string): Promise<void>Removes an enrolled factor. Throws if it’s the last remaining one.
recover()
Section titled “recover()”recover(): Promise<boolean>Silently re-mints a session token by re-proving the in-memory identity — no
password prompt. Resolves true if it succeeded, false if it can’t (the
identity isn’t in memory, e.g. after a reload that kept only the token). The
client calls this automatically when a request hits 401; you normally use the
client-level recoverSession() +
onSessionExpired instead of calling it
directly.
hasPasskeyForThisOrigin()
Section titled “hasPasskeyForThisOrigin()”if (await client.auth.zk.hasPasskeyForThisOrigin()) { /* offer passkey sign-in */ }Whether this account has a passkey that will actually work here. Passkeys are
per-origin, so an account with a passkey enrolled on another host has one that
WebAuthn will refuse on this one. Use this before offering a passkey button —
not listFactors(), which will happily report a passkey you cannot use.
Machine access without the seed
Section titled “Machine access without the seed”For a CLI, a server-side tool, or any process that needs your encryption identity without a person typing a password each time.
enrollDevice(label)
Section titled “enrollDevice(label)”const { factorId, deviceKey } = await client.auth.zk.enrollDevice("ci-runner");Generates a random key, wraps the master seed under it, and stores only the
ciphertext in your vault as a device factor. The key is returned for you to
persist; it is never sent anywhere.
The split is the point: the seed itself never lands on the machine’s disk, and revoking the factor makes the stored key inert immediately — which a copied seed can never be, because nothing can reach out and un-store it.
Treat deviceKey as the credential it is, and store it with the tightest
permissions available.
unlockWithDevice(username, factorId, deviceKey)
Section titled “unlockWithDevice(username, factorId, deviceKey)”await client.auth.zk.unlockWithDevice(username, factorId, deviceKey);Fetches the wrapped seed and opens it locally. Establishes the encryption identity only — pair it with a session for calls that need one.
Throws distinguishably: a revoked factor is gone from the vault and says so, where a wrong key fails to decrypt. A machine whose access was withdrawn should be able to tell you that, not merely report a decryption failure.
listDevices() / revokeDevice(factorId)
Section titled “listDevices() / revokeDevice(factorId)”await client.auth.zk.listDevices(); // [{ id, label, createdAt }]await client.auth.zk.revokeDevice(id); // that machine's key stops workingunlockWithSeed(seedBase64)
Section titled “unlockWithSeed(seedBase64)”client.auth.zk.unlockWithSeed(process.env.MUHKOO_SEED);The inverse of seedBase64, for hosts that hold the seed
themselves. Without it, anything outside a browser must run a full ZK login —
circuit download and proof — on every invocation.
Sets the encryption identity only; it does not authenticate. A persisted seed is
not scoped and cannot be revoked server-side the way a session token or a device
factor can, so prefer enrollDevice where you have the choice.
logout()
Section titled “logout()”logout(): Promise<void>Clears the session, identity, and persisted token.
Getters
Section titled “Getters”| Getter | Type | Description |
|---|---|---|
client.auth.zk.user | AuthUser | null | Current user (sync). |
client.auth.zk.token | string | null | Current session token (sync). |
client.auth.zk.identity | ZkIdentity | null | Derived identity, or null when locked. |
client.auth.zk.seedBase64 | string | null | Master seed as base64, or null when locked. Wrap per-user app data to this so it survives password changes and passkey login. |
client.auth.user | AuthUser | null | Convenience alias. |
Errors
Section titled “Errors”import { VaultUnavailableError } from "@muhkoo/connect";VaultUnavailableError— thrown bylogin/unlockwhen the per-user vault is unreachable (network / 5xx / rate-limit). Distinct from a wrong password — tell the user to retry rather than re-enter credentials.
client.auth.hosted
Section titled “client.auth.hosted”Centralized hosted auth on auth.muhkoo.dev (authorization-code + PKCE). See the
Hosted authentication guide.
login({ appId, redirectUri })—Promise<string>. Stashes a PKCE verifier +stateand redirects the browser to the hosted/authorizepage; resolves to the URL (also returned for popup/test use).redirectUrimust be registered for the app (portal → App Detail → Hosted sign-in).handleCallback()—Promise<{ username, commitment }>. On your callback route: verifiesstate, exchanges the code for a session, unseals the master seed from the URL fragment, establishes the session + identity, and scrubs the URL. Throws on a bad/expired code or PKCE/CSRF mismatch.isCallback()—boolean.truewhen the current URL is a hosted-auth callback (?code&state). Use it to guardhandleCallback().manageAccount({ returnUri? })—Promise<string>. Redirect to centralized account & security management (auth.muhkoo.dev/security), where the user manages passkeys, recovery email, Google, recovery phrase, and password.returnUri(default: the current URL) becomes a “Back to app” button. Wire it to a “Manage account” / settings entry in your app — there’s no per-app security UI to build.
Set authBaseUrl on the Client to override the hosted origin (default
auth.muhkoo.dev).
Device pairing
Section titled “Device pairing”Sign-in for keyboard-less devices (TVs, set-top boxes). Requires
@muhkoo/connect@0.10.13-alpha.0 or later. See the
TV & device pairing guide for the full flow, the
storage caveats, and the error table.
Device side:
startDevicePairing({ appId, label? })—Promise<DevicePairingSession>. Generates the ephemeral keypair and returns{ userCode, verificationCode, verificationUri, verificationUriComplete, expiresAt, interval, deviceCode, privateKey, devicePublicKeyB64 }. DisplayuserCodeandverificationCode.waitForDevicePairing(session, { signal?, onTick? })—Promise<{ username, commitment }>. Polls to completion (withslow_downbackoff), unseals the seed, verifies the commitment, mints the device’s own session, and persists it. Rejects withDevicePairingError.pollDevicePairing(session)—Promise<DevicePairingPoll>. One poll, for custom loops:{ status: "pending" | "slow_down" | "approved" | "denied" | "expired" }.cancelDevicePairing(session)—void. Abandons the attempt; an in-flightwaitForDevicePairingrejects with reasonaborted.resumeDeviceSession()—Promise<{ username, commitment } | null>. Cold start from the persisted identity.nullmeans “pair again”; never throws.hasDeviceSession()—Promise<boolean>. Whether anything is persisted.forgetDeviceSession()—Promise<void>. Wipes the persisted seed, the device identity key, and the session.
Approver side (signed in, seed unlocked):
lookupDevicePairing(userCode)—Promise<DevicePairingRequest>. Resolves a typed code to{ userCode, devicePublicKeyB64, verificationCode, deviceFingerprint, deviceLabel, appId, appLabel, isKnownDevice, requiresReauth, expiresAt }.approveDevicePairing(req, { label? })—Promise<{ deviceId }>. Seals the seed to the device’s key and posts it. ThrowsReauthRequiredErrorwhen a new device needs a fresh sign-in factor.denyDevicePairing(userCode)—Promise<void>. Refuse. No fresh factor needed.listPairedDevices()—Promise<PairedDevice[]>—{ id, label, appId, pairedAt, lastSeenAt, activeSession }.revokePairedDevice(id)—Promise<void>. Removes the device from the roster so it must be re-approved. It does not end the device’s current session (that survives until expiry) and does not take back the encryption key the device already holds.
Errors: DevicePairingError (.reason, .code, .status, .retryAfter,
.details) and ReauthRequiredError (extends it; adds .authAgeSeconds,
.maxAgeSeconds), both exported from @muhkoo/connect.