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. Who owns which client call: Client boundaries.
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({
passkeyCounterGuard: passkeyStack.controller,
passwordOnlyIfNoStronger: true,
elevatedTtlSec: 300,
}),
zeroAccess({}),
];Product vault and chat (browser): local crypto; persist through zeroAccessClient. Elevate is sudo only — Client boundaries.
import { createAuthClient } from "better-auth/client";
import { passkeyClient } from "@better-auth/passkey/client";
import { elevateClient } from "@railman/auth-elevate/client";
import { zeroAccessClient } from "@railman/auth-zero-access/client";
import { defineZeroAccessPasskeyCryptoConfig } from "@railman/auth-zero-access-passkey/client";
import { createVaultClient } from "@railman/zero-vault";
const authClient = createAuthClient({
plugins: [passkeyClient(), elevateClient(), zeroAccessClient()],
});
const cryptoConfig = defineZeroAccessPasskeyCryptoConfig({ salt });
createVaultClient({ userId, cryptoConfig, authClient });Never unlock MEK because elevate succeeded.
Storage is Better Auth's database + secondaryStorage. There is no plugin topology option and no in-memory product adapter. See Shared storage.
Elevate
Browser elevateClient: ceremony HTTP only — Client boundaries. Verify JSON is UI-only.
| 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. Nested accountRecovery is rejected. |
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 + assertAccess | Mode B public key + newKeyProof |
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 |