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
| Method | Path | Auth | Notes |
|---|---|---|---|
GET | /elevate/methods | session | Available factors |
GET | /elevate/passkey/options | session | WebAuthn options for step-up |
POST | /elevate/verify | session | Mint/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.
| 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 |
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 | Mode B public material |
POST | /zero-access/account-recovery/start | none* | Challenge |
POST | /zero-access/account-recovery/finish | none* | Verify + HttpOnly __Host- recovery-session cookie |
POST | /zero-access/account-recovery/session | recovery cookie + origin | Inspect 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 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
| Concern | Import |
|---|---|
| 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 |