import path from 'node:path'; import { fileURLToPath } from 'node:url'; import dotenv from 'dotenv'; import { z } from 'zod'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); // Load .env from the monorepo root regardless of cwd — but NEVER under test. The test suites // inject their own env explicitly; loading a developer's/CI's real .env here makes tests // non-hermetic (e.g. a real TURNSTILE_SECRET_KEY would switch on CAPTCHA and break auth tests). if (process.env.NODE_ENV !== 'test') { dotenv.config({ path: path.resolve(__dirname, '../../../.env') }); } const envSchema = z.object({ NODE_ENV: z.enum(['development', 'production', 'test']).default('development'), PORT: z.coerce.number().int().positive().default(8080), PUBLIC_URL: z.string().url().default('http://localhost:8080'), COOKIE_DOMAIN: z.string().optional(), SESSION_SECRET: z.string().min(32), CSRF_SECRET: z.string().min(32), DATABASE_URL: z.string().min(1), DATABASE_CA_CERT_PATH: z.string().optional(), WEB_DIST_PATH: z.string().optional(), SUPERADMIN_EMAILS: z.string().optional().default(''), SENTRY_DSN_API: z.string().optional().default(''), // Object storage — DigitalOcean Spaces (S3-compatible), the platform's sole storage backend. // Required: the API must not boot into a state where uploads silently have nowhere to go. SPACES_ENDPOINT: z.string().url(), SPACES_REGION: z.string().min(1), SPACES_BUCKET: z.string().min(1), SPACES_KEY: z.string().min(1), SPACES_SECRET: z.string().min(1), SPACES_PUBLIC_BASE: z.string().optional().default(''), // Legacy local path — read only by the one-time migration script, not the running app. STORAGE_PATH: z.string().optional().default('./storage'), SMTP2GO_API_KEY: z.string().optional().default(''), ANTHROPIC_API_KEY: z.string().optional().default(''), TURNSTILE_SECRET_KEY: z.string().optional().default(''), EMAIL_FROM: z.string().optional().default('eLegal Software '), STRIPE_SECRET_KEY: z.string().optional().default(''), STRIPE_WEBHOOK_SECRET: z.string().optional().default(''), STRIPE_PRICE_PRO: z.string().optional().default(''), STRIPE_PRICE_LIFETIME: z.string().optional().default(''), }); const parsed = envSchema.parse(process.env); // Production guard: Turnstile bot protection fails open when TURNSTILE_SECRET_KEY is unset // (correct for dev/test, but in production a forgotten key silently disables all CAPTCHA/bot // protection on signup/login/password-reset/contact). Fail fast at boot rather than run exposed. if (parsed.NODE_ENV === 'production' && !parsed.TURNSTILE_SECRET_KEY) { throw new Error( 'TURNSTILE_SECRET_KEY is required in production: without it, bot protection fails open and CAPTCHA verification is skipped entirely. Set TURNSTILE_SECRET_KEY in the environment.', ); } export const env = { ...parsed, superadminEmails: parsed.SUPERADMIN_EMAILS.split(',') .map((s) => s.trim().toLowerCase()) .filter(Boolean), }; export const isProd = env.NODE_ENV === 'production';