export const SESSION_COOKIE_NAME = "zhinian_session"; export const AUTH_STATE_COOKIE_NAME = "zhinian_auth_state"; export type AuthRuntimeConfig = { required: boolean; configured: boolean; missing: string[]; authBaseUrl?: string; authorizeUrl?: string; tokenUrl?: string; jwksUrl?: string; logoutUrl?: string; clientId: string; clientSecret?: string; scope: string; issuer: string; passwordEncryptionKey?: string; sessionSecret?: string; clockSkewSeconds: number; }; export function getAuthRuntimeConfig(): AuthRuntimeConfig { const authBaseUrl = trimTrailingSlash(envValue("ZHINIAN_AUTH_BASE_URL", "AUTH_BASE")); const clientId = envValue("ZHINIAN_AUTH_CLIENT_ID", "AUTH_CLIENT_ID") || "customPC"; const clientSecret = envValue("ZHINIAN_AUTH_CLIENT_SECRET", "AUTH_CLIENT_SECRET"); const scope = envValue("ZHINIAN_AUTH_SCOPE", "AUTH_SCOPE") || "server"; const issuer = envValue("ZHINIAN_AUTH_ISSUER", "AUTH_ISSUER") || "https://pig4cloud.com"; const passwordEncryptionKey = envValue("ZHINIAN_AUTH_PASSWORD_ENC_KEY", "AUTH_PASSWORD_ENC_KEY", "AGENTBUS_SSO_PASSWORD_ENC_KEY"); const sessionSecret = envValue("ZHINIAN_AUTH_SESSION_SECRET", "AUTH_SESSION_SECRET", "NEXTAUTH_SECRET"); const explicitRequired = boolEnv("ZHINIAN_AUTH_REQUIRED"); const disabled = boolEnv("ZHINIAN_AUTH_DISABLED") === true; const hasAnyAuthConfig = Boolean(authBaseUrl || clientSecret || sessionSecret); const required = disabled ? false : explicitRequired ?? (process.env.NODE_ENV === "production" || Boolean(authBaseUrl)); const wantsConfiguration = required || hasAnyAuthConfig; const missing: string[] = []; if (wantsConfiguration && !authBaseUrl) missing.push("ZHINIAN_AUTH_BASE_URL"); if (wantsConfiguration && !clientSecret) missing.push("ZHINIAN_AUTH_CLIENT_SECRET"); if (wantsConfiguration && !sessionSecret) missing.push("ZHINIAN_AUTH_SESSION_SECRET"); return { required, configured: wantsConfiguration && missing.length === 0, missing, authBaseUrl, authorizeUrl: endpointUrl(authBaseUrl, "ZHINIAN_AUTH_AUTHORIZE_URL", "/oauth2/authorize"), tokenUrl: endpointUrl(authBaseUrl, "ZHINIAN_AUTH_TOKEN_URL", "/oauth2/token"), jwksUrl: endpointUrl(authBaseUrl, "ZHINIAN_AUTH_JWKS_URL", "/oauth2/jwks"), logoutUrl: endpointUrl(authBaseUrl, "ZHINIAN_AUTH_LOGOUT_URL", "/token/logout"), clientId, clientSecret, scope, issuer, passwordEncryptionKey, sessionSecret, clockSkewSeconds: numberEnv("ZHINIAN_AUTH_CLOCK_SKEW_SECONDS") ?? 60 }; } export function safeNextPath(value: string | null | undefined, fallback = "/create"): string { if (!value || !value.startsWith("/") || value.startsWith("//")) return fallback; try { const parsed = new URL(value, "http://zhinian.local"); if (parsed.origin !== "http://zhinian.local") return fallback; const path = `${parsed.pathname}${parsed.search}${parsed.hash}`; if (path.startsWith("/api/auth") || path.startsWith("/auth/login")) return fallback; return path; } catch { return fallback; } } export function authConfigSummary(config = getAuthRuntimeConfig()) { if (!config.required) return "disabled"; return config.configured ? "configured" : "missing"; } export function shouldUseSecureAuthCookie(requestUrl?: string): boolean { const explicit = boolEnv("ZHINIAN_AUTH_COOKIE_SECURE"); if (explicit !== undefined) return explicit; const configured = envValue("NEXT_PUBLIC_APP_URL", "ZHINIAN_PUBLIC_BASE_URL"); const candidate = configured || requestUrl; if (!candidate) return false; try { return new URL(candidate).protocol === "https:"; } catch { return false; } } function endpointUrl(authBaseUrl: string | undefined, overrideKey: string, path: string) { const override = envValue(overrideKey); if (override) return override.replace(/\/$/, ""); return authBaseUrl ? `${authBaseUrl}${path}` : undefined; } function envValue(...names: string[]): string | undefined { for (const name of names) { const value = process.env[name]?.trim(); if (value) return value; } return undefined; } function boolEnv(name: string): boolean | undefined { const value = process.env[name]?.trim().toLowerCase(); if (!value || value === "auto") return undefined; if (["1", "true", "yes", "on"].includes(value)) return true; if (["0", "false", "no", "off"].includes(value)) return false; return undefined; } function numberEnv(name: string): number | undefined { const value = process.env[name]?.trim(); if (!value) return undefined; const parsed = Number(value); return Number.isFinite(parsed) ? parsed : undefined; } function trimTrailingSlash(value?: string) { return value?.replace(/\/$/, ""); }