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
@@ -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;
}