@wrnexus/auth
Authentication routes, sessions, forms, guards, and account flows.
Install the package
After WorkRoot approves private registry access, install the release-aligned package:
bun add @wrnexus/auth@0.5.0Request preview access. Never put registry tokens in source control.
Framework-native authentication, identity, account-security, and session management for WRNexusJS.
+Capabilities
+-
+
- Password registration, login, recovery, reset, and authenticated password changes +
- Email, phone, and username identities with verification and generic resend responses +
- Magic links and passwordless email/SMS OTP login +
- MFA transactions using verified email OTP, verified SMS OTP, TOTP, or recovery codes +
- RFC 6238 TOTP with counter replay protection +
- One-use recovery codes; regeneration invalidates previous unused codes +
- Passkey/WebAuthn registration and strong passwordless sign-in through a provider contract +
- OAuth account linking and provider sign-in +
- Invitations, session rotation, idle and absolute expiry, revocation, and trusted devices +
- Deny-by-default audited support impersonation +
- Adaptive risk scoring, CAPTCHA escalation, temporary lockout, and optional login alerts +
- Memory and SQL stores +
- Optional encryption-keyring protection for TOTP and OAuth secrets +
- Automatic API routes, middleware, browser schemas, components, runtime, migrations, and DevToolbar checks +
Passkeys are a strong sign-in method. They are not currently exposed as a selectable second step in TwoFactorChallenge; the implemented MFA methods are email OTP, SMS OTP, TOTP, and recovery codes.
Install
+bun add @wrnexus/auth
+WRNexusJS discovers the package automatically. Do not copy package components, client scripts, schemas, or standard /api/auth/* route files into the application.
Default configuration
+Create the engine:
+// app/lib/auth.ts
+import { createAuthEngine, MemoryAuthStore } from "@wrnexus/auth";
+
+export const auth = createAuthEngine({
+ store: new MemoryAuthStore(),
+ secret: process.env.AUTH_SECRET!,
+ issuer: "My application",
+ onSignedIn(ctx, returnTo) {
+ const safe = returnTo?.startsWith("/") && !returnTo.startsWith("//") ? returnTo : "/account";
+ return Response.redirect(new URL(safe, ctx.url), 303);
+ },
+ onSignedOut(ctx) {
+ return Response.redirect(new URL("/sign-in", ctx.url), 303);
+ },
+ onSuccessfulSignUp() {
+ return {
+ autoSignIn: true,
+ redirectTo: "/account",
+ };
+ },
+ delivery: {
+ async send(message) {
+ // Queue email/SMS through your provider. Never log message.code or message.token.
+ },
+ },
+});
+Authentication behavior belongs in this engine definition: delivery, token URL mapping, successful sign-in/sign-out responses, password policy, risk thresholds, MFA, passkeys, and auditing can all be configured in one server-only location. The older config.auth.onSignedIn and config.auth.onSignedOut fields remain supported as compatibility overrides, but new applications should configure these hooks on createAuthEngine.
Successful signup behavior
+Without onSuccessfulSignUp, a successful package registration redirects to /sign-in.
To sign in immediately after registration:
+onSuccessfulSignUp(ctx, user) {
+ return {
+ autoSignIn: true,
+ redirectTo: "/account",
+ };
+}
+Automatic sign-in runs the normal login policy. It does not bypass required email or phone verification, CAPTCHA, MFA, account status, or risk checks. The hook may also return a Response for a completely custom HTTP result, or return { redirectTo: "/welcome" } to redirect without creating a session.
Register it through application configuration:
+// wrnexus.config.ts
+import type { AuthConfig } from "@wrnexus/auth";
+import type { AppConfig } from "@wrnexus/styles";
+import { auth } from "./app/lib/auth.ts";
+
+const config = {
+ auth: {
+ engine: auth,
+ routes: true,
+ middleware: true,
+ migrations: false,
+ },
+} satisfies AppConfig & { auth: AuthConfig };
+
+export default config;
+That configuration automatically activates package routes, auth-session middleware, components, browser validation schemas, and the auth client runtime. setDefaultAuthEngine() remains available only for advanced manual integrations and tests.
SQL production configuration
+import { createAuthEngine, SqlAuthStore } from "@wrnexus/auth";
+import { getDb } from "@wrnexus/db";
+
+export const auth = createAuthEngine({
+ store: new SqlAuthStore(getDb()),
+ secret: process.env.AUTH_SECRET!,
+});
+export default {
+ db: {
+ // Application database configuration.
+ },
+ auth: {
+ engine: auth,
+ routes: true,
+ middleware: true,
+ migrations: true,
+ },
+};
+The package contributes both ordered migrations:
+001_auth.sql
+002_auth_otp_purpose.sql
+Migrations are enabled automatically only when auth.engine and a default config.db are present. Set auth.migrations explicitly when an application needs different behavior.
Delivered action URLs
+By default, the engine builds links from the supplied baseUrl and token purpose. Applications can map those links to their own page structure without replacing package APIs:
const auth = createAuthEngine({
+ store: new SqlAuthStore(getDb()),
+ secret: process.env.AUTH_SECRET!,
+ tokenUrl({ purpose, token, baseUrl }) {
+ if (!baseUrl) return undefined;
+
+ const paths = {
+ "verify-email": `/verify-email?token=${encodeURIComponent(token)}`,
+ "verify-phone": `/verify-phone?token=${encodeURIComponent(token)}`,
+ "password-reset": `/recover/reset?token=${encodeURIComponent(token)}`,
+ "magic-link": `/magic-link?token=${encodeURIComponent(token)}`,
+ invite: `/invitation?token=${encodeURIComponent(token)}`,
+ };
+
+ const path = paths[purpose as keyof typeof paths];
+ return path ? new URL(path, baseUrl).toString() : undefined;
+ },
+});
+Returning undefined intentionally omits the URL while still delivering the raw token. The callback runs only in trusted server code.
Built-in validation
+Every packaged auth form has a built-in @wrnexus/validation schema. The same resolved schema is used by the browser and the package API handler.
Default use requires no app/schemas files:
<SignUp />
+<SignIn />
+<ForgotPassword />
+<ResetPassword token='{token}' />
+<TwoFactorChallenge />
+To customize one schema, extend the package default and register only that override:
+// app/schemas/custom-password-request.ts
+import { authSchemas } from "@wrnexus/auth";
+import { v } from "@wrnexus/validation";
+
+export default authSchemas.passwordResetRequest.extend({
+ identifier: v
+ .string()
+ .trim()
+ .required("Enter your registered email address")
+ .email("Enter a valid registered email address"),
+});
+import customPasswordRequest from "./app/schemas/custom-password-request.ts";
+
+export default {
+ auth: {
+ engine: auth,
+ schemas: {
+ passwordResetRequest: customPasswordRequest,
+ },
+ },
+};
+<ForgotPassword /> can keep its default schema="auth-password-request". The plugin automatically publishes the overridden browser descriptor under that same built-in schema ID. All other forms continue using package defaults.
Route controls
+Use a boolean to enable or disable all package routes:
+auth: {
+ engine: auth,
+ routes: true,
+}
+Or control feature groups:
+routes: {
+ enabled: true,
+ registration: true,
+ login: true,
+ verification: true,
+ password: true,
+ invitations: true,
+ magicLink: true,
+ otp: true,
+ mfa: true,
+ sessions: true,
+ impersonation: false,
+ passkeys: true,
+}
+Application routes have normal framework precedence. Disable a package group only when the application intentionally owns every endpoint in that group; no excludeRoutes list is required.
Package endpoints
+POST /api/auth/register
+POST /api/auth/login
+POST /api/auth/logout
+POST /api/auth/verification/request
+GET|POST /api/auth/verify/email
+POST /api/auth/verify/phone
+POST /api/auth/password/request
+POST /api/auth/password/reset
+POST /api/auth/password/change
+POST /api/auth/invitations/accept
+POST /api/auth/magic-link/request
+GET|POST /api/auth/magic-link
+POST /api/auth/otp/login/request
+POST /api/auth/otp/login/complete
+POST /api/auth/otp
+POST /api/auth/otp/verify
+POST /api/auth/totp/setup
+POST /api/auth/totp/confirm
+POST /api/auth/totp/disable
+POST /api/auth/recovery-codes
+POST /api/auth/mfa/otp
+POST /api/auth/mfa/complete
+GET /api/auth/sessions
+POST /api/auth/sessions/revoke
+POST /api/auth/impersonation/start
+POST /api/auth/impersonation/stop
+POST /api/auth/passkeys/register/options
+POST /api/auth/passkeys/register/verify
+POST /api/auth/passkeys/login/options
+POST /api/auth/passkeys/login/verify
+Each URL uses a route-specific module, so rewritten framework request URLs cannot make the handler fall through to a shared-dispatcher 404.
Unsafe package routes validate the framework CSRF token by default. Set auth.csrf: false only when an external API gateway provides an equivalent protection model.
Components
+<SignIn />
+<SignUp />
+<ForgotPassword />
+<ResetPassword token='{token}' />
+<OtpSignIn method="email-otp" />
+<MagicLinkSignIn />
+<PasskeyButton mode="authenticate" />
+<TwoFactorChallenge mfaToken='{mfaToken}' challengeId='{challengeId}' />
+<AuthenticatorSetup credentialId='{credentialId}' secret='{secret}' uri='{uri}' />
+<RecoveryCodes codes='{codes}' />
+<DeviceSessions sessions='{sessions}' currentSessionId='{currentSessionId}' />
+<VerifyEmail token='{token}' identifier='{identifier}' />
+<VerifyPhone token='{token}' identifier='{identifier}' />
+<InvitationAccept token='{token}' />
+<ImpersonationBanner targetName='{targetName}' />
+<AccountStatus status='{account.status}' />
+identifier is optional on verification components. Supply it when an unauthenticated verification page should support resending a token. The response remains generic whether the account exists or not.
CAPTCHA and risk
+The HTTP handlers never trust a browser captchaVerified field. CAPTCHA completion is accepted only from server-populated ctx.locals.captcha.success or ctx.locals.captchaVerified === true.
Rate limiting remains an application or gateway responsibility. Apply it to registration, login, reset, magic-link, OTP, verification, passkey, invitation, and impersonation endpoints.
+MFA
+1. Password, OAuth, magic-link, or OTP login may return code: "mfa-required" with a short-lived mfaToken. 2. The response lists only methods actually available to that user. 3. Email/SMS MFA is offered only for verified linked identities. 4. beginMfaOtp() issues an MFA-bound OTP when needed. 5. completeMfa() consumes the one-time transaction and creates the session.
Passkeys
+The browser runtime coordinates navigator.credentials.create() and navigator.credentials.get(). A configured server-side PasskeyProvider must verify the challenge, RP ID, origin, signature, user presence or verification, counter, and credential ownership.
Multi-process deployments must provide a shared PasskeyChallengeStore; the default memory implementation is process-local. Missing passkey providers return a controlled 503 response rather than crashing the route.
Protect long-lived secrets
+import { createAuthSecretProtector } from "@wrnexus/auth";
+import { createKeyring } from "@wrnexus/encryption";
+
+const keyring = createKeyring([
+ {
+ id: "auth-2026-01",
+ secret: process.env.AUTH_ENCRYPTION_KEY!,
+ active: true,
+ },
+]);
+
+const auth = createAuthEngine({
+ store: new SqlAuthStore(getDb()),
+ secret: process.env.AUTH_SECRET!,
+ secretProtector: createAuthSecretProtector(keyring),
+});
+TOTP seeds and OAuth access/refresh tokens are protected before persistence. Keep old keys available during rotation.
+Custom HTTP integration
+createAuthHttpHandlers() remains available for custom route paths or response behavior. Prefer package routes for standard flows; copied application API files are unnecessary.
Development
+bun run auth:dev
+bun run validate:auth
+Read [SECURITY.md](./SECURITY.md) before production deployment.
Complete TypeScript API
Generated from the exact installed package declarations.
export { A as AuthEngine, c as createAuthEngine, i as inferIdentityType, n as normalizeEmail, a as normalizeIdentity, b as normalizePhone, d as normalizeUsername, p as publicUser, s as safeAuthReturnTo } from './engine-jttXj6PP.js';
+import { n as AuthRiskSignals, l as AuthRiskDecision } from './types-JLkQpcAt.js';
+export { A as AuthAccountStatus, a as AuthClock, b as AuthDeliveryMessage, c as AuthDeliveryProvider, d as AuthEngineOptions, e as AuthIdentity, f as AuthIdentityType, g as AuthImpersonationDecision, h as AuthMfaMethod, i as AuthPublicUser, j as AuthRandom, k as AuthResult, m as AuthRiskLevel, o as AuthSecretProtector, p as AuthSecurityEvent, q as AuthSession, r as AuthSignedInHandler, s as AuthSignedOutHandler, M as AuthStore, t as AuthSuccessfulSignUpAction, u as AuthSuccessfulSignUpHandler, v as AuthTokenPurpose, w as AuthTokenUrlInput, x as AuthUser, y as AuthenticatedContext, L as LoginAttempt, z as LoginInput, N as MemoryPasskeyChallengeStore, O as OAuthAccount, B as OneTimeToken, C as OtpChallenge, P as PasskeyAuthenticationOptions, Q as PasskeyChallengeKind, S as PasskeyChallengeRecord, U as PasskeyChallengeStore, D as PasskeyCredential, E as PasskeyProvider, F as PasskeyRegistrationOptions, G as PasskeyVerificationResult, H as PasswordBreachProvider, I as PasswordCredential, R as RecoveryCodeRecord, J as RegisterInput, T as TotpCredential, K as TrustedDevice, V as assertPasskeyProvider } from './types-JLkQpcAt.js';
+export { MemoryAuthStore } from './stores/memory.js';
+export { SqlAuthStore } from './stores/sql.js';
+export { AUTH_SESSION_KEY, authSession, clearAuthSession, establishAuthSession, getAuthSession, getAuthUser, isAuthenticatedContext, requireAuth } from './middleware.js';
+export { A as AuthHttpOptions, a as AuthPasskeyHttpOptions, b as AuthSchemaOverrides, c as AuthSchemaSet, d as authBrowserSchemaDescriptors, e as authBrowserSchemaMap, f as authSchemas, g as authenticatorConfirmSchema, h as authenticatorDisableSchema, i as authenticatorSetupSchema, j as changePasswordSchema, k as createAuthHttpHandlers, l as emptyActionSchema, m as impersonationStartSchema, n as invitationAcceptSchema, o as loginSchema, p as magicLinkConsumeSchema, q as magicLinkRequestSchema, r as mfaOtpRequestSchema, s as mfaSchema, t as otpIssueSchema, u as otpLoginCompleteSchema, v as otpLoginRequestSchema, w as otpSchema, x as passkeyAuthenticationOptionsSchema, y as passkeyAuthenticationVerifySchema, z as passkeyRegistrationOptionsSchema, B as passkeyRegistrationVerifySchema, C as passwordResetRequestSchema, D as passwordResetSchema, E as recoveryCodesSchema, F as registerSchema, G as resolveAuthSchemas, H as sessionRevokeSchema, I as signUpSchema, J as verificationRequestSchema, K as verificationTokenSchema } from './index-CrF_lZDQ.js';
+export { AuthAuditIssue, AuthConfig, AuthPluginOptions, AuthRoutesConfig, authComponentsDir, authPlugin } from './plugin.js';
+export { DefaultAuthRouteOptions, clearDefaultAuthEngine, getDefaultAuthEngine, getDefaultAuthRouteOptions, getDefaultAuthSchemas, hasDefaultAuthEngine, setDefaultAuthEngine, setDefaultAuthRouteOptions, setDefaultAuthSchemas, tryGetDefaultAuthEngine } from './runtime.js';
+export { createAuthSecretProtector } from './protector.js';
+export { decodeBase32, encodeBase32, generateTotp, generateTotpSecret, totpUri, verifyTotp } from './totp/index.js';
+import '@wrnexus/oauth';
+import '@wrnexus/core';
+import '@wrnexus/db';
+import '@wrnexus/validation';
+import '@wrnexus/plugin';
+import '@wrnexus/encryption';
+
+interface RiskPolicy {
+ captchaThreshold: number;
+ mfaThreshold: number;
+ blockThreshold: number;
+}
+declare function evaluateAuthRisk(signals?: AuthRiskSignals, policy?: RiskPolicy): AuthRiskDecision;
+
+export { AuthRiskDecision, AuthRiskSignals, type RiskPolicy, evaluateAuthRisk };
+Examples
Copy-ready examples from the installed package documentation.
## Install
bun add @wrnexus/authCreate the engine
// app/lib/auth.ts
+import { createAuthEngine, MemoryAuthStore } from "@wrnexus/auth";
+
+export const auth = createAuthEngine({
+ store: new MemoryAuthStore(),
+ secret: process.env.AUTH_SECRET!,
+ issuer: "My application",
+ onSignedIn(ctx, returnTo) {
+ const safe = returnTo?.startsWith("/") && !returnTo.startsWith("//") ? returnTo : "/account";
+ return Response.redirect(new URL(safe, ctx.url), 303);
+ },
+ onSignedOut(ctx) {
+ return Response.redirect(new URL("/sign-in", ctx.url), 303);
+ },
+ onSuccessfulSignUp() {
+ return {
+ autoSignIn: true,
+ redirectTo: "/account",
+ };
+ },
+ delivery: {
+ async send(message) {
+ // Queue email/SMS through your provider. Never log message.code or message.token.
+ },
+ },
+});Successful signup behavior
onSuccessfulSignUp(ctx, user) {
+ return {
+ autoSignIn: true,
+ redirectTo: "/account",
+ };
+}Successful signup behavior
// wrnexus.config.ts
+import type { AuthConfig } from "@wrnexus/auth";
+import type { AppConfig } from "@wrnexus/styles";
+import { auth } from "./app/lib/auth.ts";
+
+const config = {
+ auth: {
+ engine: auth,
+ routes: true,
+ middleware: true,
+ migrations: false,
+ },
+} satisfies AppConfig & { auth: AuthConfig };
+
+export default config;Successful signup behavior
import { createAuthEngine, SqlAuthStore } from "@wrnexus/auth";
+import { getDb } from "@wrnexus/db";
+
+export const auth = createAuthEngine({
+ store: new SqlAuthStore(getDb()),
+ secret: process.env.AUTH_SECRET!,
+});Successful signup behavior
export default {
+ db: {
+ // Application database configuration.
+ },
+ auth: {
+ engine: auth,
+ routes: true,
+ middleware: true,
+ migrations: true,
+ },
+};