import { createHmac } from "node:crypto"; export type ServerConfig = { host: string; port: number; accessTokenSecret: string; accessTokenTtlSeconds: number; refreshCookieName: string; corsOrigin?: string; adminEmail: string; adminPassword: string; adminMfaRequired: boolean; adminSelfApprovalAllowed?: boolean; adminMfaSecret?: string; adminCaptchaRequired?: boolean; userMfaRequired?: boolean; userVerificationRequired?: boolean; emailRegistrationVerificationRequired?: boolean; phoneRegistrationVerificationRequired?: boolean; captchaRequired?: boolean; cookieSecure?: boolean; csrfCookieName?: string; maxBodyBytes?: number; stagingDir?: string; storeFile?: string; rateLimitWindowMs?: number; rateLimitMax?: number; adminRoles?: string[]; databaseUrl?: string; redisUrl?: string; persistenceAdapter?: "memory" | "file" | "postgres"; queueAdapter?: "memory" | "redis"; stagingTtlSeconds?: number; channelEncryptionKey?: string; adminRole?: string; refreshAbsoluteTtlSeconds?: number; paymentWebhookSecret?: string; adminAccounts?: Array<{ id: string; email: string; password: string; role: string; mfaSecret?: string; mfaRequired: boolean; recoveryCodeHashes?: string[]; mfaLastTotpCounter?: number }>; }; function positiveInt(value: string | undefined, fallback: number) { const parsed = Number(value); return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback; } function localMfaSecret(secret: string) { const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; const bytes = createHmac("sha256", secret).update("miragenflow-admin-mfa").digest().subarray(0, 20); let output = ""; let buffer = 0; let bits = 0; for (const byte of bytes) { buffer = (buffer << 8) | byte; bits += 8; while (bits >= 5) { output += alphabet[(buffer >>> (bits - 5)) & 31]; bits -= 5; } } if (bits > 0) output += alphabet[(buffer << (5 - bits)) & 31]; return output; } function adminAccounts(env: NodeJS.ProcessEnv, fallback: { email: string; password: string; role: string; mfaSecret: string; mfaRequired: boolean }) { if (!env.MIRAGENFLOW_ADMIN_ACCOUNTS_JSON?.trim()) return [{ id: `admin:${fallback.email.toLowerCase()}`, ...fallback }]; const parsed = JSON.parse(env.MIRAGENFLOW_ADMIN_ACCOUNTS_JSON) as Array>; if (!Array.isArray(parsed) || !parsed.length) throw new Error("MIRAGENFLOW_ADMIN_ACCOUNTS_JSON must contain at least one administrator"); return parsed.map((item, index) => { const email = typeof item.email === "string" ? item.email.trim().toLowerCase() : ""; const password = typeof item.password === "string" ? item.password : ""; if (!email || password.length < 12) throw new Error(`administrator account ${index + 1} is invalid`); return { id: typeof item.id === "string" && item.id.trim() ? item.id.trim() : `admin:${email}`, email, password, role: typeof item.role === "string" ? item.role : "operator", mfaSecret: typeof item.mfaSecret === "string" ? item.mfaSecret.trim() : undefined, mfaRequired: item.mfaRequired !== false, recoveryCodeHashes: Array.isArray(item.recoveryCodeHashes) ? item.recoveryCodeHashes.filter((value): value is string => typeof value === "string") : undefined, mfaLastTotpCounter: Number.isInteger(item.mfaLastTotpCounter) ? Number(item.mfaLastTotpCounter) : undefined }; }); } export function loadConfig(env = process.env): ServerConfig { const isProduction = env.NODE_ENV === "production"; const secret = env.MIRAGENFLOW_ACCESS_TOKEN_SECRET?.trim(); const adminPassword = env.MIRAGENFLOW_ADMIN_PASSWORD; if (isProduction && (!secret || secret.length < 32)) throw new Error("MIRAGENFLOW_ACCESS_TOKEN_SECRET must be at least 32 characters in production"); if (isProduction && (!adminPassword || adminPassword.length < 12)) throw new Error("MIRAGENFLOW_ADMIN_PASSWORD must be set in production"); if (isProduction && env.MIRAGENFLOW_ADMIN_MFA_REQUIRED !== "false" && !env.MIRAGENFLOW_ADMIN_MFA_SECRET?.trim()) throw new Error("MIRAGENFLOW_ADMIN_MFA_SECRET must be set when administrator MFA is enabled"); if (isProduction && !env.MIRAGENFLOW_CHANNEL_ENCRYPTION_KEY?.trim()) throw new Error("MIRAGENFLOW_CHANNEL_ENCRYPTION_KEY must be set in production"); if (isProduction && env.MIRAGENFLOW_PERSISTENCE_ADAPTER !== "postgres") throw new Error("MIRAGENFLOW_PERSISTENCE_ADAPTER=postgres is required in production"); if (isProduction && !(env.DATABASE_URL || env.MIRAGENFLOW_DATABASE_URL)) throw new Error("DATABASE_URL is required in production"); if (isProduction && env.MIRAGENFLOW_QUEUE_ADAPTER !== "redis") throw new Error("MIRAGENFLOW_QUEUE_ADAPTER=redis is required in production"); if (isProduction && !(env.REDIS_URL || env.MIRAGENFLOW_REDIS_URL)) throw new Error("REDIS_URL is required in production"); const effectiveSecret = secret || "local-development-secret-change-me"; // Administrator login stays lightweight by default. MFA/CAPTCHA remain // available as explicit security switches for deployments that enable them. const defaultAdminMfaRequired = env.MIRAGENFLOW_ADMIN_MFA_REQUIRED === "true"; const defaultAdminMfaSecret = env.MIRAGENFLOW_ADMIN_MFA_SECRET?.trim() || localMfaSecret(effectiveSecret); const configuredAdminAccounts = adminAccounts(env, { email: env.MIRAGENFLOW_ADMIN_EMAIL?.trim().toLowerCase() || "admin@admin.com", password: adminPassword || "admin", role: env.MIRAGENFLOW_ADMIN_ROLE?.trim() || "super_admin", mfaSecret: defaultAdminMfaSecret, mfaRequired: defaultAdminMfaRequired }).map((account) => ({ ...account, mfaSecret: account.mfaSecret || localMfaSecret(`${effectiveSecret}:${account.email}`) })); return { host: env.MIRAGENFLOW_HOST?.trim() || "127.0.0.1", port: positiveInt(env.MIRAGENFLOW_PORT, 3100), accessTokenSecret: effectiveSecret, accessTokenTtlSeconds: positiveInt(env.MIRAGENFLOW_ACCESS_TOKEN_TTL_SECONDS, 900), refreshCookieName: env.MIRAGENFLOW_REFRESH_COOKIE?.trim() || "miragenflow_refresh", corsOrigin: env.MIRAGENFLOW_CORS_ORIGIN?.trim() || undefined, adminEmail: env.MIRAGENFLOW_ADMIN_EMAIL?.trim() || "admin@admin.com", adminPassword: adminPassword || "admin", adminMfaRequired: defaultAdminMfaRequired, adminSelfApprovalAllowed: !isProduction && env.MIRAGENFLOW_ADMIN_SELF_APPROVAL === "true", adminCaptchaRequired: env.MIRAGENFLOW_ADMIN_CAPTCHA_REQUIRED === "true", adminMfaSecret: defaultAdminMfaSecret, adminAccounts: configuredAdminAccounts, // Ordinary-user verification is an optional enhancement. The default // preview/production policy is email+password only; deployments may turn // CAPTCHA, registration verification, or user MFA on independently. userMfaRequired: env.MIRAGENFLOW_USER_MFA_REQUIRED === "true", userVerificationRequired: env.MIRAGENFLOW_USER_VERIFICATION_REQUIRED === "true", emailRegistrationVerificationRequired: env.MIRAGENFLOW_EMAIL_REGISTRATION_VERIFICATION_REQUIRED === undefined ? undefined : env.MIRAGENFLOW_EMAIL_REGISTRATION_VERIFICATION_REQUIRED === "true", phoneRegistrationVerificationRequired: env.MIRAGENFLOW_PHONE_REGISTRATION_VERIFICATION_REQUIRED === undefined ? undefined : env.MIRAGENFLOW_PHONE_REGISTRATION_VERIFICATION_REQUIRED === "true", captchaRequired: env.MIRAGENFLOW_CAPTCHA_REQUIRED === "true", // Secure cookies require an HTTPS public origin. Production keeps the // secure default, while explicit false is useful for the HTTP local // compose profile (and must not be swallowed by an `|| isProduction` // expression). cookieSecure: env.MIRAGENFLOW_COOKIE_SECURE === undefined ? isProduction : env.MIRAGENFLOW_COOKIE_SECURE === "true", csrfCookieName: env.MIRAGENFLOW_CSRF_COOKIE?.trim() || "miragenflow_csrf", maxBodyBytes: positiveInt(env.MIRAGENFLOW_MAX_BODY_BYTES, 10 * 1024 * 1024), stagingDir: env.MIRAGENFLOW_STAGING_DIR?.trim() || "/tmp/miragenflow-staging", storeFile: env.MIRAGENFLOW_STORE_FILE?.trim() || undefined, rateLimitWindowMs: positiveInt(env.MIRAGENFLOW_RATE_LIMIT_WINDOW_MS, 60_000), rateLimitMax: positiveInt(env.MIRAGENFLOW_RATE_LIMIT_MAX, 12), adminRoles: (env.MIRAGENFLOW_ADMIN_ROLES || "super_admin,operator,finance,support,auditor").split(",").map((role) => role.trim()).filter(Boolean), databaseUrl: env.DATABASE_URL?.trim() || env.MIRAGENFLOW_DATABASE_URL?.trim() || undefined, redisUrl: env.REDIS_URL?.trim() || env.MIRAGENFLOW_REDIS_URL?.trim() || undefined, persistenceAdapter: env.MIRAGENFLOW_PERSISTENCE_ADAPTER === "postgres" && (env.DATABASE_URL || env.MIRAGENFLOW_DATABASE_URL) ? "postgres" : env.MIRAGENFLOW_STORE_FILE?.trim() ? "file" : "memory", queueAdapter: env.MIRAGENFLOW_QUEUE_ADAPTER === "redis" && (env.REDIS_URL || env.MIRAGENFLOW_REDIS_URL) ? "redis" : "memory", stagingTtlSeconds: positiveInt(env.MIRAGENFLOW_STAGING_TTL_SECONDS, 7 * 24 * 60 * 60), channelEncryptionKey: env.MIRAGENFLOW_CHANNEL_ENCRYPTION_KEY?.trim() || effectiveSecret, adminRole: env.MIRAGENFLOW_ADMIN_ROLE?.trim() || "super_admin", refreshAbsoluteTtlSeconds: positiveInt(env.MIRAGENFLOW_REFRESH_ABSOLUTE_TTL_SECONDS, 90 * 24 * 60 * 60), paymentWebhookSecret: env.MIRAGENFLOW_PAYMENT_WEBHOOK_SECRET?.trim() || effectiveSecret, }; }