Architecture
Package map, responsibilities, and what does not belong in the key store.
Architecture
Package map
| Package | Role |
|---|---|
@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-factor | Login-factor stamps and server-constant requireAccess maps. |
@railman/zero-vault | Product vault UX — create vault, daily unlock, recovery reset. Not a Better Auth plugin. Pass passkeyOptions from defineZeroAccessPasskeyCryptoConfig. |
@railman/zero-e2e | X3DH + Double Ratchet primitives that consume a Vault-derived identity seed. Directly WebAuthn-free at the library boundary; layered on Vault in the product. |
auth-* means Better Auth (or BA-adjacent) surface area. Creating a vault is product code, not the store plugin.
Wrap kinds (key store)
All are client-wrapped ciphertext the server validates and stores:
| Kind | Typical KDF | Daily unlock? |
|---|---|---|
prf_passkey | HKDF from WebAuthn PRF | Yes |
password_kdf | PBKDF2-SHA256 (≥ 600_000 iterations) | Yes |
recovery_bip39 | PBKDF2 from BIP-39 seed (≥ 600_000) | Reset only (product policy) |
Key store endpoints (sketch)
Under your Better Auth basePath (for example /api/auth):
| Method | Path | Purpose |
|---|---|---|
POST | /zero-access/mek | Store MEK meta + recovery wrap |
GET | /zero-access/mek | List associations for the session user |
POST | /zero-access/wrap-slots | Put a daily wrap |
GET | /zero-access/wrap-slots | List wraps (?mekId=) |
POST | /zero-access/wrap-slots/revoke | Revoke a slot |
| * | /zero-access/account-recovery/* | Enroll / start / finish / session-to-registration exchange |
Full contract: Plugin contract.
Out of scope for the key-store plugin
| Concern | Owner |
|---|---|
| “Create vault” screens and copy | App / zero-vault |
| WebAuthn login register/assert | enhancePasskey → stock @better-auth/passkey |
| PRF ceremony + tenant salt | Browser defineZeroAccessPasskeyCryptoConfig |
| Sudo window | auth-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({
deploymentMode: "single-instance",
passkeyCounterGuard: passkeyStack.controller,
passwordOnlyIfNoStronger: true,
elevatedTtlSec: 300,
}),
zeroAccess({
deploymentMode: "single-instance",
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-e2eBrowser product path:
import { defineZeroAccessPasskeyCryptoConfig } from "@railman/auth-zero-access-passkey/client";
import { createVaultClient } from "@railman/zero-vault";
const cryptoConfig = defineZeroAccessPasskeyCryptoConfig({ salt });
createVaultClient({ userId, passkeyOptions: cryptoConfig });
// unlock → seal app data → never POST MEKSecurity 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.