Raises the typescript devDependency across the workspace, bumps package versions, re-adds ignoreDeprecations, and repoints the @wrnexus registry. These were pre-existing working-tree changes, committed as-is rather than authored here. The .npmrc change redirects @wrnexus publishes from registry.npmjs.org to registry.workroot.in — confirm that is intended before publishing from this branch. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@wrnexus/auth
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
- Password, OAuth, magic-link, or OTP login may return
code: "mfa-required"with a short-livedmfaToken. - The response lists only methods actually available to that user.
- Email/SMS MFA is offered only for verified linked identities.
beginMfaOtp()issues an MFA-bound OTP when needed.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 before production deployment.
Package-owned UI blocks and route helpers
Authentication forms continue to compose @wrnexus/ui inputs, buttons, cards, alerts, badges, avatars, and PIN controls. The package also provides:
<AuthShell /><AuthProviderButtons /><AuthSecurityNotice />- complete sign-in, sign-up, MFA, passkey, recovery, account-status, session, and impersonation blocks
Server helpers include authRoute, authSuccess, authFailure, requireAuthUser, optionalAuthUser, currentAuthSession, and authComponentProps.