Zero Accessby Railman

Architecture

Package map, responsibilities, and what does not belong in the key store.

Architecture

Package map

PackageRole
@railman/auth-zero-access (zeroAccess())Better Auth blind key store. MEK metadata, wrap slots, account recovery, optional E2E relay. Never holds MEK, mnemonic, PRF, or vault password.
@railman/auth-zero-access-passkey (enhancePasskey(passkey, …) + browser crypto config)Passkey counter guard, hardened stock composition, and client PRF helpers (salt/info, ceremony, suites).
@railman/auth-elevate (elevate())Step-up / sudo: short window, elevatedAt + amr, password / TOTP / passkey. Takes passkeyCounterGuard from the composition.
@railman/auth-login-factorLogin-factor stamps and server-constant requireAccess maps.
@railman/zero-vaultProduct vault UX — create vault, daily unlock, recovery reset. Not a Better Auth plugin. Pass cryptoConfig from defineZeroAccessPasskeyCryptoConfig.
@railman/zero-e2eX3DH + Double Ratchet. Identity from Vault. Relay via zeroAccessClient, not a chat plugin.

auth-* means Better Auth (or BA-adjacent) surface area. Vault and chat are additional products: they persist through zeroAccessClient. Client boundaries.

Wrap kinds (key store)

All are client-wrapped ciphertext the server validates and stores:

KindTypical KDFDaily unlock?
prf_passkeyHKDF from WebAuthn PRFYes
password_kdfPBKDF2-SHA256 (≥ 600_000 iterations)Yes
recovery_bip39PBKDF2 from BIP-39 seed (≥ 600_000)Reset only (product policy)

Key store endpoints (sketch)

Under your Better Auth basePath (for example /api/auth):

MethodPathPurpose
POST/zero-access/mekStore MEK meta + recovery wrap (not Mode B enroll)
GET/zero-access/mekList associations for the session user
POST/zero-access/wrap-slotsPut a daily wrap
GET/zero-access/wrap-slotsList wraps (?mekId=)
POST/zero-access/wrap-slots/revokeRevoke a slot
POST/zero-access/account-recovery/enrollMode B public key + possession proof
*/zero-access/account-recovery/start …Challenge, finish, session, registration exchange

Full contract: Plugin contract.

Out of scope for the key-store plugin

ConcernOwner
“Create vault” screens and copyApp / zero-vault
WebAuthn login register/assertenhancePasskey → stock @better-auth/passkey
PRF ceremony + tenant saltBrowser defineZeroAccessPasskeyCryptoConfig
Sudo windowauth-elevate
App domain encryption (notes, files)App (sealBytes helpers optional)

Composition

const passkeyStack = enhancePasskey(passkey, {
  rpID,
  origin,
  assertCredentialAccess: "session", // rung 0; upgrade to a step-up assertion
});

plugins: [
  ...passkeyStack.plugins, // [stock passkey (pinned options), guard plugin]
  loginFactors(),
  elevate({
    passkeyCounterGuard: passkeyStack.controller,
    passwordOnlyIfNoStronger: true,
    elevatedTtlSec: 300,
  }),
  zeroAccess({
    requireRecoveryExportAck: true,
  }),
];

Follow the layered matrix in Use cases. Elevate runs standalone with password/TOTP (omit passkeyCounterGuard); the passkey stack alone hardens login. Vault automatically binds the installed passkey guard by exact identity, and Chat extends the unlocked Vault identity.

Elevate: Better Auth session → fresh password/TOTP/(optional passkey) → approval claim

Vault:   hardened passkey + PRF → local MEK unlock
                                           ↓
Chat:                              Vault identity seed → zero-e2e

Browser product path:

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

const cryptoConfig = defineZeroAccessPasskeyCryptoConfig({ salt });
createVaultClient({ userId, cryptoConfig });
// createVault is local; persistCreatedVault POSTs ciphertext via zeroAccessClient

Storage

Better Auth owns the bindings. Plugins do not take a topology flag or an in-memory session adapter.

ConcernBinding
Sessions, passkeys, MEK metadata, wraps, recoverybetterAuth({ database })
Rate limits, elevate challenges, TOTP replay, rotate noncesbetterAuth({ secondaryStorage })

Labs use sqlite or D1. See Shared storage.

Security tests

Cross-package security regressions live in the monorepo as tests/security/ (not a product package). Prefer pnpm ci:core for the hosted-equivalent gate; pnpm test:security runs the matrix alone.

On this page