Zero Accessby Railmandocs

Plugin contract

HTTP endpoints, auth requirements, and M6 denylist for zero-access plugins.

Plugin contract

Short reference. Canonical tables and bodies: API & specs. Per-package setup: Packages.

Status: Binding for implementers · Maturity: 0.0.x

Paths sit under your Better Auth basePath (often /api/auth).

Composition

const passkeyStack = enhancePasskey(passkey, {
  rpID,
  origin,
  assertCredentialAccess, // fn | "session" | "deny"
});

plugins: [
  ...passkeyStack.plugins,
  loginFactors(),
  elevate({
    deploymentMode: "single-instance",
    passkeyCounterGuard: passkeyStack.controller,
    passwordOnlyIfNoStronger: true,
    elevatedTtlSec: 300,
  }),
  zeroAccess({
    deploymentMode: "single-instance",
  }),
];

Product vault (browser):

import { defineZeroAccessPasskeyCryptoConfig } from "@railman/auth-zero-access-passkey/client";
import { createVaultClient } from "@railman/zero-vault";

const cryptoConfig = defineZeroAccessPasskeyCryptoConfig({ salt });
createVaultClient({ userId, passkeyOptions: cryptoConfig });

Never unlock MEK because elevate succeeded.

Elevate

MethodPathAuthNotes
GET/elevate/methodssessionAvailable factors
GET/elevate/passkey/optionssessionWebAuthn options for step-up
POST/elevate/verifysessionMint/update elevate claim

Claim carries userId, elevatedAt, amr[], and binds the live Better Auth sessionToken.
Default TTL 300s. Extending elevation requires a new successful verify — there is no detached one-shot mint/consume API.

Verify methods: password (credential hash), TOTP (two-factor secret), passkey (WebAuthn verify; rpID/origin from elevate or composition).

import { requireElevate } from "@railman/auth-elevate";

await requireElevate(session.session.elevateClaim, {
  userId: session.user.id,
  sessionToken: session.session.token,
  secret: process.env.BETTER_AUTH_SECRET!,
  ttlSec: 300,
});

Zero-access (blind store)

Wrap kinds: prf_passkey · password_kdf · recovery_bip39.

Password / recovery wraps require ≥ 600_000 PBKDF2 iterations.

MethodPathAuthNotes
POST/zero-access/meksessionMeta + recovery wrap
GET/zero-access/meksessionAssociations
GET/zero-access/wrap-slotssessionList wraps
POST/zero-access/wrap-slotssessionPut wrap (full-body M6)
POST/zero-access/wrap-slots/revokesessionRevoke
POST/zero-access/account-recovery/enrollsessionMode B public material
POST/zero-access/account-recovery/startnone*Challenge
POST/zero-access/account-recovery/finishnone*Verify + HttpOnly __Host- recovery-session cookie
POST/zero-access/account-recovery/sessionrecovery cookie + originInspect recovery session
POST/zero-access/account-recovery/registration/exchangerecovery cookie + originExchange for __Host-zero_access_recovery_registration
POST / callbackpasskey registration/finalizegrant cookie + originReserve, register, and finalize the grant; no direct session-consume endpoint

* Rate-limited and purpose-scoped; still unauthenticated HTTP but not “open.”

Recovery bearers are cookie-only. They are never returned in JSON or stored in sessionStorage. Bearer lookup uses the internal storage-ID HMAC family, not plain SHA-256.

M6 denylist

Reject bodies that include raw secret keys (case-insensitive), including:

mnemonic, recoveryPhrase, rawMek, masterKey, prfSecret, plaintext, seed, privateKey, authSecret, …

Wrap puts scan the full request body before snapshotting allowlisted fields.

Client libraries

ConcernImport
Init / wrap / unlock helpers@railman/auth-zero-access/client
Composition + PRF crypto config@railman/auth-zero-access-passkey (+ /client for salt)
Product vault controller@railman/zero-vault
requireElevate@railman/auth-elevate
requireAccess / opener@railman/auth-login-factor

On this page