API & specs
HTTP endpoints, privilege helpers, client libraries, and M6 denylist.
API & specs
Status: Binding for implementers · Maturity: 0.0.x
Paths sit under your Better Auth basePath (often /api/auth). Package getting started: Packages overview.
Hard rule
Never unlock MEK because elevate succeeded. Never authorize from client-forged claims — verify on the server session row.
Composition sketch
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",
assertAccess: assertZeroAccess,
}),
];Client BA plugins: passkeyClient(), elevateClient(), zeroAccessClient().
Elevate
| Method | Path | Auth | Body | Response |
|---|---|---|---|---|
GET | /elevate/methods | session | — | { available, defaultMethod, enrolled?, … } or elevate_no_method |
GET | /elevate/passkey/options | session | — | WebAuthn authentication options |
POST | /elevate/verify | session | { method, password? | totpCode? | assertion? } | { ok, elevatedAt, expiresAt, amr, … } — UI-only; claim lives on the session row |
Verify methods
| Method | Status |
|---|---|
password | Credential hash |
totp | Decrypts twoFactor.secret + OTP (needs two-factor plugin data) |
passkey | Options + verifyAuthenticationResponse (rpID/origin from elevate or passkey stack) |
Enrollment for policy is server-derived — never from the client body.
Signed claim
HMAC from HKDF(BETTER_AUTH_SECRET, salt="auth-elevate", info="elevate-claim-v1"). Wire: payload.sig. Fields: userId, elevatedAt, firstElevatedAt?, expiresAt / TTL, amr[], opaque sessionBinding. Default TTL 300s. Extending requires a new successful verify — no one-shot mint/consume API.
import { requireElevate } from "@railman/auth-elevate";
// Better Auth getSession bundle: claim lives on session.session
await requireElevate(session.session.elevateClaim, {
secret,
userId: session.user.id,
sessionToken: session.session.token,
ttlSec: 300,
});Package docs: Elevate.
Zero-access
Wrap kinds: prf_passkey · password_kdf · recovery_bip39. Password/recovery wraps require ≥ 600_000 PBKDF2 iterations.
zeroAccess({ secret?: string }) is the only persistent key-authority option. Omit it to capture Better Auth's resolved secret, or set a stable Railman-specific root to separate persistent-data lifecycle. The root derives purpose-separated non-extractable MAC and storage-ID families internally. An installed passkey guard is discovered and bound automatically by exact plugin identity.
| Method | Path | Auth | Notes |
|---|---|---|---|
POST | /zero-access/mek | session | Meta + recovery wrap |
GET | /zero-access/mek | session | Associations |
GET | /zero-access/wrap-slots | session | List wraps (?mekId=) |
POST | /zero-access/wrap-slots | session | Put wrap (full-body M6) |
POST | /zero-access/wrap-slots/revoke | session | Revoke |
POST | /zero-access/account-recovery/enroll | session + assertAccess | Mode B public key + possession proof |
POST | /zero-access/account-recovery/start | none* | Challenge |
POST | /zero-access/account-recovery/finish | none* | Verify + recovery session cookie |
POST | /zero-access/account-recovery/session | recovery cookie + origin | Inspect the recovery session |
POST | /zero-access/account-recovery/registration/exchange | recovery cookie + origin | Exchange for __Host-zero_access_recovery_registration |
POST / callback | passkey registration/finalize | grant cookie + origin | Reserve, register, and finalize the recovery registration grant |
* Rate-limited and purpose-scoped.
Package docs: Zero-access.
E2E relay
Session-gated; bodies M6-checked; persistent rows are MAC-protected.
| Method | Path | Auth | Notes |
|---|---|---|---|
PUT | /zero-access/e2e/prekey | session | Own bundle; identity change needs permit |
GET | /zero-access/e2e/prekey?userId= | session | Attested bundle |
POST | /zero-access/e2e/identity/rotate-request | session | One-time permit for newFp |
POST | /zero-access/e2e/inbox | session | Deliver envelope (contact-gated) |
GET | /zero-access/e2e/inbox?seq= | session | Own inbox |
POST | /zero-access/e2e/contacts | session | Add contact |
POST | /zero-access/e2e/contacts/remove | session | Remove |
GET | /zero-access/e2e/contacts | session | List |
Fingerprint: SHA-256("zero-e2e-fp-v1" ‖ signingKey ‖ identityKey) base64url. Client crypto: Zero-e2e.
M6 denylist
Reject bodies that include raw secret keys (NFKC + compacted, case-insensitive), including:
mnemonic, recoveryPhrase, rawMek, masterKey, prfSecret, plaintext, seed, privateKey, password, passphrase, authSecret, …
Wrap puts scan the full request body before snapshotting allowlisted fields. Canonical list: package m6.ts.
Privilege helpers
import { requireElevate } from "@railman/auth-elevate";
import {
AUTH_REQUIRE_PRESETS,
createElevateOpener,
requireAccess,
} from "@railman/auth-login-factor";
await requireElevate(claim, { secret, userId, sessionToken });
await requireAccess({
secret,
session,
map: AUTH_REQUIRE_PRESETS.privilegedAction,
elevateOpener: createElevateOpener({
secret,
userId: session.userId,
sessionToken: session.sessionToken,
ttlSec: 300,
maxElevatedSec: 3600,
}),
});requireAccess maps are server constants. Maps that inspect elevate AMR need elevateOpener from createElevateOpener (same user + session token). Browser MEK session helpers are never a server authorization signal.
Package docs: Login factor.
Client libraries
| Concern | Package / import |
|---|---|
| Passkey stack + PRF salt | enhancePasskey · defineZeroAccessPasskeyCryptoConfig — Passkey |
| Blind store BA client + low-level wraps | @railman/auth-zero-access/client — Zero-access |
| Product vault UX | createVaultClient — Zero-vault |
| Sealed chat crypto | @railman/zero-e2e — Zero-e2e |
| Elevate / login clients | @railman/auth-elevate/client · stock passkey client |
Vault client (surface)
| Method | Role |
|---|---|
createVault | Init MEK + recovery (+ daily wraps) |
unlockDaily | PRF and/or vault password |
resetWithRecoveryPhrase | Break-glass |
reenrollDailyMethods | After recovery |
Related
- Packages overview
- Plugin contract (short mirror)
- Use cases / compose guides
- Checklist