feat: centralize application framework primitives
This commit is contained in:
@@ -2,6 +2,13 @@ import { readFileSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { definePlugin, type PluginContext } from "@wrnexus/plugin";
|
||||
import {
|
||||
discord,
|
||||
github,
|
||||
google,
|
||||
type OAuthProvider,
|
||||
type ProviderCredentials,
|
||||
} from "@wrnexus/oauth";
|
||||
import type { AuthEngine } from "./engine.ts";
|
||||
import type { AuthPasskeyHttpOptions } from "./http/index.ts";
|
||||
import type { AuthSessionVerificationHandler } from "./types.ts";
|
||||
@@ -32,8 +39,11 @@ export interface AuthRoutesConfig {
|
||||
sessions?: boolean;
|
||||
impersonation?: boolean;
|
||||
passkeys?: boolean;
|
||||
oauth?: boolean;
|
||||
}
|
||||
|
||||
export type AuthOAuthProviderConfig = OAuthProvider | ProviderCredentials;
|
||||
|
||||
export interface AuthConfig {
|
||||
enabled?: boolean;
|
||||
engine?: AuthEngine;
|
||||
@@ -48,12 +58,36 @@ export interface AuthConfig {
|
||||
baseUrl?: string;
|
||||
csrf?: boolean;
|
||||
passkey?: AuthPasskeyHttpOptions;
|
||||
oauth?: Partial<Record<"google" | "github" | "discord", AuthOAuthProviderConfig>> &
|
||||
Record<string, AuthOAuthProviderConfig>;
|
||||
oauthMfaPath?: string;
|
||||
/** Disable only when an external deployment gate performs equivalent checks. */
|
||||
productionValidation?: boolean;
|
||||
/** Shared SSO cookie whose value is an AuthEngine session id. */
|
||||
sessionCookieName?: string;
|
||||
/** Customize the package forward-auth verification response. */
|
||||
onSessionVerification?: AuthSessionVerificationHandler;
|
||||
}
|
||||
|
||||
export function validateProductionAuthConfig(
|
||||
config: Pick<AuthConfig, "baseUrl" | "oauth">,
|
||||
env: Record<string, string | undefined> = process.env,
|
||||
): void {
|
||||
const secret = env.AUTH_SECRET?.trim() ?? "";
|
||||
if (secret.length < 32 || /^(?:change-me|development|test-only)/i.test(secret)) {
|
||||
throw new Error(
|
||||
"WRN-AUTH-CONFIG: AUTH_SECRET must be a non-development secret of at least 32 characters in production",
|
||||
);
|
||||
}
|
||||
if (!config.baseUrl) throw new Error("WRN-AUTH-CONFIG: auth.baseUrl is required in production");
|
||||
const base = new URL(config.baseUrl);
|
||||
if (base.protocol !== "https:")
|
||||
throw new Error("WRN-AUTH-CONFIG: auth.baseUrl must use HTTPS in production");
|
||||
if (["localhost", "127.0.0.1", "::1"].includes(base.hostname)) {
|
||||
throw new Error("WRN-AUTH-CONFIG: auth.baseUrl must not use localhost in production");
|
||||
}
|
||||
}
|
||||
|
||||
/** Explicit plugin options remain supported for compatibility. Prefer config.auth. */
|
||||
export interface AuthPluginOptions {
|
||||
componentDir?: string;
|
||||
@@ -88,6 +122,8 @@ interface ResolvedAuthConfig {
|
||||
passkey?: AuthPasskeyHttpOptions;
|
||||
sessionCookieName?: string;
|
||||
onSessionVerification?: AuthSessionVerificationHandler;
|
||||
oauth: Record<string, OAuthProvider>;
|
||||
oauthMfaPath?: string;
|
||||
}
|
||||
|
||||
const moduleRoot = dirname(fileURLToPath(import.meta.url));
|
||||
@@ -121,6 +157,24 @@ function resolveConfig(
|
||||
const enabled = raw.enabled !== false;
|
||||
const hasEngine = Boolean(raw.engine);
|
||||
const hasDefaultDb = Boolean(config.db);
|
||||
const oauth: Record<string, OAuthProvider> = {};
|
||||
for (const [name, provider] of Object.entries(raw.oauth ?? {})) {
|
||||
if (!provider || !provider.clientId?.trim() || !provider.clientSecret?.trim()) continue;
|
||||
oauth[name] =
|
||||
"authorizeUrl" in provider
|
||||
? provider
|
||||
: name === "google"
|
||||
? google(provider)
|
||||
: name === "github"
|
||||
? github(provider)
|
||||
: name === "discord"
|
||||
? discord(provider)
|
||||
: (() => {
|
||||
throw new Error(
|
||||
`WRN-AUTH-OAUTH-CONFIG: custom provider '${name}' requires a complete OAuthProvider`,
|
||||
);
|
||||
})();
|
||||
}
|
||||
|
||||
return {
|
||||
enabled,
|
||||
@@ -148,6 +202,8 @@ function resolveConfig(
|
||||
passkey: raw.passkey,
|
||||
sessionCookieName: raw.sessionCookieName,
|
||||
onSessionVerification: raw.onSessionVerification,
|
||||
oauth,
|
||||
oauthMfaPath: raw.oauthMfaPath,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -176,6 +232,7 @@ function fallbackConfig(options: AuthPluginOptions): ResolvedAuthConfig {
|
||||
schemas: resolveAuthSchemas(),
|
||||
|
||||
csrf: true,
|
||||
oauth: {},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -353,6 +410,16 @@ export function authPlugin(options: AuthPluginOptions = {}) {
|
||||
},
|
||||
|
||||
configure(config, context) {
|
||||
const raw = (config.auth ?? {}) as AuthConfig;
|
||||
if (
|
||||
context.mode === "production" &&
|
||||
process.env.NODE_ENV === "production" &&
|
||||
raw.enabled !== false &&
|
||||
raw.engine &&
|
||||
raw.productionValidation !== false
|
||||
) {
|
||||
validateProductionAuthConfig(raw);
|
||||
}
|
||||
const value = resolveConfig(config, options);
|
||||
|
||||
context.metadata.set(resolvedConfigKey, value);
|
||||
@@ -383,6 +450,8 @@ export function authPlugin(options: AuthPluginOptions = {}) {
|
||||
sessionCookieName: value.sessionCookieName,
|
||||
|
||||
onSessionVerification: value.onSessionVerification,
|
||||
oauth: value.oauth,
|
||||
oauthMfaPath: value.oauthMfaPath,
|
||||
});
|
||||
|
||||
context.metadata.set("@wrnexus/auth:component-dir", value.componentDir);
|
||||
|
||||
Reference in New Issue
Block a user