Files
WRNexusJS/packages/auth/src/plugin.ts
T
Clintchiz 2c960fc1dc
Quality / quality (ubuntu-latest) (push) Failing after 9m49s
Quality / quality (windows-latest) (push) Canceled after 0s
refactor: migrate legacy wire namespace to wrn
2026-08-12 18:51:15 +05:30

437 lines
12 KiB
TypeScript

import { readFileSync } from "node:fs";
import { dirname, join } from "node:path";
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 {
AuthSessionVerificationHandler,
AuthSignedInHandler,
AuthSignedOutHandler,
} from "./types.ts";
import { AUTH_ROUTE_DEFINITIONS, type AuthRouteGroup } from "./routes/definitions.ts";
import {
clearDefaultAuthEngine,
setDefaultAuthEngine,
setDefaultAuthRouteOptions,
setDefaultAuthSchemas,
} from "./runtime.ts";
import {
authBrowserSchemaDescriptors,
resolveAuthSchemas,
type AuthSchemaOverrides,
type AuthSchemaSet,
} from "./validation.ts";
export interface AuthRoutesConfig {
enabled?: boolean;
registration?: boolean;
login?: boolean;
verification?: boolean;
password?: boolean;
invitations?: boolean;
magicLink?: boolean;
otp?: boolean;
mfa?: boolean;
sessions?: boolean;
impersonation?: boolean;
passkeys?: boolean;
}
export interface AuthConfig {
enabled?: boolean;
engine?: AuthEngine;
routes?: boolean | AuthRoutesConfig;
migrations?: boolean;
middleware?: boolean;
components?: boolean;
client?: boolean;
devToolbar?: boolean;
componentDir?: string;
schemas?: AuthSchemaOverrides;
baseUrl?: string;
csrf?: boolean;
passkey?: AuthPasskeyHttpOptions;
/** @deprecated Prefer createAuthEngine({ onSignedIn }). */
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. */
export interface AuthPluginOptions {
componentDir?: string;
exposeComponentDirectory?: boolean;
enableDevToolbar?: boolean;
includeMigrations?: boolean;
includeRoutes?: boolean;
includeMiddleware?: boolean;
}
export interface AuthAuditIssue {
id: string;
severity: "error" | "warning" | "suggestion";
title: string;
message: string;
file: string;
}
interface ResolvedAuthConfig {
enabled: boolean;
engine?: AuthEngine;
routes: boolean | AuthRoutesConfig;
migrations: boolean;
middleware: boolean;
components: boolean;
client: boolean;
devToolbar: boolean;
componentDir: string;
schemas: AuthSchemaSet;
baseUrl?: string;
csrf: boolean;
passkey?: AuthPasskeyHttpOptions;
onSignedIn?: AuthSignedInHandler;
onSignedOut?: AuthSignedOutHandler;
sessionCookieName?: string;
onSessionVerification?: AuthSessionVerificationHandler;
}
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 = 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 {
return join(packageRoot, "components");
}
function authApiRouteEntry(path: string): string {
const routeName = path.replace(/^\/api\/auth\/?/, "").replace(/\//g, "-") || "index";
return join(apiRoutesDir, routeName + (compiledPackage ? ".js" : ".ts"));
}
function resolveConfig(
config: Record<string, unknown>,
options: AuthPluginOptions,
): ResolvedAuthConfig {
const raw = (config.auth ?? {}) as AuthConfig;
const enabled = raw.enabled !== false;
const hasEngine = Boolean(raw.engine);
const hasDefaultDb = Boolean(config.db);
return {
enabled,
engine: raw.engine,
routes: options.includeRoutes !== undefined ? options.includeRoutes : (raw.routes ?? hasEngine),
migrations:
options.includeMigrations !== undefined
? options.includeMigrations
: (raw.migrations ?? (hasEngine && hasDefaultDb)),
middleware:
options.includeMiddleware !== undefined
? options.includeMiddleware
: (raw.middleware ?? hasEngine),
components:
options.exposeComponentDirectory !== undefined
? options.exposeComponentDirectory
: (raw.components ?? true),
client: raw.client ?? true,
devToolbar:
options.enableDevToolbar !== undefined ? options.enableDevToolbar : (raw.devToolbar ?? true),
componentDir: options.componentDir ?? raw.componentDir ?? authComponentsDir(),
schemas: resolveAuthSchemas(raw.schemas),
baseUrl: raw.baseUrl,
csrf: raw.csrf ?? true,
passkey: raw.passkey,
onSignedIn: raw.onSignedIn ?? raw.engine?.onSignedIn,
onSignedOut: raw.onSignedOut ?? raw.engine?.onSignedOut,
sessionCookieName: raw.sessionCookieName,
onSessionVerification: raw.onSessionVerification,
};
}
function fallbackConfig(options: AuthPluginOptions): ResolvedAuthConfig {
return {
enabled: true,
// Automatic package discovery must never expose authentication
// endpoints unless the application configures auth or the developer
// explicitly enables the routes through plugin options.
routes: options.includeRoutes ?? false,
migrations: options.includeMigrations ?? false,
middleware: options.includeMiddleware ?? false,
// Components and browser assets are safe to expose automatically.
components: options.exposeComponentDirectory ?? true,
client: true,
devToolbar: options.enableDevToolbar ?? true,
componentDir: options.componentDir ?? authComponentsDir(),
schemas: resolveAuthSchemas(),
csrf: true,
};
}
function resolved(context: PluginContext, options: AuthPluginOptions): ResolvedAuthConfig {
return (
(context.metadata.get(resolvedConfigKey) as ResolvedAuthConfig | undefined) ??
fallbackConfig(options)
);
}
function routeEnabled(routes: boolean | AuthRoutesConfig, group: AuthRouteGroup): boolean {
if (typeof routes === "boolean") return routes;
if (routes.enabled === false) return false;
return routes[group] !== false;
}
function authClientSource(schemas: AuthSchemaSet): string {
const descriptors = JSON.stringify(authBrowserSchemaDescriptors(schemas));
const runtime = readFileSync(clientRuntime, "utf8");
return `
(function () {
var defaults = ${descriptors};
window.__wrnSchemas =
window.__wrnSchemas || {};
Object.keys(defaults).forEach(
function (name) {
if (!(name in window.__wrnSchemas)) {
window.__wrnSchemas[name] =
defaults[name];
}
}
);
if (
window.__wrnValidate &&
typeof window.__wrnValidate
.registerSchemas === "function"
) {
window.__wrnValidate.registerSchemas(
defaults,
document
);
} else if (
window.__wrnValidate &&
typeof window.__wrnValidate.init ===
"function"
) {
window.__wrnValidate.init(document);
}
})();
${runtime}
`;
}
function audit(code: string, file: string): AuthAuditIssue[] {
const issues: AuthAuditIssue[] = [];
const push = (id: string, severity: AuthAuditIssue["severity"], title: string, message: string) =>
issues.push({ id: `${id}:${file}`, severity, title, message, file });
if (/<SignIn|<SignUp|data-component=["']SignIn|data-component=["']SignUp/.test(code)) {
if (!/<Captcha|captchaGuard|captchaPageGate/.test(code)) {
push(
"captcha-escalation",
"suggestion",
"Add adaptive CAPTCHA",
"Authentication forms should connect suspicious attempts to @wrnexus/captcha.",
);
}
if (!/autocomplete=/.test(code)) {
push(
"autocomplete",
"warning",
"Credential autocomplete is missing",
"Use username, current-password, and new-password autocomplete values.",
);
}
}
if (/secret\s*=|clientSecret\s*=|privateKey\s*=/.test(code) && /\.wrn$/.test(file)) {
push(
"client-secret",
"error",
"Authentication secret exposed",
"Never pass server secrets to a .wrn component.",
);
}
if (
/returnTo|redirect/.test(code) &&
!/validateOAuthReturnTo|safeReturnTo|startsWith\(["']\//.test(code)
) {
push(
"open-redirect",
"suggestion",
"Confirm redirects are same-origin",
"Validate returnTo values before redirecting after sign-in.",
);
}
return issues;
}
export function authPlugin(options: AuthPluginOptions = {}) {
const metadataKey = "@wrnexus/auth:audit";
return definePlugin({
name: "@wrnexus/auth",
version: "0.8.0",
enforce: "post",
componentDirs(context) {
const value = resolved(context, options);
return value.enabled && value.components ? [value.componentDir] : [];
},
clientRuntimes(context) {
const value = resolved(context, options);
if (!value.enabled || !value.client) return [];
return [
{
id: "auth",
source: authClientSource(value.schemas),
type: "script" as const,
load: "defer" as const,
singleton: true,
bundle: false,
},
];
},
styleSources(context) {
const value = resolved(context, options);
return value.enabled && value.components
? [{ id: "auth-components", source: value.componentDir, order: "normal" as const }]
: [];
},
routeEntries(context) {
const value = resolved(context, options);
if (!value.enabled) {
return [];
}
return AUTH_ROUTE_DEFINITIONS.filter((route) => routeEnabled(value.routes, route.group)).map(
({ path }) => ({
kind: "api" as const,
path,
entry: authApiRouteEntry(path),
}),
);
},
middleware(context) {
const value = resolved(context, options);
return value.enabled && value.middleware ? [middlewareFile] : [];
},
migrations(context) {
const value = resolved(context, options);
return value.enabled && value.migrations
? [
{
id: "wrnexus-auth-001",
source: readFileSync(migrationsFile, "utf8"),
},
{
id: "wrnexus-auth-002-otp-purpose",
source: readFileSync(otpPurposeMigrationFile, "utf8"),
},
]
: [];
},
configure(config, context) {
const value = resolveConfig(config, options);
context.metadata.set(resolvedConfigKey, value);
config.auth = {
...((config.auth ?? {}) as Record<string, unknown>),
componentDir: value.componentDir,
schemas: value.schemas,
};
if (value.engine) {
setDefaultAuthEngine(value.engine);
} else {
clearDefaultAuthEngine();
}
setDefaultAuthSchemas(value.schemas);
setDefaultAuthRouteOptions({
baseUrl: value.baseUrl,
csrf: value.csrf,
passkey: value.passkey,
onSignedIn: value.onSignedIn,
onSignedOut: value.onSignedOut,
sessionCookieName: value.sessionCookieName,
onSessionVerification: value.onSessionVerification,
});
context.metadata.set("@wrnexus/auth:component-dir", value.componentDir);
context.metadata.set("@wrnexus/auth:configured", Boolean(value.engine));
},
transformCode(code, context) {
if (context.mode !== "development") return;
const previous = (context.metadata.get(metadataKey) as AuthAuditIssue[] | undefined) ?? [];
context.metadata.set(metadataKey, [
...previous.filter((issue) => issue.file !== context.file),
...audit(code, context.file),
]);
},
devToolbarPanels(context) {
const value = resolved(context, options);
if (!value.enabled || !value.devToolbar) return [];
const issues = (context.metadata.get(metadataKey) as AuthAuditIssue[] | undefined) ?? [];
return [
{
id: "wrnexus-auth",
title: "Authentication",
icon: "shield-user",
badge: issues.length,
description: "Authentication security, session, passkey, and recovery checks",
issues,
},
];
},
});
}
export default authPlugin;