release: WRNexusJS 0.8.0
Quality / quality (ubuntu-latest) (push) Failing after 21s
Quality / quality (windows-latest) (push) Canceled after 0s

This commit is contained in:
2026-08-02 23:18:51 +05:30
parent 87507edf59
commit 586a6db8ff
625 changed files with 243608 additions and 11210 deletions
+11
View File
@@ -368,3 +368,14 @@ bun run validate:auth
```
Read [SECURITY.md](./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`.
+23 -7
View File
@@ -1,11 +1,27 @@
component AccountStatus {
props { status = "active" title = "Account status" activeMessage = "Your account is active and ready to use." pendingMessage = "Verify your contact details to activate your account." lockedMessage = "Your account is temporarily locked for security." disabledMessage = "Your account has been disabled." supportHref = "/support" color = "primary" size = "md" class = "" }
props {
status: string = "active"
title: string = "Account status"
activeMessage: string = "Your account is active and ready to use."
pendingMessage: string = "Verify your contact details to activate your account."
lockedMessage: string = "Your account is temporarily locked for security."
disabledMessage: string = "Your account has been disabled."
supportHref: string = "/support"
color: string = "primary"
size: string = "md"
class: string = ""
}
view {
<section {...attrs} data-status='{status}' 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-center {class}'>
<span class='mx-auto flex size-14 items-center justify-center rounded-full bg-[var(--wire-color-surface-2)] text-[var(--wire-color-primary)] data-[status=locked]:text-[var(--wire-color-warning)] data-[status=disabled]:text-[var(--wire-color-danger)]'>{#if status == "active"}<span class="icon-[lucide--circle-check] size-7"></span>{:else if status == "pending"}<span class="icon-[lucide--clock-3] size-7"></span>{:else}<span class="icon-[lucide--shield-alert] size-7"></span>{/if}</span>
<h1 class="mb-0 mt-4 text-2xl font-semibold">{title}</h1>
<p class="mx-auto mt-2 max-w-md text-sm text-[var(--wire-color-muted)]">{status == "active" ? activeMessage : status == "pending" ? pendingMessage : status == "locked" ? lockedMessage : disabledMessage}</p>
{#if status != "active"}<a href='{supportHref}' class="mt-4 inline-flex h-10 items-center justify-center rounded-[var(--wire-radius-sm)] bg-[var(--wire-color-primary)] px-4 text-sm font-semibold text-white">Contact support</a>{/if}
</section>
<Card {...attrs} title='{title}' color='{color}' size='{size}' align="center" class='w-full max-w-lg {class}'>
<div class="flex flex-col items-center text-center" data-status='{status}'>
<span class='flex size-14 items-center justify-center rounded-full bg-[var(--wire-color-surface-2)] text-[var(--wire-color-primary)]'>
{#if status == "active"}<span class="icon-[lucide--circle-check] size-7"></span>{:else if status == "pending"}<span class="icon-[lucide--clock-3] size-7"></span>{:else}<span class="icon-[lucide--shield-alert] size-7"></span>{/if}
</span>
<p class="mx-auto mt-3 max-w-md text-sm text-[var(--wire-color-muted)]">{status == "active" ? activeMessage : status == "pending" ? pendingMessage : status == "locked" ? lockedMessage : disabledMessage}</p>
<Badge label='{status}' color='{status == "active" ? "success" : status == "pending" ? "warning" : "danger"}' variant="soft" size="sm" />
{#if status != "active"}<Button href='{supportHref}' label="Contact support" color='{color}' size='{size}' class="mt-4" />{/if}
</div>
</Card>
}
}
@@ -0,0 +1,43 @@
component AuthProviderButtons {
outputs {
select(payload: { provider: string; href: string })
}
props {
providers: unknown[] = []
title: string = "Continue with"
dividerLabel: string = "or"
color: string = "primary"
size: string = "md"
class: string = ""
}
functions {
client function choose(provider) {
output.select({ provider: provider.id || provider.name || provider.label, href: provider.href || "" })
}
}
view {
<div {...attrs} class='space-y-3 {class}'>
{#if title}<p class="m-0 text-sm font-semibold text-[var(--wire-color-text)]">{title}</p>{/if}
<div class="grid gap-2 sm:grid-cols-2">
{#each providers as provider}
<Button
label='{provider.label || provider.name || "Continue"}'
href='{provider.href || ""}'
icon='{provider.icon || "icon-[lucide--log-in]"}'
variant='{provider.variant || "outline"}'
color='{provider.color || color}'
size='{size}'
fullWidth="true"
@click='choose(provider)'
/>
{/each}
</div>
{#if dividerLabel}
<div class="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>{dividerLabel}</span><span class="h-px flex-1 bg-[var(--wire-color-border)]"></span></div>
{/if}
</div>
}
}
@@ -0,0 +1,22 @@
component AuthSecurityNotice {
props {
title: string = "Security notice"
description: string = "Your session and credentials are protected by WRNexusJS security controls."
color: string = "info"
size: string = "sm"
class: string = ""
}
view {
<Alert
{...attrs}
title='{title}'
description='{description}'
icon="icon-[lucide--shield-check]"
color='{color}'
size='{size}'
variant="soft"
class='text-[var(--wire-color-text)] {class}'
/>
}
}
+37
View File
@@ -0,0 +1,37 @@
component AuthShell {
props {
title: string = "Welcome"
description: string = ""
eyebrow: string = ""
icon: string = "icon-[lucide--shield-check]"
footer: string = ""
color: string = "primary"
size: string = "md"
maxWidth: string = "md"
class: string = ""
}
view {
<Card
{...attrs}
title='{title}'
description='{description}'
header='{eyebrow}'
footer='{footer}'
color='{color}'
size='{size}'
class='w-full {class}'
class:max-w-sm='maxWidth === "sm"'
class:max-w-md='maxWidth === "md"'
class:max-w-lg='maxWidth === "lg"'
class:max-w-xl='maxWidth === "xl"'
class:max-w-2xl='maxWidth === "2xl"'
class:max-w-none='maxWidth === "full"'
>
<div class="mb-5 flex size-12 items-center justify-center rounded-full bg-[color-mix(in_srgb,var(--wire-color-primary)_12%,transparent)] text-[var(--wire-color-primary)]">
<span class='{icon}' aria-hidden="true"></span>
</div>
<slot></slot>
</Card>
}
}
+18 -19
View File
@@ -1,40 +1,39 @@
component DeviceSessions {
props {
sessions = []
currentSessionId = ""
title = "Active sessions"
description = "Review devices signed in to your account."
revokeAction = "/api/auth/sessions/revoke"
revokeSchema = "auth-session-revoke"
revokeLabel = "Sign out"
successMessage = "Session revoked."
color = "primary"
size = "md"
class = ""
sessions: unknown[] = []
currentSessionId: string = ""
title: string = "Active sessions"
description: string = "Review devices signed in to your account."
revokeAction: string = "/api/auth/sessions/revoke"
revokeSchema: string = "auth-session-revoke"
revokeLabel: string = "Sign out"
successMessage: string = "Session revoked."
color: string = "primary"
size: string = "md"
class: string = ""
}
view {
<section {...attrs} data-wrnexus-runtime="auth" class='w-full max-w-2xl rounded-[var(--wire-radius-lg)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface)] p-6 text-[var(--wire-color-text)] {class}'>
<h2 class="m-0 text-xl font-semibold">{title}</h2>
<p class="mt-1 text-sm text-[var(--wire-color-muted)]">{description}</p>
<div class="mt-5 divide-y divide-[var(--wire-color-border)]">
<Card {...attrs} title='{title}' description='{description}' color='{color}' size='{size}' class='w-full max-w-2xl {class}'>
<div class="divide-y divide-[var(--wire-color-border)]">
{#each sessions as session}
<article class="flex items-center gap-3 py-4">
<span class="flex size-10 items-center justify-center rounded-full bg-[var(--wire-color-surface-2)]"><span class="icon-[lucide--monitor-smartphone] size-5"></span></span>
<Avatar fallback='{session.userAgent || "Device"}' icon="icon-[lucide--monitor-smartphone]" size="sm" color='{color}' />
<div class="min-w-0 flex-1"><p class="m-0 truncate text-sm font-semibold">{session.userAgent || "Unknown device"}</p><p class="m-0 mt-1 text-xs text-[var(--wire-color-muted)]">{session.ip || "Unknown IP"} · Last active {session.lastSeenAt}</p></div>
{#if session.id == currentSessionId}
<span class="rounded-full bg-[color-mix(in_srgb,var(--wire-color-success)_12%,transparent)] px-2 py-1 text-xs font-semibold text-[var(--wire-color-success)]">Current</span>
<Badge label="Current" color="success" variant="soft" size="sm" />
{:else}
<form method="post" action='{revokeAction}' data-schema='{revokeSchema}' novalidate>
<input type="hidden" name="sessionId" value='{session.id}' />
<p data-error="sessionId" 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>
<p data-success='{successMessage}' role="status" hidden class="m-0 text-xs text-[var(--wire-color-success)]">{successMessage}</p>
<button type="submit" class="text-sm font-semibold text-[var(--wire-color-danger)] disabled:opacity-60">{revokeLabel}</button>
<Button type="submit" label='{revokeLabel}' color="danger" variant="ghost" size="sm" />
</form>
{/if}
</article>
{/each}
</div>
</section>
</Card>
}
}
@@ -1,22 +1,25 @@
component ImpersonationBanner {
props {
visible = true
targetName = "this user"
stopAction = "/api/auth/impersonation/stop"
stopSchema = "auth-empty"
redirect = "/account"
message = "You are viewing the application as"
stopLabel = "Stop impersonating"
color = "warning"
size = "md"
class = ""
visible: boolean = true
targetName: string = "this user"
stopAction: string = "/api/auth/impersonation/stop"
stopSchema: string = "auth-empty"
redirect: string = "/account"
message: string = "You are viewing the application as"
stopLabel: string = "Stop impersonating"
color: string = "warning"
size: string = "md"
class: string = ""
}
view {
{#if visible}
<aside {...attrs} data-wrnexus-runtime="auth" role="status" class='flex w-full flex-wrap items-center justify-between gap-3 border-b border-[var(--wire-color-warning)] bg-[color-mix(in_srgb,var(--wire-color-warning)_14%,var(--wire-color-surface))] px-4 py-2 text-sm text-[var(--wire-color-text)] {class}'>
<span class="flex items-center gap-2"><span class="icon-[lucide--scan-face] size-4 text-[var(--wire-color-warning)]"></span><span>{message} <strong>{targetName}</strong>.</span></span>
<form method="post" action='{stopAction}' data-schema='{stopSchema}' data-redirect='{redirect}' novalidate><p data-error="_form" role="alert" class="m-0 hidden text-xs text-[var(--wire-color-danger)]"></p><button type="submit" class="inline-flex h-8 items-center rounded-[var(--wire-radius-sm)] bg-[var(--wire-color-warning)] px-3 text-xs font-semibold text-black disabled:opacity-60">{stopLabel}</button></form>
</aside>
<Alert {...attrs} title='{message + " " + targetName}' icon="icon-[lucide--scan-face]" color='{color}' size='{size}' variant="soft" class='{class}'>
<form method="post" action='{stopAction}' data-schema='{stopSchema}' data-redirect='{redirect}' novalidate class="mt-2">
<p data-error="_form" role="alert" class="m-0 hidden text-xs text-[var(--wire-color-danger)]"></p>
<Button type="submit" label='{stopLabel}' color='{color}' size="sm" />
</form>
</Alert>
{/if}
}
}
+35 -26
View File
@@ -1,33 +1,42 @@
component PasskeyButton {
props {
mode = "authenticate"
label = "Continue with a passkey"
registerLabel = "Add a passkey"
identifier = ""
rpId = ""
rpName = "WRNexusJS"
passkeyName = "Passkey"
optionsEndpoint = ""
verifyEndpoint = ""
redirect = ""
mfaHref = "/two-factor"
conditional = false
fullWidth = false
loadingMessage = "Waiting for your passkey…"
successMessage = "Passkey verified."
color = "primary"
size = "md"
class = ""
@event passkeyRegistered = function
@event passkeyAuthenticated = function
@event error = function
outputs {
passkeyRegistered(payload: { credentialId: string; response?: object })
passkeyAuthenticated(payload: { userId?: string; sessionId?: string; response?: object })
error(payload: { code: string; message: string })
}
props {
mode: string = "authenticate"
label: string = "Continue with a passkey"
registerLabel: string = "Add a passkey"
identifier: string = ""
rpId: string = ""
rpName: string = "WRNexusJS"
passkeyName: string = "Passkey"
optionsEndpoint: string = ""
verifyEndpoint: string = ""
redirect: string = ""
mfaHref: string = "/two-factor"
conditional: boolean = false
fullWidth: boolean = false
loadingMessage: string = "Waiting for your passkey…"
successMessage: string = "Passkey verified."
color: string = "primary"
size: string = "md"
class: string = ""
}
view {
<div {...attrs} data-wrnexus-runtime="auth" data-auth-passkey='{mode}' data-identifier='{identifier}' data-rp-id='{rpId}' data-rp-name='{rpName}' data-passkey-name='{passkeyName}' data-options-endpoint='{optionsEndpoint}' data-verify-endpoint='{verifyEndpoint}' data-redirect='{redirect}' data-mfa-href='{mfaHref}' data-conditional='{conditional}' data-loading-message='{loadingMessage}' data-success-message='{successMessage}' data-auth-state="idle" aria-busy="false" class='space-y-2 {class}'>
<button type="button" class='inline-flex h-11 items-center justify-center gap-2 rounded-[var(--wire-radius-sm)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface)] px-4 text-sm font-semibold text-[var(--wire-color-text)] hover:bg-[var(--wire-color-surface-2)] disabled:cursor-wait disabled:opacity-60 {fullWidth ? "w-full" : ""}'>
<span aria-hidden="true" class="icon-[lucide--key-round] size-4"></span>
{mode == "register" ? registerLabel : label}
</button>
<Button
type="button"
label='{mode == "register" ? registerLabel : label}'
icon="icon-[lucide--key-round]"
variant="outline"
color='{color}'
size='{size}'
fullWidth='{fullWidth}'
/>
<p data-auth-status hidden role="status" class="m-0 text-xs text-[var(--wire-color-muted)]"></p>
</div>
}
+22 -73
View File
@@ -1,86 +1,35 @@
component RecoveryCodes {
props {
codes = []
schema = "auth-recovery-codes"
title = "Recovery codes"
description = "Store these codes somewhere safe. Each code can be used once."
downloadLabel = "Download codes"
regenerateLabel = "Generate new codes"
regenerateAction = "/api/auth/recovery-codes"
successMessage = "New recovery codes generated. Previous unused codes are no longer valid."
count = 10
color = "primary"
size = "md"
class = ""
codes: unknown[] = []
schema: string = "auth-recovery-codes"
title: string = "Recovery codes"
description: string = "Store these codes somewhere safe. Each code can be used once."
downloadLabel: string = "Download codes"
regenerateLabel: string = "Generate new codes"
regenerateAction: string = "/api/auth/recovery-codes"
successMessage: string = "New recovery codes generated. Previous unused codes are no longer valid."
count: number = 10
color: string = "primary"
size: string = "md"
class: string = ""
}
view {
<section
{...attrs}
data-wrnexus-runtime="auth"
data-auth-recovery-codes
data-recovery-filename="wrnexus-recovery-codes.txt"
class='w-full max-w-xl rounded-[var(--wire-radius-lg)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface)] p-6 {class}'
>
<div class="flex items-start justify-between gap-4">
<div>
<h2 class="m-0 text-xl font-semibold">{title}</h2>
<p class="mt-1 text-sm text-[var(--wire-color-muted)]">{description}</p>
</div>
<span class="icon-[lucide--shield-keyhole] size-6 text-[var(--wire-color-primary)]"></span>
<Card {...attrs} title='{title}' description='{description}' color='{color}' size='{size}' class='w-full max-w-xl {class}' data-wrnexus-runtime="auth" data-auth-recovery-codes data-recovery-filename="wrnexus-recovery-codes.txt">
<Alert title="Keep these codes private" description="Anyone with a recovery code may be able to access your account." icon="icon-[lucide--shield-alert]" color="warning" variant="soft" size="sm" />
<div data-recovery-code-list class="mt-4 grid grid-cols-2 gap-2 rounded-[var(--wire-radius-sm)] bg-[var(--wire-color-surface-2)] p-4 font-mono text-sm sm:grid-cols-3">
{#each codes as code}<code data-recovery-code class="rounded bg-[var(--wire-color-surface)] px-2 py-1.5 text-center">{code}</code>{/each}
</div>
<div
data-recovery-code-list
class="mt-5 grid grid-cols-2 gap-2 rounded-[var(--wire-radius-sm)] bg-[var(--wire-color-surface-2)] p-4 font-mono text-sm sm:grid-cols-3"
>
{#each codes as code}
<code
data-recovery-code
class="rounded bg-[var(--wire-color-surface)] px-2 py-1.5 text-center"
>{code}</code>
{/each}
</div>
<div class="mt-4 flex flex-wrap gap-2">
<button
type="button"
data-recovery-download
class="h-10 rounded-[var(--wire-radius-sm)] border border-[var(--wire-color-border)] px-4 text-sm font-semibold"
>
{downloadLabel}
</button>
<form
method="post"
action='{regenerateAction}'
data-schema='{schema}'
novalidate
class="flex-1"
>
<Button type="button" label='{downloadLabel}' icon="icon-[lucide--download]" variant="outline" color='{color}' size='{size}' data-recovery-download />
<form method="post" action='{regenerateAction}' data-schema='{schema}' novalidate class="min-w-48 flex-1">
<input type="hidden" name="count" value='{count}' />
<p data-error="count" 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>
<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-xs text-[var(--wire-color-success)]"
>
{successMessage}
</p>
<button
type="submit"
class="h-10 w-full rounded-[var(--wire-radius-sm)] border border-[var(--wire-color-border)] px-4 text-sm font-semibold disabled:cursor-wait disabled:opacity-60"
>
{regenerateLabel}
</button>
<p data-error="_form" role="alert" class="m-0 hidden text-xs text-[var(--wire-color-danger)]"></p>
<p data-success='{successMessage}' role="status" hidden class="m-0 text-xs text-[var(--wire-color-success)]">{successMessage}</p>
<Button type="submit" label='{regenerateLabel}' icon="icon-[lucide--refresh-cw]" variant="outline" color='{color}' size='{size}' fullWidth="true" />
</form>
</div>
</section>
</Card>
}
}
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/auth",
"version": "0.7.0",
"version": "0.8.0",
"description": "Complete authentication, account security, MFA, passkeys, recovery, devices, risk, and audit system for WRNexusJS.",
"type": "module",
"sideEffects": false,
@@ -52,7 +52,7 @@
"@wrnexus/db": "workspace:*"
},
"devDependencies": {
"@types/bun": "latest",
"@types/bun": "^1.3.14",
"@wrnexus/syntax": "workspace:*",
"typescript": "^5.9.2"
},
+89
View File
@@ -0,0 +1,89 @@
import type { Context } from "@wrnexus/core";
import type { AuthEngine } from "./engine.ts";
import type { AuthSession, AuthUser } from "./types.ts";
export type AuthRouteName =
| "signIn"
| "signUp"
| "signOut"
| "forgotPassword"
| "resetPassword"
| "verifyEmail"
| "verifyPhone"
| "twoFactor"
| "sessions"
| "passkeys";
const DEFAULT_AUTH_ROUTES: Record<AuthRouteName, string> = {
signIn: "/sign-in",
signUp: "/sign-up",
signOut: "/api/auth/logout",
forgotPassword: "/forgot-password",
resetPassword: "/reset-password",
verifyEmail: "/verify-email",
verifyPhone: "/verify-phone",
twoFactor: "/two-factor",
sessions: "/account/sessions",
passkeys: "/account/passkeys",
};
export function authRoute(
name: AuthRouteName,
options: { basePath?: string; overrides?: Partial<Record<AuthRouteName, string>> } = {},
): string {
const route = options.overrides?.[name] ?? DEFAULT_AUTH_ROUTES[name];
if (!options.basePath || route.startsWith("http://") || route.startsWith("https://"))
return route;
return `${options.basePath.replace(/\/$/, "")}/${route.replace(/^\//, "")}`;
}
export function authSuccess<T extends Record<string, unknown>>(
data: T,
init: ResponseInit = {},
): Response {
return Response.json({ ok: true, ...data }, { status: init.status ?? 200, ...init });
}
export function authFailure(
code: string,
message: string,
status = 400,
details?: Record<string, unknown>,
): Response {
return Response.json(
{ ok: false, error: { code, message, ...(details ? { details } : {}) } },
{ status },
);
}
export function requireAuthUser(ctx: Context): AuthUser {
if (!ctx.user || typeof ctx.user !== "object") {
throw new Response("Unauthorized", { status: 401 });
}
return ctx.user as AuthUser;
}
export function optionalAuthUser(ctx: Context): AuthUser | null {
return ctx.user && typeof ctx.user === "object" ? (ctx.user as AuthUser) : null;
}
export async function currentAuthSession(
engine: AuthEngine,
sessionId: string | undefined,
): Promise<AuthSession | null> {
if (!sessionId) return null;
const session = await engine.store.findSession(sessionId);
return session ?? null;
}
export function authComponentProps(
input: Record<string, unknown>,
defaults: { color?: string; size?: string; class?: string } = {},
): Record<string, unknown> {
return {
color: defaults.color ?? "primary",
size: defaults.size ?? "md",
class: defaults.class ?? "",
...input,
};
}
+2
View File
@@ -101,3 +101,5 @@ export {
type AuthSchemaSet,
type AuthSchemaOverrides,
} from "./validation.ts";
export * from "./helpers.ts";
+1 -1
View File
@@ -297,7 +297,7 @@ export function authPlugin(options: AuthPluginOptions = {}) {
return definePlugin({
name: "@wrnexus/auth",
version: "0.5.0",
version: "0.8.0",
enforce: "post",
componentDirs(context) {
+46 -1
View File
@@ -166,10 +166,55 @@ export interface LoginAttempt {
riskLevel: AuthRiskLevel;
}
export const AUTH_SECURITY_EVENT_TYPES = [
"account.registered",
"account.status-changed",
"delivery.failed",
"device.revoked",
"device.trusted",
"identity.email-verification-requested",
"identity.email-verified",
"identity.otp-verified",
"identity.phone-verification-requested",
"identity.phone-verified",
"impersonation.denied",
"impersonation.ended",
"impersonation.started",
"invitation.accepted",
"invitation.created",
"login.failed",
"login.magic-link",
"login.oauth",
"login.otp",
"login.otp-verified",
"login.passkey",
"login.succeeded",
"mfa.failed",
"mfa.otp-verified",
"mfa.recovery-code-used",
"mfa.recovery-codes-generated",
"mfa.succeeded",
"mfa.totp-disabled",
"mfa.totp-enabled",
"mfa.totp-verified",
"oauth.linked",
"oauth.unlinked",
"passkey.registered",
"password.changed",
"password.reset",
"password.reset-requested",
"session.revoked",
"session.revoked-all",
] as const;
export type KnownAuthSecurityEventType = (typeof AUTH_SECURITY_EVENT_TYPES)[number];
/** Known framework events plus application-defined extension events. */
export type AuthSecurityEventType = KnownAuthSecurityEventType | (string & {});
export interface AuthSecurityEvent {
id: string;
userId?: string;
type: string;
type: AuthSecurityEventType;
severity: "info" | "warning" | "critical";
actorUserId?: string;
sessionId?: string;
@@ -0,0 +1,32 @@
import { expect, test } from "bun:test";
import { readdirSync, readFileSync } from "node:fs";
import { join } from "node:path";
import { parse } from "@wrnexus/syntax";
const componentsDir = join(import.meta.dir, "..", "components");
test("all auth package components parse", () => {
for (const file of readdirSync(componentsDir).filter((name) => name.endsWith(".wrn"))) {
expect(() => parse(readFileSync(join(componentsDir, file), "utf8"))).not.toThrow();
}
});
test("auth form blocks use WRNexus UI controls", () => {
const formFiles = [
"SignIn.wrn",
"SignUp.wrn",
"ForgotPassword.wrn",
"ResetPassword.wrn",
"OtpSignIn.wrn",
"TwoFactorChallenge.wrn",
"VerifyEmail.wrn",
"VerifyPhone.wrn",
"InvitationAccept.wrn",
"MagicLinkSignIn.wrn",
];
for (const file of formFiles) {
const source = readFileSync(join(componentsDir, file), "utf8");
expect(source).toMatch(/<(?:Input|PinInput|StrongPassword|TogglePassword|Checkbox|Select)\b/);
expect(source).toContain("<Button");
}
});