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, 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 (/ 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), 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;