fix(auth): secure hydration and align password forms
Quality / quality (ubuntu-latest) (push) Failing after 6m7s
Quality / quality (windows-latest) (push) Canceled after 0s

This commit is contained in:
2026-08-11 17:05:04 +05:30
parent 743275e6fa
commit b1086d14e9
17 changed files with 77 additions and 27 deletions
+4 -4
View File
@@ -212,7 +212,7 @@
}, },
"packages/auth": { "packages/auth": {
"name": "@wrnexus/auth", "name": "@wrnexus/auth",
"version": "0.8.9", "version": "0.8.10",
"dependencies": { "dependencies": {
"@wrnexus/authz": "workspace:*", "@wrnexus/authz": "workspace:*",
"@wrnexus/captcha": "workspace:*", "@wrnexus/captcha": "workspace:*",
@@ -268,7 +268,7 @@
}, },
"packages/cli": { "packages/cli": {
"name": "@wrnexus/cli", "name": "@wrnexus/cli",
"version": "0.8.12", "version": "0.8.13",
"bin": { "bin": {
"wrnexus": "src/index.ts", "wrnexus": "src/index.ts",
}, },
@@ -332,7 +332,7 @@
}, },
"packages/dev-server": { "packages/dev-server": {
"name": "@wrnexus/dev-server", "name": "@wrnexus/dev-server",
"version": "0.8.11", "version": "0.8.12",
"dependencies": { "dependencies": {
"@wrnexus/authz": "workspace:*", "@wrnexus/authz": "workspace:*",
"@wrnexus/cache": "workspace:*", "@wrnexus/cache": "workspace:*",
@@ -617,7 +617,7 @@
}, },
"packages/ui": { "packages/ui": {
"name": "@wrnexus/ui", "name": "@wrnexus/ui",
"version": "0.8.10", "version": "0.8.11",
"dependencies": { "dependencies": {
"@wrnexus/core": "workspace:*", "@wrnexus/core": "workspace:*",
}, },
+3 -3
View File
@@ -1,10 +1,10 @@
// Global document layout. The framework renders this once around the selected // Global document layout. The framework renders this once around the selected
// page layout and merges SEO metadata, styles, and scripts into <head>/<body>. // page layout and merges SEO metadata, styles, and scripts into <head>/<body>.
// Request cookies, resolved theme, language, URL, and pathname are available // Resolved theme, language, URL, and pathname are available as safe SSR props.
// as SSR props, so document attributes do not need a client-side correction.
layout Document { layout Document {
runtime = "server"
props { props {
cookies = {}
theme = "light" theme = "light"
language = "en" language = "en"
url = "" url = ""
@@ -1,5 +1,7 @@
// Generated by scripts/generate-showcase.mjs. Do not edit directly. // Generated by scripts/generate-showcase.mjs. Do not edit directly.
layout Document { layout Document {
runtime = "server"
props { props {
cookies = {} cookies = {}
theme = "light" theme = "light"
@@ -1729,6 +1729,8 @@ layout Showcase {
function documentLayout() { function documentLayout() {
return `// Generated by scripts/generate-showcase.mjs. Do not edit directly. return `// Generated by scripts/generate-showcase.mjs. Do not edit directly.
layout Document { layout Document {
runtime = "server"
props { props {
cookies = {} cookies = {}
theme = "light" theme = "light"
@@ -1,4 +1,6 @@
layout Document { layout Document {
runtime = "server"
props { props {
language: string = "en" language: string = "en"
} }
@@ -1,10 +1,10 @@
// Global document layout. The framework renders this once around the selected // Global document layout. The framework renders this once around the selected
// page layout and merges SEO metadata, styles, and scripts into <head>/<body>. // page layout and merges SEO metadata, styles, and scripts into <head>/<body>.
// Request cookies, resolved theme, language, URL, and pathname are available // Resolved theme, language, URL, and pathname are available as safe SSR props.
// as SSR props, so document attributes do not need a client-side correction.
layout Document { layout Document {
runtime = "server"
props { props {
cookies = {}
theme = "light" theme = "light"
language = "en" language = "en"
url = "" url = ""
@@ -1,10 +1,10 @@
// Global document layout. The framework renders this once around the selected // Global document layout. The framework renders this once around the selected
// page layout and merges SEO metadata, styles, and scripts into <head>/<body>. // page layout and merges SEO metadata, styles, and scripts into <head>/<body>.
// Request cookies, resolved theme, language, URL, and pathname are available // Resolved theme, language, URL, and pathname are available as safe SSR props.
// as SSR props, so document attributes do not need a client-side correction.
layout Document { layout Document {
runtime = "server"
props { props {
cookies = {}
theme = "light" theme = "light"
language = "en" language = "en"
url = "" url = ""
+2 -1
View File
@@ -36,9 +36,10 @@ component SignIn {
<input type="hidden" name="deviceName" 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}" /> <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"> <div class="space-y-1.5">
<div class="flex justify-end"><a href='{forgotHref}' class="text-xs text-[var(--wire-color-primary)] hover:underline">Forgot password?</a></div>
<TogglePassword <TogglePassword
label="{passwordLabel}" label="{passwordLabel}"
cornerHint="Forgot password?"
cornerHref="{forgotHref}"
name="password" name="password"
placeholder="{passwordPlaceholder}" placeholder="{passwordPlaceholder}"
autocomplete="current-password" autocomplete="current-password"
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/auth", "name": "@wrnexus/auth",
"version": "0.8.9", "version": "0.8.10",
"description": "Complete authentication, account security, MFA, passkeys, recovery, devices, risk, and audit system for WRNexusJS.", "description": "Complete authentication, account security, MFA, passkeys, recovery, devices, risk, and audit system for WRNexusJS.",
"type": "module", "type": "module",
"sideEffects": false, "sideEffects": false,
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/cli", "name": "@wrnexus/cli",
"version": "0.8.12", "version": "0.8.13",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"exports": { "exports": {
+5 -3
View File
@@ -452,11 +452,13 @@ body {
`, `,
"app/layouts/document.wrn": `// Global document layout. The framework renders this once around the selected "app/layouts/document.wrn": `// Global document layout. The framework renders this once around the selected
// page layout and merges SEO metadata, styles, and scripts into <head>/<body>. // page layout and merges SEO metadata, styles, and scripts into <head>/<body>.
// Request cookies, resolved theme, language, URL, and pathname are available // Resolved theme, language, URL, and pathname are available as safe SSR props,
// as SSR props, so document attributes do not need a client-side correction. // so document attributes do not need a client-side correction. Request cookies
// remain server-private and are never passed into document hydration.
layout Document { layout Document {
runtime = "server"
props { props {
cookies = {}
theme = "light" theme = "light"
language = "en" language = "en"
url = "" url = ""
+4
View File
@@ -67,6 +67,10 @@ test("scaffoldApp includes the complete v0.8 configuration and starter structure
scaffoldApp(root, "complete-app"); scaffoldApp(root, "complete-app");
const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8")); const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
const config = readFileSync(join(root, "wrnexus.config.ts"), "utf8"); const config = readFileSync(join(root, "wrnexus.config.ts"), "utf8");
const documentLayout = readFileSync(join(root, "app/layouts/document.wrn"), "utf8");
expect(documentLayout).toContain('runtime = "server"');
expect(documentLayout).not.toContain("cookies =");
for (const packageName of [ for (const packageName of [
"@wrnexus/auth", "@wrnexus/auth",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/dev-server", "name": "@wrnexus/dev-server",
"version": "0.8.11", "version": "0.8.12",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"exports": { "exports": {
-1
View File
@@ -1785,7 +1785,6 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
if (typeof documentRender === "function") { if (typeof documentRender === "function") {
const rendered = String( const rendered = String(
documentRender({ documentRender({
cookies: ctx.cookies.getAll(),
theme: resolvedTheme, theme: resolvedTheme,
language, language,
url: ctx.url.toString(), url: ctx.url.toString(),
+13 -4
View File
@@ -174,7 +174,7 @@ component StrongPassword {
{#if requireLowercase}<li data-met="{password && hasLowercase() ? 'true' : 'false'}"><span aria-hidden="true"></span>One lowercase letter</li>{/if} {#if requireLowercase}<li data-met="{password && hasLowercase() ? 'true' : 'false'}"><span aria-hidden="true"></span>One lowercase letter</li>{/if}
{#if requireUppercase}<li data-met="{password && hasUppercase() ? 'true' : 'false'}"><span aria-hidden="true"></span>One uppercase letter</li>{/if} {#if requireUppercase}<li data-met="{password && hasUppercase() ? 'true' : 'false'}"><span aria-hidden="true"></span>One uppercase letter</li>{/if}
{#if requireNumber}<li data-met="{password && hasNumber() ? 'true' : 'false'}"><span aria-hidden="true"></span>One number</li>{/if} {#if requireNumber}<li data-met="{password && hasNumber() ? 'true' : 'false'}"><span aria-hidden="true"></span>One number</li>{/if}
{#if requireSpecialCharacter}<li data-met="{password && hasSpecialCharacter() ? 'true' : 'false'}"><span aria-hidden="true"></span>One special character: {specialCharactersSet}</li>{/if} {#if requireSpecialCharacter}<li class="wire-next__strong-password-special" data-met="{password && hasSpecialCharacter() ? 'true' : 'false'}"><span aria-hidden="true"></span>One special character: {specialCharactersSet}</li>{/if}
</ul> </ul>
{/if} {/if}
{#if hintText}<small id="{name}-hint">{hintText}</small>{/if} {#if hintText}<small id="{name}-hint">{hintText}</small>{/if}
@@ -205,7 +205,7 @@ component StrongPassword {
{#if requireLowercase}<li data-met="{password && hasLowercase() ? 'true' : 'false'}"><span aria-hidden="true"></span>One lowercase letter</li>{/if} {#if requireLowercase}<li data-met="{password && hasLowercase() ? 'true' : 'false'}"><span aria-hidden="true"></span>One lowercase letter</li>{/if}
{#if requireUppercase}<li data-met="{password && hasUppercase() ? 'true' : 'false'}"><span aria-hidden="true"></span>One uppercase letter</li>{/if} {#if requireUppercase}<li data-met="{password && hasUppercase() ? 'true' : 'false'}"><span aria-hidden="true"></span>One uppercase letter</li>{/if}
{#if requireNumber}<li data-met="{password && hasNumber() ? 'true' : 'false'}"><span aria-hidden="true"></span>One number</li>{/if} {#if requireNumber}<li data-met="{password && hasNumber() ? 'true' : 'false'}"><span aria-hidden="true"></span>One number</li>{/if}
{#if requireSpecialCharacter}<li data-met="{password && hasSpecialCharacter() ? 'true' : 'false'}"><span aria-hidden="true"></span>One special character: {specialCharactersSet}</li>{/if} {#if requireSpecialCharacter}<li class="wire-next__strong-password-special" data-met="{password && hasSpecialCharacter() ? 'true' : 'false'}"><span aria-hidden="true"></span>One special character: {specialCharactersSet}</li>{/if}
</ul> </ul>
{/if} {/if}
{#if hintText}<small id="{name}-hint">{hintText}</small>{/if} {#if hintText}<small id="{name}-hint">{hintText}</small>{/if}
@@ -236,8 +236,8 @@ component StrongPassword {
.wire-next--strong-password input { .wire-next--strong-password input {
width: 100%; width: 100%;
min-height: 2.75em; min-height: 2.65rem;
padding: 0.68em 0.85em; padding: 0.7rem 0.85rem;
border: 1px solid var(--wire-color-border); border: 1px solid var(--wire-color-border);
border-radius: var(--wire-radius-sm); border-radius: var(--wire-radius-sm);
outline: 0; outline: 0;
@@ -322,6 +322,15 @@ component StrongPassword {
line-height: 1.45; line-height: 1.45;
} }
.wire-next__strong-password-requirements .wire-next__strong-password-special {
grid-column: 1 / -1;
white-space: nowrap;
}
.wire-next__strong-password-special [data-text="specialCharactersSet"] {
white-space: nowrap;
}
.wire-next__strong-password-requirements li > span[aria-hidden="true"] { .wire-next__strong-password-requirements li > span[aria-hidden="true"] {
position: relative; position: relative;
width: 0.9rem; width: 0.9rem;
+29
View File
@@ -29,6 +29,8 @@ component TogglePassword {
invalid: boolean = false invalid: boolean = false
helpText: string = "" helpText: string = ""
validationMessage: string = "" validationMessage: string = ""
cornerHint: string = ""
cornerHref: string = ""
class: string = "" class: string = ""
} }
@@ -98,7 +100,12 @@ component TogglePassword {
{/each} {/each}
</div> </div>
{:else} {:else}
<div class="wire-next__password-heading">
<label for="{name}-password">{label}</label> <label for="{name}-password">{label}</label>
{#if cornerHint}
{#if cornerHref}<a href="{cornerHref}">{cornerHint}</a>{:else}<span>{cornerHint}</span>{/if}
{/if}
</div>
<div class="wire-next__password-control"> <div class="wire-next__password-control">
<input <input
{...attrs} {...attrs}
@@ -172,6 +179,28 @@ component TogglePassword {
font-weight: 600; font-weight: 600;
} }
.wire-next__password-heading {
display: flex;
min-width: 0;
align-items: center;
justify-content: space-between;
gap: 1rem;
color: var(--wire-color-text);
font-size: 0.75rem;
font-weight: 600;
}
.wire-next__password-heading :is(a, span) {
color: var(--wire-component-color);
font-size: 0.75rem;
font-weight: 500;
text-decoration: none;
}
.wire-next__password-heading a:hover {
text-decoration: underline;
}
.wire-next__password-fields { .wire-next__password-fields {
display: grid; display: grid;
gap: 1rem; gap: 1rem;
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/ui", "name": "@wrnexus/ui",
"version": "0.8.10", "version": "0.8.11",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",