Zero Accessby Railman
Packages

Elevate

Getting started with @railman/auth-elevate — step-up / sudo for Better Auth.

Getting started — Elevate

@railman/auth-elevate adds a short step-up window on an existing session. It does not unlock vault material.

Demo: /elevate · Guide: Elevate and sudo · HTTP: API

Install

pnpm add @railman/auth-elevate @railman/auth-login-factor
# optional TOTP storage:
# better-auth twoFactor plugin
# optional passkey step-up:
# @better-auth/passkey @railman/auth-zero-access-passkey

Minimal — password / TOTP only

import { betterAuth } from "better-auth";
import { twoFactor } from "better-auth/plugins";
import { elevate, requireElevate } from "@railman/auth-elevate";
import { loginFactors } from "@railman/auth-login-factor";

export const auth = betterAuth({
  plugins: [
    loginFactors(),
    twoFactor(),
    elevate({
      // omit passkeyCounterGuard → passkey step-up off
      passwordOnlyIfNoStronger: true,
      elevatedTtlSec: 300,
    }),
  ],
});

Gate a route:

await requireElevate(session.session.elevateClaim, {
  userId: session.user.id,
  sessionToken: session.session.token ?? session.session.id,
  secret: process.env.BETTER_AUTH_SECRET!,
  ttlSec: 300,
});

With passkey step-up

import { passkey } from "@better-auth/passkey";
import { enhancePasskey } from "@railman/auth-zero-access-passkey";

const passkeyStack = enhancePasskey(passkey, {
  rpID: "example.com",
  origin: ["https://example.com"],
  assertCredentialAccess: "session",
});

elevate({
  passkeyCounterGuard: passkeyStack.controller,
  passwordOnlyIfNoStronger: true,
  elevatedTtlSec: 300,
});

Client

elevateClient is L1 ceremony HTTP. It does not unlock a vault, persist wraps, enroll TOTP/passkeys, or authorize from its JSON. Full split: Client boundaries.

import { elevateClient } from "@railman/auth-elevate/client";
import { createAuthClient } from "better-auth/client";

const authClient = createAuthClient({ plugins: [elevateClient()] });

await authClient.elevate.methods();
await authClient.elevate.verify({ method: "password", password });
await authClient.elevate.verify({ method: "totp", totpCode });
await authClient.elevate.withPasskey(); // existing session; not login; not PRF unlock

Returned verify JSON is UI-only. Authorize with requireElevate / requireAccess on the server session.

Key options

OptionDefault intent
elevatedTtlSec300
passwordOnlyIfNoStrongertrue at mint (optional). Prefer action gates (amrAnyOf, privilegedAction) so password can mint while vault rejects it.
passkeyCounterGuardOptional — omit to disable passkey method
StorageBetter Auth database + secondaryStorage (not a plugin option)

Next

On this page