release: WRNexusJS 0.5.0

This commit is contained in:
2026-07-29 12:51:10 +05:30
parent 76c768099d
commit 6afe32f63f
456 changed files with 40879 additions and 8850 deletions
+13
View File
@@ -0,0 +1,13 @@
# Authentication showcase
Run from the monorepo root:
```bash
bun run auth:dev
```
The showcase uses package-owned `/api/auth/*` routes, automatic browser schemas, auth-session middleware, components, styles, and runtime. It intentionally contains no copied default API handlers or `app/schemas` files.
It demonstrates registration, password login and recovery, passwordless OTP, magic-link requests, two-factor verification, authenticator setup, passkeys, sessions, invitations, account state, and impersonation safety UI.
Development delivery messages are retained in the in-memory `outbox`. Console output reports only the template and destination; it does not print OTP codes, raw tokens, or reset URLs. Replace the memory store, development secret, and delivery implementation before production use.
@@ -0,0 +1,10 @@
import type { Context } from "@wrnexus/core";
import { captchaHandlers } from "../../lib/captcha.ts";
async function handle(ctx: Context): Promise<Response> {
return (await captchaHandlers.handle(ctx.req, ctx)) ?? new Response("Not Found", { status: 404 });
}
export const GET = handle;
export const HEAD = handle;
export const POST = handle;
+29
View File
@@ -0,0 +1,29 @@
import { createAuthEngine, MemoryAuthStore } from "@wrnexus/auth";
export const outbox: unknown[] = [];
export const auth = createAuthEngine({
store: new MemoryAuthStore(),
secret: "development-auth-showcase-secret-change-in-production",
issuer: "WRNexus Auth Showcase",
sendLoginAlerts: true,
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) {
outbox.push(message);
console.info(`[auth-showcase] queued ${message.template} for ${message.destination}`);
},
},
});
+21
View File
@@ -0,0 +1,21 @@
import {
createCaptchaEngine,
createCaptchaHttpHandlers,
MemoryCaptchaStore,
} from "@wrnexus/captcha/server";
export const captchaEngine = createCaptchaEngine({
secret: process.env.CAPTCHA_SECRET ?? "development-auth-showcase-captcha-secret-change-this",
store: new MemoryCaptchaStore(),
basePath: "/api/captcha",
challengeTtlMs: 2 * 60_000,
responseTokenTtlMs: 5 * 60_000,
maxAttempts: 3,
minCompletionMs: 800,
});
export const captchaHandlers = createCaptchaHttpHandlers(captchaEngine, {
createLimit: 60,
verifyLimit: 60,
windowMs: 60_000,
});
@@ -0,0 +1,14 @@
import { authSession, requireAuth } from "@wrnexus/auth";
import type { Middleware } from "@wrnexus/core";
import { auth } from "../lib/auth.ts";
const hydrate = authSession(auth);
const accountGuard = requireAuth({ loginPath: "/sign-in" });
const middleware: Middleware = async (ctx, next) => {
return hydrate(ctx, () =>
ctx.url.pathname.startsWith("/account") ? accountGuard(ctx, next) : next(),
);
};
export default middleware;
@@ -0,0 +1,31 @@
import type { Context, Next } from "@wrnexus/core";
import { captchaGuard } from "@wrnexus/captcha/server";
import { captchaEngine } from "../lib/captcha.ts";
const protectLogin = captchaGuard({
action: "auth-login",
engine: captchaEngine,
responseField: "wrn-captcha-response",
bindHostname: true,
bindSession: true,
verifiedForMs: 5 * 60_000,
onFailure(_ctx, result) {
return Response.json(
{
ok: false,
code: result.code ?? "captcha-invalid",
message: result.message ?? "Complete the security check below before signing in.",
},
{
status: 403,
headers: { "cache-control": "no-store" },
},
);
},
});
export default function captchaLogin(ctx: Context, next: Next) {
return ctx.req.method === "POST" && ctx.url.pathname === "/api/auth/login"
? protectLogin(ctx, next)
: next();
}
@@ -0,0 +1,20 @@
page AccountPage {
state user = ctx.user
seo { title = "Account" }
view {
<main class="min-h-screen bg-[var(--wire-color-bg)] p-6 text-[var(--wire-color-text)]">
<div class="mx-auto max-w-3xl">
<h1 class="text-3xl font-bold">Account security</h1>
<p class="text-[var(--wire-color-muted)]">Signed in as {user.displayName || user.username || user.id}</p>
<div class="mt-8 grid gap-5">
<AccountStatus status='{user.status}' />
<PasskeyButton mode="register" />
<form method="post" action="/api/auth/logout" data-schema="auth-empty" novalidate>
<p data-error="_form" role="alert" class="mb-3 hidden text-sm text-[var(--wire-color-danger)]"></p>
<button type="submit" class="h-11 rounded-[var(--wire-radius-sm)] bg-[var(--wire-color-danger)] px-5 font-semibold text-white disabled:cursor-wait disabled:opacity-60">Sign out</button>
</form>
</div>
</div>
</main>
}
}
@@ -0,0 +1,4 @@
page AuthenticatorPage {
seo { title = "Authenticator setup" }
view { <main class="flex min-h-screen items-center justify-center bg-[var(--wire-color-bg)] p-4"><AuthenticatorSetup credentialId="demo" secret="JBSWY3DPEHPK3PXP" uri="otpauth://totp/WRNexusJS:developer@example.com?secret=JBSWY3DPEHPK3PXP&amp;issuer=WRNexusJS" /></main> }
}
@@ -0,0 +1,4 @@
page ImpersonationPage {
seo { title = "Impersonation safety banner" }
view { <main class="min-h-screen bg-[var(--wire-color-bg)] text-[var(--wire-color-text)]"><ImpersonationBanner targetName="Demo Customer" /><div class="mx-auto max-w-4xl p-8"><h1 class="text-3xl font-bold">Support impersonation</h1><p class="text-[var(--wire-color-muted)]">The banner remains visible while an audited support session is active.</p></div></main> }
}
@@ -0,0 +1,21 @@
page AuthShowcase {
seo { title = "Authentication system showcase" }
view {
<main class="min-h-screen bg-[var(--wire-color-bg)] px-4 py-12 text-[var(--wire-color-text)] sm:px-6">
<div class="mx-auto max-w-6xl">
<header class="max-w-3xl">
<p class="mb-2 text-sm font-semibold text-[var(--wire-color-primary)]">@wrnexus/auth</p>
<h1 class="m-0 text-4xl font-bold tracking-tight sm:text-6xl">Complete authentication for WRNexusJS</h1>
<p class="mt-5 text-lg leading-8 text-[var(--wire-color-muted)]">Password login, verification, recovery, OTP, authenticator apps, recovery codes, passkeys, OAuth linking, devices, sessions, risk, and audit events.</p>
<div class="mt-6 flex flex-wrap gap-3"><a href="/sign-up" class="inline-flex h-11 items-center rounded-[var(--wire-radius-sm)] bg-[var(--wire-color-primary)] px-5 font-semibold text-white">Create account</a><a href="/sign-in" class="inline-flex h-11 items-center rounded-[var(--wire-radius-sm)] border border-[var(--wire-color-border)] px-5 font-semibold">Sign in</a><a href="/otp" class="inline-flex h-11 items-center rounded-[var(--wire-radius-sm)] border border-[var(--wire-color-border)] px-5 font-semibold">OTP login</a><a href="/magic-link" class="inline-flex h-11 items-center rounded-[var(--wire-radius-sm)] border border-[var(--wire-color-border)] px-5 font-semibold">Magic link</a></div>
</header>
<section class="mt-12 grid gap-5 sm:grid-cols-2 lg:grid-cols-3">
{#each ["Password and magic links", "Email, SMS, TOTP and recovery", "Passkeys and OAuth", "Sessions and trusted devices", "Adaptive CAPTCHA risk", "Security event audit"] as feature}
<article class="rounded-[var(--wire-radius-lg)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface)] p-5"><span class="icon-[lucide--shield-check] size-5 text-[var(--wire-color-primary)]"></span><h2 class="mb-0 mt-3 text-base font-semibold">{feature}</h2></article>
{/each}
</section>
<nav class="mt-8 flex flex-wrap gap-x-5 gap-y-2 text-sm font-semibold text-[var(--wire-color-primary)]"><a href="/two-factor">Two-factor</a><a href="/authenticator">Authenticator</a><a href="/invitation">Invitation</a><a href="/impersonation">Impersonation</a><a href="/account">Sessions</a></nav>
</div>
</main>
}
}
@@ -0,0 +1,4 @@
page InvitationPage {
seo { title = "Accept invitation" }
view { <main class="flex min-h-screen items-center justify-center bg-[var(--wire-color-bg)] p-4"><InvitationAccept token="demo-invitation" organization="WRNexus Workspace" inviter="Ajay" /></main> }
}
@@ -0,0 +1,4 @@
page MagicLinkPage {
seo { title = "Magic-link sign-in" }
view { <main class="flex min-h-screen items-center justify-center bg-[var(--wire-color-bg)] p-4"><MagicLinkSignIn /></main> }
}
+4
View File
@@ -0,0 +1,4 @@
page OtpSignInPage {
seo { title = "Passwordless OTP sign-in" }
view { <main class="flex min-h-screen items-center justify-center bg-[var(--wire-color-bg)] p-4"><OtpSignIn method="email-otp" /></main> }
}
@@ -0,0 +1,4 @@
page RecoverPage {
seo { title = "Recover account" }
view { <main class="flex min-h-screen items-center justify-center bg-[var(--wire-color-bg)] p-4"><ForgotPassword /></main> }
}
@@ -0,0 +1,18 @@
page SignInPage {
seo { title = "Sign in" }
view {
<main class="flex min-h-screen items-center justify-center bg-[var(--wire-color-bg)] p-4">
<SignIn action="/api/auth/login" returnTo="/account" showPasskey="false">
<Captcha
endpoint="/api/captcha/challenge"
verifyEndpoint="/api/captcha/verify"
type="not-robot"
action="auth-login"
required="true"
resetOnError="false"
requiredMessage="Complete the security check below before signing in."
/>
</SignIn>
</main>
}
}
@@ -0,0 +1,8 @@
page SignUpPage {
seo { title = "Create account" }
view {
<main class="flex min-h-screen items-center justify-center bg-[var(--wire-color-bg)] p-4">
<SignUp redirect="/sign-in" />
</main>
}
}
@@ -0,0 +1,4 @@
page TwoFactorPage {
seo { title = "Two-factor authentication" }
view { <main class="flex min-h-screen items-center justify-center bg-[var(--wire-color-bg)] p-4"><TwoFactorChallenge mfaToken="demo-transaction" /></main> }
}
+65
View File
@@ -0,0 +1,65 @@
// AUTO-GENERATED by `wrnexus dev` - do not edit.
// Typed routes support required, optional, and catch-all parameters.
export interface Routes {
"/": Record<string, never>;
"/account": Record<string, never>;
"/authenticator": Record<string, never>;
"/impersonation": Record<string, never>;
"/invitation": Record<string, never>;
"/magic-link": Record<string, never>;
"/otp": Record<string, never>;
"/recover": Record<string, never>;
"/sign-in": Record<string, never>;
"/sign-up": Record<string, never>;
"/two-factor": Record<string, never>;
}
export type RoutePath = keyof Routes;
type RouteValue = string | readonly string[] | undefined;
function encodeRouteValue(value: RouteValue, catchAll: boolean): string {
if (value === undefined) return "";
const values = Array.isArray(value) ? value : catchAll ? String(value).split("/") : [String(value)];
return values.map((part) => encodeURIComponent(part)).join("/");
}
export function href<P extends RoutePath>(
path: P,
...args: keyof Routes[P] extends never
? []
: Record<string, never> extends Routes[P]
? [params?: Routes[P]]
: [params: Routes[P]]
): string {
const params = (args[0] ?? {}) as Record<string, RouteValue>;
const output: string[] = [];
for (const segment of String(path).split("/").filter(Boolean)) {
let name: string | undefined;
let optional = false;
let catchAll = false;
if (segment.startsWith("[[") && segment.endsWith("]]")) {
optional = true;
name = segment.slice(2, -2);
} else if (segment.startsWith("[") && segment.endsWith("]")) {
name = segment.slice(1, -1);
if (name.endsWith("?")) {
optional = true;
name = name.slice(0, -1);
}
}
if (!name) {
output.push(segment);
continue;
}
if (name.startsWith("...")) {
catchAll = true;
name = name.slice(3);
}
const value = params[name];
if (value === undefined && optional) continue;
if (value === undefined) throw new Error(`WRN-ROUTE-MISSING-PARAM: Missing route parameter '${name}'.`);
output.push(encodeRouteValue(value, catchAll));
}
return "/" + output.filter(Boolean).join("/");
}
@@ -0,0 +1,14 @@
@import "tailwindcss";
@plugin "@iconify/tailwind4";
@source "../**/*.wrn";
@source "../../../packages/auth/components/*.wrn";
html {
color-scheme: light dark;
}
body {
margin: 0;
background: var(--wire-color-bg);
color: var(--wire-color-text);
font-family: "Plus Jakarta Sans", ui-sans-serif, system-ui, sans-serif;
}
+26
View File
@@ -0,0 +1,26 @@
{
"name": "auth-showcase",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"dev": "bun run ../../packages/cli/src/index.ts dev .",
"build": "bun run ../../packages/cli/src/index.ts build .",
"test": "bun run ../../packages/cli/src/index.ts test .",
"typecheck": "tsc --noEmit -p tsconfig.json",
"check": "bun run typecheck && bun run test && bun run build"
},
"dependencies": {
"@wrnexus/auth": "workspace:*",
"@wrnexus/captcha": "workspace:*",
"@wrnexus/core": "workspace:*",
"@wrnexus/validation": "workspace:*"
},
"devDependencies": {
"@iconify-json/lucide": "^1.2.0",
"@iconify/tailwind4": "^1.0.0",
"@tailwindcss/cli": "^4.0.0",
"tailwindcss": "^4.0.0",
"typescript": "^5.9.2"
}
}
@@ -0,0 +1,93 @@
import { expect, test } from "bun:test";
import { existsSync, readFileSync } from "node:fs";
import { join } from "node:path";
const root = join(import.meta.dir, "..");
const authPackageRoot = join(root, "..", "..", "packages", "auth");
function read(...parts: string[]): string {
return readFileSync(join(...parts), "utf8");
}
test("auth showcase contains registration, login, recovery, and protected account pages", () => {
for (const page of [
"index.wrn",
"sign-in.wrn",
"sign-up.wrn",
"recover.wrn",
"account.wrn",
"otp.wrn",
"magic-link.wrn",
"two-factor.wrn",
"authenticator.wrn",
"invitation.wrn",
"impersonation.wrn",
]) {
expect(existsSync(join(root, "app", "pages", page))).toBe(true);
}
const account = read(root, "app", "pages", "account.wrn");
const signIn = read(root, "app", "pages", "sign-in.wrn");
expect(account).toContain("PasskeyButton");
expect(account).toContain("AccountStatus");
expect(account).toContain('action="/api/auth/logout"');
expect(account).toContain('data-schema="auth-empty"');
expect(signIn).toContain("<Captcha");
expect(signIn).toContain('action="auth-login"');
expect(signIn).toContain('required="true"');
expect(signIn).toContain('resetOnError="false"');
expect(existsSync(join(root, "app", "middleware", "captcha-login.ts"))).toBe(true);
expect(existsSync(join(root, "app", "api", "captcha", "[...path].ts"))).toBe(true);
expect(read(root, "app", "middleware", "captcha-login.ts")).toContain(
"verifiedForMs: 5 * 60_000",
);
});
test("auth showcase uses package-owned routes and advanced components", () => {
const auth = read(root, "app", "lib", "auth.ts");
const config = read(root, "wrnexus.config.ts");
expect(auth).toContain("createAuthEngine");
expect(auth).toContain("onSignedIn");
expect(auth).toContain("onSignedOut");
expect(auth).toContain("onSuccessfulSignUp");
expect(auth).toContain("autoSignIn: true");
expect(config).toContain("engine: auth");
expect(config).toContain("routes: true");
expect(config).toContain("migrations: false");
expect(config).not.toContain("onSignedIn");
expect(config).not.toContain("onSignedOut");
expect(existsSync(join(root, "app", "api", "auth", "register.ts"))).toBe(false);
expect(existsSync(join(root, "app", "schemas", "auth-register.ts"))).toBe(false);
expect(read(root, "app", "pages", "otp.wrn")).toContain("OtpSignIn");
expect(read(root, "app", "pages", "magic-link.wrn")).toContain("MagicLinkSignIn");
expect(read(root, "app", "pages", "impersonation.wrn")).toContain("ImpersonationBanner");
});
test("package auth schemas are shared by browser forms and API handlers", () => {
const validation = read(authPackageRoot, "src", "validation.ts");
const plugin = read(authPackageRoot, "src", "plugin.ts");
const handlers = read(authPackageRoot, "src", "http", "index.ts");
const signUp = read(authPackageRoot, "components", "SignUp.wrn");
const forgotPassword = read(authPackageRoot, "components", "ForgotPassword.wrn");
expect(validation).toContain("export const signUpSchema");
expect(validation).toContain("export const authSchemas");
expect(validation).toContain("export const authBrowserSchemaMap");
expect(plugin).toContain("authBrowserSchemaDescriptors");
expect(plugin).toContain("setDefaultAuthEngine(value.engine)");
expect(handlers).toContain("parseBody(schemas.register, ctx.req)");
expect(handlers).toContain("parseBody(schemas.passwordResetRequest, ctx.req)");
expect(signUp).toContain('schema = "auth-register"');
expect(signUp).toContain("data-schema='{schema}'");
expect(signUp).toContain("novalidate");
expect(forgotPassword).toContain('schema = "auth-password-request"');
expect(forgotPassword).toContain("data-schema='{schema}'");
expect(forgotPassword).toContain("novalidate");
});
+5
View File
@@ -0,0 +1,5 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": { "noEmit": true },
"include": ["app/**/*.ts", "test/**/*.ts", "wrnexus.config.ts"]
}
+61
View File
@@ -0,0 +1,61 @@
import type { AuthConfig } from "@wrnexus/auth";
import type { AppConfig } from "@wrnexus/styles";
import { auth } from "./app/lib/auth.ts";
const config = {
seo: {
title: "WRNexus Auth Showcase",
description: "Complete authentication, MFA, passkey, recovery, session, and security examples.",
canonicalBase: "http://localhost:3000",
robots: "noindex,nofollow",
},
theme: { palette: "amber", default: "dark" },
auth: {
engine: auth,
migrations: false,
routes: true,
middleware: true,
components: true,
baseUrl: "http://localhost:3000",
},
styles: {
entry: "app/styles/global.css",
process: async ({ entryPath, appRoot, mode }) => {
if (!entryPath) {
throw new Error("Auth showcase stylesheet entry was not resolved.");
}
const args = ["@tailwindcss/cli", "-i", entryPath];
if (mode === "production") {
args.push("--minify");
}
const result = await Bun.$.cwd(appRoot)`bunx ${args}`.nothrow().quiet();
const css = result.stdout.toString();
const diagnostics = result.stderr.toString().trim();
if (result.exitCode !== 0) {
throw new Error(diagnostics || css || "Tailwind CSS processing failed.");
}
const doneLine = diagnostics
.split(/\r?\n/)
.map((line) => line.trim())
.filter(Boolean)
.reverse()
.find((line) => /^done in\b/i.test(line));
console.log(doneLine ? ` ✓ Tailwind CSS: ${doneLine}` : " ✓ Tailwind CSS: compiled");
return css;
},
failureMode: "throw",
},
} satisfies AppConfig & { auth: AuthConfig };
export default config;