release: WRNexusJS 0.5.10

This commit is contained in:
2026-07-30 13:36:29 +05:30
parent 8fc6f15402
commit d1b0c55b53
159 changed files with 7509 additions and 604 deletions
@@ -36,13 +36,9 @@ component AuthenticatorSetup {
novalidate
class='mt-5 space-y-4 {credentialId == "" ? "" : "hidden"}'
>
<div class="space-y-1.5">
<label for="auth-authenticator-label" class="block text-sm font-medium">Authenticator name</label>
<input id="auth-authenticator-label" name="label" value="Authenticator" autocomplete="off" class="h-11 w-full rounded-[var(--wire-radius-sm)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface-2)] px-3" />
<p data-error="label" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p>
</div>
<Input id="auth-authenticator-label" name="label" label="Authenticator name" value="Authenticator" autocomplete="off" icon="icon-[lucide--smartphone]" color="{color}" size="{size}" />
<p data-error="_form" role="alert" class="m-0 hidden rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-danger)_10%,transparent)] p-3 text-sm text-[var(--wire-color-danger)]"></p>
<button type="submit" class="h-11 w-full rounded-[var(--wire-radius-sm)] bg-[var(--wire-color-primary)] text-sm font-semibold text-white disabled:opacity-60">{setupLabel}</button>
<Button type="submit" label="{setupLabel}" variant="solid" color="{color}" size="{size}" fullWidth="true" />
</form>
<div data-auth-authenticator-details class='{credentialId == "" ? "hidden" : ""}'>
@@ -53,15 +49,11 @@ component AuthenticatorSetup {
</div>
<form method="post" action='{action}' data-schema='{schema}' data-auth-authenticator-confirm novalidate class="mt-5 space-y-4">
<input type="hidden" name="credentialId" value='{credentialId}' />
<div class="space-y-1.5">
<label for="auth-authenticator-code" class="block text-sm font-medium">Authenticator code</label>
<input id="auth-authenticator-code" name="code" inputmode="numeric" autocomplete="one-time-code" maxlength="6" class="h-12 w-full rounded-[var(--wire-radius-sm)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface-2)] px-3 text-center text-xl tracking-[0.35em]" />
<p data-error="code" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p>
</div>
<PinInput label="Authenticator code" name="code" length="6" inputMode="numeric" autocomplete="one-time-code" color="{color}" size="{size}" />
<p data-error="credentialId" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p>
<p data-error="_form" role="alert" class="m-0 hidden rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-danger)_10%,transparent)] p-3 text-sm text-[var(--wire-color-danger)]"></p>
<p data-success='{successMessage}' role="status" hidden class="m-0 rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-success)_10%,transparent)] p-3 text-sm text-[var(--wire-color-success)]">{successMessage}</p>
<button type="submit" class="h-11 w-full rounded-[var(--wire-radius-sm)] bg-[var(--wire-color-primary)] text-sm font-semibold text-white disabled:opacity-60">{submitLabel}</button>
<Button type="submit" label="{submitLabel}" variant="solid" color="{color}" size="{size}" fullWidth="true" />
</form>
</div>
</section>
+4 -6
View File
@@ -5,6 +5,7 @@ component ForgotPassword {
title = "Recover your account"
description = "Enter your email, phone, or username."
identifierLabel = "Account identifier"
identifierPlaceholder = "Enter your registered email, mobile number, or username"
submitLabel = "Send recovery link"
successMessage = "If an account matches, a recovery link has been sent."
signInHref = "/sign-in"
@@ -17,14 +18,11 @@ component ForgotPassword {
<h1 class="m-0 text-2xl font-semibold">{title}</h1>
<p class="mt-2 text-sm text-[var(--wire-color-muted)]">{description}</p>
<form method="post" action='{action}' data-schema='{schema}' novalidate class="mt-6 space-y-4">
<div class="space-y-1.5">
<label for="auth-recovery-identifier" class="block text-sm font-medium">{identifierLabel}</label>
<input id="auth-recovery-identifier" name="identifier" autocomplete="username" aria-describedby="auth-recovery-identifier-error" class="h-11 w-full rounded-[var(--wire-radius-sm)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface-2)] px-3 text-sm outline-none focus:border-[var(--wire-color-primary)]" />
<p id="auth-recovery-identifier-error" data-error="identifier" role="alert" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p>
</div>
<Input id="auth-recovery-identifier" name="identifier" label="{identifierLabel}" autocomplete="username" placeholder="{identifierPlaceholder}" icon="icon-[lucide--at-sign]" color="{color}" size="{size}" />
<slot></slot>
<p data-error="_form" role="alert" class="m-0 hidden rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-danger)_10%,transparent)] p-3 text-sm text-[var(--wire-color-danger)]"></p>
<p data-success='{successMessage}' role="status" hidden class="m-0 rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-success)_10%,transparent)] p-3 text-sm text-[var(--wire-color-success)]">{successMessage}</p>
<button type="submit" class="h-11 w-full rounded-[var(--wire-radius-sm)] bg-[var(--wire-color-primary)] px-4 text-sm font-semibold text-white disabled:cursor-wait disabled:opacity-60">{submitLabel}</button>
<Button type="submit" label="{submitLabel}" variant="solid" color="{color}" size="{size}" fullWidth="true" />
</form>
<a href='{signInHref}' class="mt-4 inline-flex text-sm text-[var(--wire-color-primary)] hover:underline">Back to sign in</a>
</section>
@@ -22,12 +22,12 @@ component InvitationAccept {
<div class="mt-5 rounded-[var(--wire-radius-sm)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface-2)] p-4 text-sm"><p class="m-0 font-semibold">{organization}</p><p class="mb-0 mt-1 text-[var(--wire-color-muted)]">Invited by {inviter}</p></div>
<form method="post" action='{action}' data-schema='{schema}' data-redirect='{redirect}' novalidate class="mt-5 space-y-4">
<input type="hidden" name="token" value='{token}' />
<div class="space-y-1.5"><label for="auth-invitation-name" class="block text-sm font-medium">Display name</label><input id="auth-invitation-name" name="displayName" autocomplete="name" class="h-11 w-full rounded-[var(--wire-radius-sm)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface-2)] px-3" /><p data-error="displayName" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p></div>
<div class="space-y-1.5"><label for="auth-invitation-password" class="block text-sm font-medium">Create password</label><input id="auth-invitation-password" name="password" type="password" autocomplete="new-password" class="h-11 w-full rounded-[var(--wire-radius-sm)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface-2)] px-3" /><p data-error="password" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p></div>
<Input id="auth-invitation-name" name="displayName" label="Display name" autocomplete="name" placeholder="Enter your display name" icon="icon-[lucide--user]" color="{color}" size="{size}" />
<StrongPassword label="Create password" name="password" autocomplete="new-password" minLength="12" requireLowercase="true" requireUppercase="true" requireNumber="true" requireSpecialCharacter="true" color="{color}" size="{size}" />
<p data-error="token" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p>
<p data-error="_form" role="alert" class="m-0 hidden rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-danger)_10%,transparent)] p-3 text-sm text-[var(--wire-color-danger)]"></p>
<p data-success='{successMessage}' role="status" hidden class="m-0 rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-success)_10%,transparent)] p-3 text-sm text-[var(--wire-color-success)]">{successMessage}</p>
<button type="submit" class="h-11 w-full rounded-[var(--wire-radius-sm)] bg-[var(--wire-color-primary)] text-sm font-semibold text-white disabled:cursor-wait disabled:opacity-60">{submitLabel}</button>
<Button type="submit" label="{submitLabel}" variant="solid" color="{color}" size="{size}" fullWidth="true" />
</form>
</section>
}
+2 -2
View File
@@ -18,10 +18,10 @@ component MagicLinkSignIn {
<h1 class="m-0 text-2xl font-semibold">{title}</h1>
<p class="mb-0 mt-2 text-sm text-[var(--wire-color-muted)]">{description}</p>
<form method="post" action='{action}' data-schema='{schema}' novalidate class="mt-6 space-y-4">
<div class="space-y-1.5"><label for="auth-magic-link-identifier" class="block text-sm font-medium">{emailLabel}</label><input id="auth-magic-link-identifier" name="identifier" type="email" autocomplete="email" class="h-11 w-full rounded-[var(--wire-radius-sm)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface-2)] px-3 text-sm outline-none focus:border-[var(--wire-color-primary)]" /><p data-error="identifier" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p></div>
<Input id="auth-magic-link-identifier" name="identifier" type="email" label="{emailLabel}" autocomplete="email" placeholder="Enter your email address" icon="icon-[lucide--mail]" color="{color}" size="{size}" />
<p data-error="_form" role="alert" class="m-0 hidden rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-danger)_10%,transparent)] p-3 text-sm text-[var(--wire-color-danger)]"></p>
<p data-success='{successMessage}' role="status" hidden class="m-0 rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-success)_10%,transparent)] p-3 text-sm text-[var(--wire-color-success)]">{successMessage}</p>
<button type="submit" class="h-11 w-full rounded-[var(--wire-radius-sm)] bg-[var(--wire-color-primary)] px-4 text-sm font-semibold text-white disabled:cursor-wait disabled:opacity-60">{submitLabel}</button>
<Button type="submit" label="{submitLabel}" variant="solid" color="{color}" size="{size}" fullWidth="true" />
</form>
<a href='{signInHref}' class="mt-4 inline-flex text-sm font-medium text-[var(--wire-color-primary)] hover:underline">Return to password sign-in</a>
</section>
+5 -13
View File
@@ -41,15 +41,11 @@ component OtpSignIn {
class='mt-6 space-y-4 {challengeId == "" ? "" : "hidden"}'
>
<input type="hidden" name="method" value='{method}' />
<div class="space-y-1.5">
<label for="auth-otp-identifier" class="block text-sm font-medium">{identifierLabel}</label>
<input id="auth-otp-identifier" name="identifier" type="text" autocomplete="username" class="h-11 w-full rounded-[var(--wire-radius-sm)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface-2)] px-3 text-sm outline-none focus:border-[var(--wire-color-primary)]" />
<p data-error="identifier" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p>
</div>
<Input id="auth-otp-identifier" name="identifier" type="text" label="{identifierLabel}" autocomplete="username" placeholder="Enter your email address or phone number" icon="icon-[lucide--at-sign]" color="{color}" size="{size}" />
<p data-error="method" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p>
<p data-error="_form" role="alert" class="m-0 hidden rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-danger)_10%,transparent)] p-3 text-sm text-[var(--wire-color-danger)]"></p>
<p data-success='{successMessage}' role="status" hidden class="m-0 rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-success)_10%,transparent)] p-3 text-sm text-[var(--wire-color-success)]">{successMessage}</p>
<button type="submit" class="h-11 w-full rounded-[var(--wire-radius-sm)] bg-[var(--wire-color-primary)] px-4 text-sm font-semibold text-white disabled:cursor-wait disabled:opacity-60">{requestLabel}</button>
<Button type="submit" label="{requestLabel}" variant="solid" color="{color}" size="{size}" fullWidth="true" />
</form>
<form
@@ -62,15 +58,11 @@ component OtpSignIn {
>
<input type="hidden" name="challengeId" value='{challengeId}' />
<input type="hidden" name="returnTo" value='{returnTo}' />
<div class="space-y-1.5">
<label for="auth-otp-code" class="block text-sm font-medium">One-time code</label>
<input id="auth-otp-code" name="code" inputmode="numeric" autocomplete="one-time-code" maxlength="6" class="h-12 w-full rounded-[var(--wire-radius-sm)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface-2)] px-3 text-center text-xl tracking-[0.35em] outline-none focus:border-[var(--wire-color-primary)]" />
<p data-error="code" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p>
</div>
<PinInput label="One-time code" name="code" length="6" inputMode="numeric" autocomplete="one-time-code" color="{color}" size="{size}" />
<p data-error="challengeId" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p>
<p data-error="_form" role="alert" class="m-0 hidden rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-danger)_10%,transparent)] p-3 text-sm text-[var(--wire-color-danger)]"></p>
<button type="submit" class="h-11 w-full rounded-[var(--wire-radius-sm)] bg-[var(--wire-color-primary)] px-4 text-sm font-semibold text-white disabled:cursor-wait disabled:opacity-60">{verifyLabel}</button>
<button type="button" data-auth-otp-back class="w-full text-sm text-[var(--wire-color-primary)] hover:underline">Use a different account</button>
<Button type="submit" label="{verifyLabel}" variant="solid" color="{color}" size="{size}" fullWidth="true" />
<Button type="button" data-auth-otp-back label="Use a different account" variant="ghost" color="{color}" size="{size}" fullWidth="true" />
</form>
</section>
}
+2 -7
View File
@@ -18,16 +18,11 @@ component ResetPassword {
<p class="mt-2 text-sm text-[var(--wire-color-muted)]">{description}</p>
<form method="post" action='{action}' data-schema='{schema}' data-redirect='{redirect}' novalidate class="mt-6 space-y-4">
<input type="hidden" name="token" value='{token}' />
<div class="space-y-1.5">
<label for="auth-reset-password" class="block text-sm font-medium">New password</label>
<input id="auth-reset-password" name="password" type="password" autocomplete="new-password" aria-describedby="auth-reset-password-error" class="h-11 w-full rounded-[var(--wire-radius-sm)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface-2)] px-3 outline-none focus:border-[var(--wire-color-primary)]" />
<p class="m-0 text-xs text-[var(--wire-color-muted)]">At least 12 characters with uppercase, lowercase, and a number.</p>
<p id="auth-reset-password-error" data-error="password" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p>
</div>
<StrongPassword label="New password" name="password" autocomplete="new-password" minLength="12" requireLowercase="true" requireUppercase="true" requireNumber="true" requireSpecialCharacter="true" color="{color}" size="{size}" />
<p data-error="token" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p>
<p data-error="_form" role="alert" class="m-0 hidden rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-danger)_10%,transparent)] p-3 text-sm text-[var(--wire-color-danger)]"></p>
<p data-success='{successMessage}' role="status" hidden class="m-0 rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-success)_10%,transparent)] p-3 text-sm text-[var(--wire-color-success)]">{successMessage}</p>
<button type="submit" class="h-11 w-full rounded-[var(--wire-radius-sm)] bg-[var(--wire-color-primary)] px-4 font-semibold text-white disabled:cursor-wait disabled:opacity-60">{submitLabel}</button>
<Button type="submit" label="{submitLabel}" variant="solid" color="{color}" size="{size}" fullWidth="true" />
</form>
</section>
}
+15 -10
View File
@@ -8,7 +8,9 @@ component SignIn {
title = "Welcome back"
description = "Sign in to continue to your account."
identifierLabel = "Email, phone, or username"
identifierPlaceholder = "Enter your email, mobile number, or username"
passwordLabel = "Password"
passwordPlaceholder = "Enter your password"
submitLabel = "Sign in"
forgotHref = "/recover"
signUpHref = "/sign-up"
@@ -29,22 +31,25 @@ component SignIn {
<input type="hidden" name="returnTo" value='{returnTo}' />
<input type="hidden" name="deviceFingerprint" value="" />
<input type="hidden" name="deviceName" value="" />
<Input id="auth-sign-in-identifier" name="identifier" type="text" label="{identifierLabel}" autocomplete="username webauthn" placeholder="{identifierPlaceholder}" icon="icon-[lucide--at-sign]" color="{color}" size="{size}" />
<div class="space-y-1.5">
<label for="auth-sign-in-identifier" class="block text-sm font-medium">{identifierLabel}</label>
<input id="auth-sign-in-identifier" name="identifier" type="text" autocomplete="username webauthn" aria-describedby="auth-sign-in-identifier-error" class="h-11 w-full rounded-[var(--wire-radius-sm)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface-2)] px-3 text-sm outline-none focus:border-[var(--wire-color-primary)] focus:ring-2 focus:ring-[color-mix(in_srgb,var(--wire-color-primary)_25%,transparent)]" />
<p id="auth-sign-in-identifier-error" data-error="identifier" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p>
</div>
<div class="space-y-1.5">
<div class="flex items-center justify-between gap-3 text-sm font-medium"><label for="auth-sign-in-password">{passwordLabel}</label><a href='{forgotHref}' class="text-xs text-[var(--wire-color-primary)] hover:underline">Forgot password?</a></div>
<input id="auth-sign-in-password" name="password" type="password" autocomplete="current-password" aria-describedby="auth-sign-in-password-error" class="h-11 w-full rounded-[var(--wire-radius-sm)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface-2)] px-3 text-sm outline-none focus:border-[var(--wire-color-primary)] focus:ring-2 focus:ring-[color-mix(in_srgb,var(--wire-color-primary)_25%,transparent)]" />
<p id="auth-sign-in-password-error" data-error="password" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p>
<div class="flex justify-end"><a href='{forgotHref}' class="text-xs text-[var(--wire-color-primary)] hover:underline">Forgot password?</a></div>
<TogglePassword
label="{passwordLabel}"
name="password"
placeholder="{passwordPlaceholder}"
autocomplete="current-password"
pattern=""
color="{color}"
size="{size}"
/>
</div>
{#if showRemember}
<label class="flex items-center gap-2 text-sm text-[var(--wire-color-muted)]"><input name="rememberDevice" type="checkbox" class="size-4 rounded border-[var(--wire-color-border)]" />Trust this device</label>
<Checkbox id="auth-sign-in-remember" name="rememberDevice" label="Trust this device" color="{color}" size="{size}" />
{/if}
<slot></slot>
<p data-error="_form" role="alert" class="m-0 hidden rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-danger)_10%,transparent)] p-3 text-sm text-[var(--wire-color-danger)]"></p>
<button type="submit" class="inline-flex h-11 w-full items-center justify-center rounded-[var(--wire-radius-sm)] bg-[var(--wire-color-primary)] px-4 text-sm font-semibold text-white transition-opacity hover:opacity-90 disabled:cursor-wait disabled:opacity-60">{submitLabel}</button>
<Button type="submit" label="{submitLabel}" variant="solid" color="{color}" size="{size}" fullWidth="true" />
</form>
{#if showPasskey}
<div class="my-4 flex items-center gap-3 text-xs text-[var(--wire-color-muted)]"><span class="h-px flex-1 bg-[var(--wire-color-border)]"></span><span>or</span><span class="h-px flex-1 bg-[var(--wire-color-border)]"></span></div>
+147 -42
View File
@@ -7,6 +7,13 @@ component SignUp {
description = "Use a strong password and verify your contact details."
submitLabel = "Create account"
signInHref = "/sign-in"
termsHref = "/legal/terms"
privacyHref = "/legal/privacy-policy"
namePlaceholder = "Enter your full name"
emailPlaceholder = "Enter your email address"
phonePlaceholder = "Enter your mobile number"
usernamePlaceholder = "Choose a username"
passwordPlaceholder = "Create a strong password"
showPhone = true
showUsername = true
requireConsent = true
@@ -15,10 +22,24 @@ component SignUp {
class = ""
}
view {
<section {...attrs} data-wrnexus-runtime="auth" class='w-full max-w-lg rounded-[var(--wire-radius-lg)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface)] p-6 text-[var(--wire-color-text)] shadow-[var(--wire-shadow-2)] {class}'>
<header class="mb-6 space-y-1.5">
<h1 class="m-0 text-2xl font-semibold">{title}</h1>
<p class="m-0 text-sm text-[var(--wire-color-muted)]">{description}</p>
<section
{...attrs}
data-wrnexus-runtime="auth"
class='w-full max-w-lg rounded-[var(--wire-radius-lg)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface)] p-6 text-[var(--wire-color-text)] shadow-[var(--wire-shadow-2)] {class}'
>
<header
class="mb-6 space-y-1.5"
>
<h1
class="m-0 text-2xl font-semibold"
>
{title}
</h1>
<p
class="m-0 text-sm text-[var(--wire-color-muted)]"
>
{description}
</p>
</header>
<form
@@ -29,62 +50,146 @@ component SignUp {
novalidate
class="grid gap-4 sm:grid-cols-2"
>
<div class="space-y-1.5 sm:col-span-2">
<label for="auth-sign-up-name" class="text-sm font-medium">Full name</label>
<input id="auth-sign-up-name" name="displayName" autocomplete="name" class="h-11 w-full rounded-[var(--wire-radius-sm)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface-2)] px-3 outline-none focus:border-[var(--wire-color-primary)]" />
<p data-error="displayName" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p>
</div>
<Input
id="auth-sign-up-name"
name="displayName"
label="Full name"
autocomplete="name"
placeholder="{namePlaceholder}"
icon="icon-[lucide--user]"
color="{color}"
size="{size}"
class="sm:col-span-2"
/>
<div class="space-y-1.5">
<label for="auth-sign-up-email" class="text-sm font-medium">Email</label>
<input id="auth-sign-up-email" name="email" type="email" autocomplete="email" class="h-11 w-full rounded-[var(--wire-radius-sm)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface-2)] px-3 outline-none focus:border-[var(--wire-color-primary)]" />
<p data-error="email" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p>
</div>
<Input
id="auth-sign-up-email"
name="email"
type="email"
label="Email"
autocomplete="email"
placeholder="{emailPlaceholder}"
icon="icon-[lucide--mail]"
color="{color}"
size="{size}"
/>
{#if showPhone}
<div class="space-y-1.5">
<label for="auth-sign-up-phone" class="text-sm font-medium">Phone</label>
<input id="auth-sign-up-phone" name="phone" type="tel" autocomplete="tel" class="h-11 w-full rounded-[var(--wire-radius-sm)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface-2)] px-3 outline-none focus:border-[var(--wire-color-primary)]" />
<p data-error="phone" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p>
</div>
<Input
id="auth-sign-up-phone"
name="phone"
type="tel"
label="Phone"
autocomplete="tel"
placeholder="{phonePlaceholder}"
icon="icon-[lucide--phone]"
color="{color}"
size="{size}"
/>
{/if}
{#if showUsername}
<div class="space-y-1.5 sm:col-span-2">
<label for="auth-sign-up-username" class="text-sm font-medium">Username</label>
<input id="auth-sign-up-username" name="username" autocomplete="username" class="h-11 w-full rounded-[var(--wire-radius-sm)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface-2)] px-3 outline-none focus:border-[var(--wire-color-primary)]" />
<p data-error="username" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p>
</div>
<Input
id="auth-sign-up-username"
name="username"
label="Username"
autocomplete="username"
placeholder="{usernamePlaceholder}"
icon="icon-[lucide--circle-user-round]"
color="{color}"
size="{size}"
class="sm:col-span-2"
/>
{/if}
<div class="space-y-1.5 sm:col-span-2">
<label for="auth-sign-up-password" class="text-sm font-medium">Password</label>
<input id="auth-sign-up-password" name="password" type="password" autocomplete="new-password" class="h-11 w-full rounded-[var(--wire-radius-sm)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface-2)] px-3 outline-none focus:border-[var(--wire-color-primary)]" />
<p class="m-0 text-xs text-[var(--wire-color-muted)]">At least 12 characters with uppercase, lowercase, and a number.</p>
<p data-error="password" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p>
<div
class="space-y-1.5 sm:col-span-2"
>
<StrongPassword
label="Password"
name="password"
placeholder="{passwordPlaceholder}"
autocomplete="new-password"
minLength="12"
requireLowercase="true"
requireUppercase="true"
requireNumber="true"
requireSpecialCharacter="true"
presentation="inline"
color="{color}"
size="{size}"
/>
</div>
{#if requireConsent}
<div class="space-y-1 sm:col-span-2">
<label class="flex items-start gap-2 text-sm text-[var(--wire-color-muted)]">
<input name="consent" type="checkbox" class="mt-0.5 size-4" />
<span>I agree to the terms and privacy policy.</span>
</label>
<p data-error="consent" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p>
</div>
<Checkbox
id="auth-sign-up-consent"
name="consent"
label="I agree to the"
color="{color}"
size="{size}"
class="sm:col-span-2"
>
<span
class="flex items-center"
>
<a
href="{termsHref}"
>
terms
</a>
and
<a
href="{privacyHref}"
>
privacy policy
</a>.
</span>
</Checkbox>
{:else}
<input type="hidden" name="consent" value="true" />
<input
type="hidden"
name="consent"
value="true"
/>
{/if}
<div class="sm:col-span-2"><slot></slot></div>
<div
class="sm:col-span-2"
>
<slot></slot>
</div>
<p data-error="_form" role="alert" class="m-0 hidden rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-danger)_10%,transparent)] p-3 text-sm text-[var(--wire-color-danger)] sm:col-span-2"></p>
<p data-success="Account created successfully." role="status" hidden class="m-0 rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-success)_10%,transparent)] p-3 text-sm text-[var(--wire-color-success)] sm:col-span-2"></p>
<p
data-error="_form"
role="alert"
class="m-0 hidden rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-danger)_10%,transparent)] p-3 text-sm text-[var(--wire-color-danger)] sm:col-span-2"
>
</p>
<p
data-success="Account created successfully."
role="status"
hidden
class="m-0 rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-success)_10%,transparent)] p-3 text-sm text-[var(--wire-color-success)] sm:col-span-2"
>
</p>
<button type="submit" class="h-11 rounded-[var(--wire-radius-sm)] bg-[var(--wire-color-primary)] px-4 text-sm font-semibold text-white sm:col-span-2 disabled:cursor-not-allowed disabled:opacity-60">{submitLabel}</button>
<Button
type="submit"
label="{submitLabel}"
variant="solid"
color="{color}"
size="{size}"
fullWidth="true"
class="sm:col-span-2"
/>
</form>
<p class="mb-0 mt-5 text-center text-sm text-[var(--wire-color-muted)]">Already have an account? <a href='{signInHref}' class="font-semibold text-[var(--wire-color-primary)] hover:underline">Sign in</a></p>
<p
class="mb-0 mt-5 text-center text-sm text-[var(--wire-color-muted)]"
>
Already have an account? <a href='{signInHref}' class="font-semibold text-[var(--wire-color-primary)] hover:underline">Sign in</a>
</p>
</section>
}
}
@@ -15,6 +15,12 @@ component TwoFactorChallenge {
size = "md"
class = ""
}
state methodOptions = [
{"label":"Authenticator app","value":"totp"},
{"label":"Recovery code","value":"recovery-code"},
{"label":"Email code","value":"email-otp"},
{"label":"SMS code","value":"sms-otp"}
]
view {
<section
{...attrs}
@@ -31,26 +37,13 @@ component TwoFactorChallenge {
<input type="hidden" name="mfaToken" value='{mfaToken}' />
<input type="hidden" name="challengeId" value='{challengeId}' />
<input type="hidden" name="returnTo" value='{returnTo}' />
<div class="space-y-1.5">
<label for="auth-mfa-method" class="block text-sm font-medium">Verification method</label>
<select id="auth-mfa-method" name="method" data-auth-mfa-method class="h-11 w-full rounded-[var(--wire-radius-sm)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface-2)] px-3">
<option value="totp">Authenticator app</option>
<option value="recovery-code">Recovery code</option>
<option value="email-otp">Email code</option>
<option value="sms-otp">SMS code</option>
</select>
<p data-error="method" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p>
</div>
<div class="space-y-1.5">
<label for="auth-mfa-code" class="block text-sm font-medium">Verification code</label>
<input id="auth-mfa-code" name="code" inputmode="numeric" autocomplete="one-time-code" class="h-12 w-full rounded-[var(--wire-radius-sm)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface-2)] px-3 text-center text-xl tracking-[0.35em]" />
<p data-error="code" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p>
</div>
<Select id="auth-mfa-method" name="method" data-auth-mfa-method label="Verification method" value="{method}" options="{methodOptions}" color="{color}" size="{size}" />
<PinInput label="Verification code" name="code" length="6" inputMode="numeric" autocomplete="one-time-code" color="{color}" size="{size}" />
<p data-auth-mfa-status role="status" hidden class="m-0 rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-primary)_10%,transparent)] p-3 text-sm text-[var(--wire-color-primary)]"></p>
<p data-error="mfaToken" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p>
<p data-error="challengeId" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p>
<p data-error="_form" role="alert" class="m-0 hidden rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-danger)_10%,transparent)] p-3 text-sm text-[var(--wire-color-danger)]"></p>
<button type="submit" class="h-11 w-full rounded-[var(--wire-radius-sm)] bg-[var(--wire-color-primary)] font-semibold text-white disabled:cursor-wait disabled:opacity-60">{submitLabel}</button>
<Button type="submit" label="{submitLabel}" variant="solid" color="{color}" size="{size}" fullWidth="true" />
</form>
<a href='{recoveryHref}' class="mt-4 inline-flex text-sm text-[var(--wire-color-primary)] hover:underline">Account recovery options</a>
</section>
+3 -3
View File
@@ -20,17 +20,17 @@ component VerifyEmail {
<h1 class="m-0 text-2xl font-semibold">{title}</h1>
<p class="mt-2 text-sm text-[var(--wire-color-muted)]">{description}</p>
<form method="post" action='{action}' data-schema='{schema}' novalidate class="mt-6 space-y-4">
<div class="space-y-1.5"><label for="auth-email-token" class="block text-sm font-medium">Verification token</label><input id="auth-email-token" name="token" value='{token}' autocomplete="one-time-code" class="h-11 w-full rounded-[var(--wire-radius-sm)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface-2)] px-3" /><p data-error="token" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p></div>
<Input id="auth-email-token" name="token" label="Verification token" value="{token}" autocomplete="one-time-code" placeholder="Enter your verification token" icon="icon-[lucide--badge-check]" color="{color}" size="{size}" />
<p data-error="_form" role="alert" class="m-0 hidden rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-danger)_10%,transparent)] p-3 text-sm text-[var(--wire-color-danger)]"></p>
<p data-success='{successMessage}' role="status" hidden class="m-0 rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-success)_10%,transparent)] p-3 text-sm text-[var(--wire-color-success)]">{successMessage}</p>
<button type="submit" class="h-11 w-full rounded-[var(--wire-radius-sm)] bg-[var(--wire-color-primary)] font-semibold text-white disabled:cursor-wait disabled:opacity-60">{submitLabel}</button>
<Button type="submit" label="{submitLabel}" variant="solid" color="{color}" size="{size}" fullWidth="true" />
</form>
<form method="post" action='{resendAction}' data-schema='{resendSchema}' novalidate class="mt-3">
<input type="hidden" name="type" value='{kind}' />
<input type="hidden" name="identifier" value='{identifier}' />
<p data-error="type" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p>
<p data-error="_form" role="alert" class="m-0 hidden text-xs text-[var(--wire-color-danger)]"></p>
<button type="submit" class="text-sm text-[var(--wire-color-primary)] hover:underline disabled:opacity-60">Send a new code</button>
<Button type="submit" label="Send a new code" variant="ghost" color="{color}" size="sm" />
</form>
</section>
}
+3 -3
View File
@@ -20,17 +20,17 @@ component VerifyPhone {
<h1 class="m-0 text-2xl font-semibold">{title}</h1>
<p class="mt-2 text-sm text-[var(--wire-color-muted)]">{description}</p>
<form method="post" action='{action}' data-schema='{schema}' novalidate class="mt-6 space-y-4">
<div class="space-y-1.5"><label for="auth-phone-token" class="block text-sm font-medium">Verification token</label><input id="auth-phone-token" name="token" value='{token}' autocomplete="one-time-code" inputmode="numeric" class="h-11 w-full rounded-[var(--wire-radius-sm)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface-2)] px-3" /><p data-error="token" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p></div>
<PinInput label="Verification token" name="token" value="{token}" length="6" inputMode="numeric" autocomplete="one-time-code" color="{color}" size="{size}" />
<p data-error="_form" role="alert" class="m-0 hidden rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-danger)_10%,transparent)] p-3 text-sm text-[var(--wire-color-danger)]"></p>
<p data-success='{successMessage}' role="status" hidden class="m-0 rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-success)_10%,transparent)] p-3 text-sm text-[var(--wire-color-success)]">{successMessage}</p>
<button type="submit" class="h-11 w-full rounded-[var(--wire-radius-sm)] bg-[var(--wire-color-primary)] font-semibold text-white disabled:cursor-wait disabled:opacity-60">{submitLabel}</button>
<Button type="submit" label="{submitLabel}" variant="solid" color="{color}" size="{size}" fullWidth="true" />
</form>
<form method="post" action='{resendAction}' data-schema='{resendSchema}' novalidate class="mt-3">
<input type="hidden" name="type" value='{kind}' />
<input type="hidden" name="identifier" value='{identifier}' />
<p data-error="type" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p>
<p data-error="_form" role="alert" class="m-0 hidden text-xs text-[var(--wire-color-danger)]"></p>
<button type="submit" class="text-sm text-[var(--wire-color-primary)] hover:underline disabled:opacity-60">Send a new code</button>
<Button type="submit" label="Send a new code" variant="ghost" color="{color}" size="sm" />
</form>
</section>
}
+3 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/auth",
"version": "0.5.1",
"version": "0.5.10",
"description": "Complete authentication, account security, MFA, passkeys, recovery, devices, risk, and audit system for WRNexusJS.",
"type": "module",
"sideEffects": false,
@@ -36,7 +36,7 @@
"scripts": {
"test": "bun test",
"typecheck": "tsc --noEmit",
"check": "bun run typecheck \u0026\u0026 bun run test"
"check": "bun run typecheck && bun run test"
},
"dependencies": {
"@wrnexus/authz": "workspace:*",
@@ -47,6 +47,7 @@
"@wrnexus/oauth": "workspace:*",
"@wrnexus/plugin": "workspace:*",
"@wrnexus/queue": "workspace:*",
"@wrnexus/ui": "workspace:*",
"@wrnexus/validation": "workspace:*",
"@wrnexus/db": "workspace:*"
},
+21 -1
View File
@@ -9,7 +9,11 @@ import {
getAuthUser,
} from "../middleware.ts";
import { resolveAuthSchemas, type AuthSchemaOverrides, type AuthSchemaSet } from "../validation.ts";
import type { AuthSignedInHandler, AuthSignedOutHandler } from "../types.ts";
import type {
AuthSessionVerificationHandler,
AuthSignedInHandler,
AuthSignedOutHandler,
} from "../types.ts";
function text(value: unknown): string {
return typeof value === "string" ? value : value == null ? "" : String(value);
@@ -53,6 +57,7 @@ export interface AuthHttpOptions {
onSignedIn?: AuthSignedInHandler;
/** @deprecated Prefer createAuthEngine({ onSignedOut }). */
onSignedOut?: AuthSignedOutHandler;
onSessionVerification?: AuthSessionVerificationHandler;
}
export function createAuthHttpHandlers(options: AuthHttpOptions) {
@@ -83,6 +88,21 @@ export function createAuthHttpHandlers(options: AuthHttpOptions) {
}
return {
async verifySession(ctx: Context): Promise<Response> {
const user = getAuthUser(ctx);
if (options.onSessionVerification) {
return options.onSessionVerification(ctx, user);
}
if (!user) return json({ ok: false, error: "Unauthorized" }, 401);
if ((ctx.req.headers.get("accept") ?? "").includes("application/json")) {
return json({ ok: true, user });
}
return new Response(null, {
status: 204,
headers: { "cache-control": "no-store" },
});
},
async register(ctx: Context): Promise<Response> {
const validation = await parseBody(schemas.register, ctx.req);
if (!validation.ok) return validation.response;
+9 -2
View File
@@ -12,9 +12,16 @@ function wantsJson(ctx: Context): boolean {
return accept.includes("application/json") && !accept.includes("text/html");
}
export function authSession(engine: AuthEngine): Middleware {
export interface AuthSessionOptions {
/** Optional shared SSO cookie containing an AuthEngine session id. */
cookieName?: string;
}
export function authSession(engine: AuthEngine, options: AuthSessionOptions = {}): Middleware {
return async (ctx, next) => {
const sessionId = ctx.session.get<string>(AUTH_SESSION_KEY);
const sessionId =
(options.cookieName ? ctx.cookies.get(options.cookieName) : undefined) ??
ctx.session.get<string>(AUTH_SESSION_KEY);
if (!sessionId) {
ctx.user = null;
ctx.locals.authUser = null;
+27 -5
View File
@@ -4,7 +4,11 @@ import { fileURLToPath } from "node:url";
import { definePlugin, type PluginContext } from "@wrnexus/plugin";
import type { AuthEngine } from "./engine.ts";
import type { AuthPasskeyHttpOptions } from "./http/index.ts";
import type { AuthSignedInHandler, AuthSignedOutHandler } from "./types.ts";
import type {
AuthSessionVerificationHandler,
AuthSignedInHandler,
AuthSignedOutHandler,
} from "./types.ts";
import { AUTH_ROUTE_DEFINITIONS, type AuthRouteGroup } from "./routes/definitions.ts";
import {
clearDefaultAuthEngine,
@@ -52,6 +56,10 @@ export interface AuthConfig {
onSignedIn?: AuthSignedInHandler;
/** @deprecated Prefer createAuthEngine({ onSignedOut }). */
onSignedOut?: AuthSignedOutHandler;
/** Shared SSO cookie whose value is an AuthEngine session id. */
sessionCookieName?: string;
/** Customize the package forward-auth verification response. */
onSessionVerification?: AuthSessionVerificationHandler;
}
/** Explicit plugin options remain supported for compatibility. Prefer config.auth. */
@@ -88,14 +96,22 @@ interface ResolvedAuthConfig {
passkey?: AuthPasskeyHttpOptions;
onSignedIn?: AuthSignedInHandler;
onSignedOut?: AuthSignedOutHandler;
sessionCookieName?: string;
onSessionVerification?: AuthSessionVerificationHandler;
}
const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
const moduleRoot = dirname(fileURLToPath(import.meta.url));
const packageRoot = dirname(moduleRoot);
const compiledPackage = moduleRoot === join(packageRoot, "dist");
const clientRuntime = join(packageRoot, "assets", "client", "auth.js");
const migrationsFile = join(packageRoot, "migrations", "001_auth.sql");
const otpPurposeMigrationFile = join(packageRoot, "migrations", "002_auth_otp_purpose.sql");
const apiRoutesDir = join(packageRoot, "src", "routes", "api");
const middlewareFile = join(packageRoot, "src", "routes", "middleware.ts");
const apiRoutesDir = compiledPackage
? join(moduleRoot, "routes", "api")
: join(packageRoot, "src", "routes", "api");
const middlewareFile = compiledPackage
? join(moduleRoot, "routes", "middleware.js")
: join(packageRoot, "src", "routes", "middleware.ts");
const resolvedConfigKey = "@wrnexus/auth:resolved-config";
export function authComponentsDir(): string {
@@ -104,7 +120,7 @@ export function authComponentsDir(): string {
function authApiRouteEntry(path: string): string {
const routeName = path.replace(/^\/api\/auth\/?/, "").replace(/\//g, "-") || "index";
return join(apiRoutesDir, routeName + ".ts");
return join(apiRoutesDir, routeName + (compiledPackage ? ".js" : ".ts"));
}
function resolveConfig(
@@ -142,6 +158,8 @@ function resolveConfig(
passkey: raw.passkey,
onSignedIn: raw.onSignedIn ?? raw.engine?.onSignedIn,
onSignedOut: raw.onSignedOut ?? raw.engine?.onSignedOut,
sessionCookieName: raw.sessionCookieName,
onSessionVerification: raw.onSessionVerification,
};
}
@@ -377,6 +395,10 @@ export function authPlugin(options: AuthPluginOptions = {}) {
onSignedIn: value.onSignedIn,
onSignedOut: value.onSignedOut,
sessionCookieName: value.sessionCookieName,
onSessionVerification: value.onSessionVerification,
});
context.metadata.set("@wrnexus/auth:component-dir", value.componentDir);
+1
View File
@@ -62,6 +62,7 @@ function handlersFor(ctx: Context): AuthHttpHandlers | undefined {
passkey: routeOptions.passkey,
onSignedIn: routeOptions.onSignedIn,
onSignedOut: routeOptions.onSignedOut,
onSessionVerification: routeOptions.onSessionVerification,
});
}
@@ -0,0 +1,6 @@
import type { Context } from "@wrnexus/core";
import { invokeAuthHandler } from "../api.ts";
export function GET(ctx: Context): Promise<Response> {
return invokeAuthHandler("verifySession", ctx);
}
+6
View File
@@ -23,6 +23,12 @@ export interface AuthRouteDefinition {
/** Single source of truth for package-contributed auth endpoints. */
export const AUTH_ROUTE_DEFINITIONS = [
{
path: "/api/auth/session/verify",
group: "sessions",
handler: "verifySession",
methods: ["GET"],
},
{ path: "/api/auth/register", group: "registration", handler: "register", methods: ["POST"] },
{ path: "/api/auth/login", group: "login", handler: "login", methods: ["POST"] },
{ path: "/api/auth/logout", group: "login", handler: "logout", methods: ["POST"] },
+8 -2
View File
@@ -1,11 +1,17 @@
import type { Middleware } from "@wrnexus/core";
import { authSession } from "../middleware.ts";
import { getDefaultAuthEngine, hasDefaultAuthEngine } from "../runtime.ts";
import {
getDefaultAuthEngine,
getDefaultAuthRouteOptions,
hasDefaultAuthEngine,
} from "../runtime.ts";
/** Package middleware: hydrates auth state when a default engine is configured. */
const middleware: Middleware = async (ctx, next) => {
if (!hasDefaultAuthEngine()) return next();
return authSession(getDefaultAuthEngine())(ctx, next);
return authSession(getDefaultAuthEngine(), {
cookieName: getDefaultAuthRouteOptions().sessionCookieName,
})(ctx, next);
};
export default middleware;
+3
View File
@@ -1,6 +1,7 @@
import type { Context } from "@wrnexus/core";
import type { AuthEngine } from "./engine.ts";
import type { AuthPasskeyHttpOptions } from "./http/index.ts";
import type { AuthSessionVerificationHandler } from "./types.ts";
import { resolveAuthSchemas, type AuthSchemaOverrides, type AuthSchemaSet } from "./validation.ts";
export interface DefaultAuthRouteOptions {
@@ -11,6 +12,8 @@ export interface DefaultAuthRouteOptions {
onSignedIn?: (ctx: Context, returnTo?: string) => Response | Promise<Response>;
onSignedOut?: (ctx: Context) => Response | Promise<Response>;
sessionCookieName?: string;
onSessionVerification?: AuthSessionVerificationHandler;
}
interface AuthRuntimeState {
+6
View File
@@ -241,6 +241,12 @@ export type AuthSignedInHandler = (ctx: Context, returnTo?: string) => Response
/** HTTP response hook invoked after the auth engine clears a signed-in session. */
export type AuthSignedOutHandler = (ctx: Context) => Response | Promise<Response>;
/** Forward-auth response hook. A null user means the request is unauthenticated. */
export type AuthSessionVerificationHandler = (
ctx: Context,
user: AuthPublicUser | null,
) => Response | Promise<Response>;
export interface AuthSuccessfulSignUpAction {
/**
* Run the normal login policy with the newly registered credentials and
+42
View File
@@ -51,6 +51,48 @@ test("passwordless and passkey components preserve MFA continuation metadata", (
expect(signIn).toContain('name="deviceName"');
});
test("recovery forms expose a form slot for CAPTCHA and policy controls", () => {
const source = readFileSync(join(directory, "ForgotPassword.wrn"), "utf8");
const formStart = source.indexOf("<form");
const formEnd = source.indexOf("</form>");
const slot = source.indexOf("<slot></slot>");
expect(formStart).toBeGreaterThanOrEqual(0);
expect(slot).toBeGreaterThan(formStart);
expect(slot).toBeLessThan(formEnd);
});
test("account forms use the dedicated password components", () => {
const signIn = readFileSync(join(directory, "SignIn.wrn"), "utf8");
const signUp = readFileSync(join(directory, "SignUp.wrn"), "utf8");
expect(signIn).toContain("<TogglePassword");
expect(signIn).not.toContain('id="auth-sign-in-password"');
expect(signUp).toContain("<StrongPassword");
expect(signUp).not.toContain('id="auth-sign-up-password"');
});
test("authentication journeys use themed UI form controls", () => {
const expected = {
"SignIn.wrn": ["<Input", "<TogglePassword", "<Checkbox", "<Button"],
"SignUp.wrn": ["<Input", "<StrongPassword", "<Checkbox", "<Button"],
"ForgotPassword.wrn": ["<Input", "<Button"],
"ResetPassword.wrn": ["<StrongPassword", "<Button"],
"MagicLinkSignIn.wrn": ["<Input", "<Button"],
"InvitationAccept.wrn": ["<Input", "<StrongPassword", "<Button"],
"OtpSignIn.wrn": ["<Input", "<PinInput", "<Button"],
"TwoFactorChallenge.wrn": ["<Select", "<PinInput", "<Button"],
"AuthenticatorSetup.wrn": ["<Input", "<PinInput", "<Button"],
"VerifyEmail.wrn": ["<Input", "<Button"],
"VerifyPhone.wrn": ["<PinInput", "<Button"],
};
for (const [file, controls] of Object.entries(expected)) {
const source = readFileSync(join(directory, file), "utf8");
for (const control of controls) expect(source).toContain(control);
}
});
test("verification components can resend without application-owned schema files", () => {
const email = readFileSync(join(directory, "VerifyEmail.wrn"), "utf8");
const phone = readFileSync(join(directory, "VerifyPhone.wrn"), "utf8");
+20
View File
@@ -178,6 +178,26 @@ test("config.auth registers package routes", async () => {
expect(contributions.migrations).toHaveLength(0);
});
test("config.auth contributes a reusable forward-auth verification route", async () => {
const runner = createPluginRunner(authPlugin(), {
root: process.cwd(),
mode: "development",
command: "dev",
metadata: new Map<string, unknown>(),
warn() {},
});
await runner.configure({
auth: {
engine: {} as never,
routes: true,
},
});
const contributions = await runner.contributions();
expect(contributions.routes.some((route) => route.path === "/api/auth/session/verify")).toBe(
true,
);
});
test("config.auth can disable route groups", async () => {
const metadata = new Map<string, unknown>();