import { existsSync } from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import dotenv from 'dotenv'; import { z } from 'zod'; const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../..'); const envFiles = [ path.join(projectRoot, '.env.local'), path.join(projectRoot, '.env'), ]; for (const envFile of envFiles) { if (existsSync(envFile)) { dotenv.config({ path: envFile, override: false }); } } const envSchema = z.object({ NODE_ENV: z.enum(['development', 'test', 'production']).default('development'), APP_HOST: z.string().default('0.0.0.0'), APP_PORT: z.coerce.number().int().positive().default(4000), APP_ORIGIN: z.string().default('http://localhost:3000'), DATABASE_URL: z.string().min(1, 'DATABASE_URL is required'), COOKIE_SECRET: z.string().min(1, 'COOKIE_SECRET is required'), GOOGLE_MAPS_SERVER_KEY: z.string().optional(), PG_BOSS_SCHEMA: z.string().default('pgboss'), SESSION_TTL_DAYS: z.coerce.number().int().positive().default(30), STRIPE_SECRET_KEY: z.string().optional(), STRIPE_PUBLISHABLE_KEY: z.string().optional(), STRIPE_WEBHOOK_SECRET: z.string().optional(), STRIPE_PRICE_STARTER_MONTHLY: z.string().optional(), STRIPE_PRICE_STARTER_ANNUAL: z.string().optional(), STRIPE_PRICE_GROWTH_MONTHLY: z.string().optional(), STRIPE_PRICE_GROWTH_ANNUAL: z.string().optional(), STRIPE_PRICE_PRO_MONTHLY: z.string().optional(), STRIPE_PRICE_PRO_ANNUAL: z.string().optional(), STRIPE_PRICE_EXPORT_PACK_10K: z.string().optional(), STRIPE_PRICE_EXPORT_PACK_50K: z.string().optional(), STRIPE_BILLING_PORTAL_CONFIGURATION_ID: z.string().optional(), BILLING_ADMIN_EMAILS: z.string().optional(), }); export type AppEnv = z.infer; let cachedEnv: AppEnv | null = null; export function getEnv(): AppEnv { if (cachedEnv) { return cachedEnv; } cachedEnv = envSchema.parse(process.env); return cachedEnv; } export function isBillingAdminEmail(email: string) { const allowlist = getEnv().BILLING_ADMIN_EMAILS; if (!allowlist) { return false; } return allowlist .split(',') .map((entry) => entry.trim().toLowerCase()) .filter(Boolean) .includes(email.trim().toLowerCase()); }