import { z } from "zod" const urlField = () => z.preprocess( (value) => (typeof value === "string" ? value.trim() || undefined : value), z.string().url().optional() ) const stringField = () => z.preprocess( (value) => (typeof value === "string" ? value.trim() || undefined : value), z.string().min(1).optional() ) const envSchema = z.object({ BETTER_AUTH_SECRET: stringField().or(z.literal("")).optional(), BETTER_AUTH_URL: urlField().or(z.literal("")).optional(), NEXT_PUBLIC_CONVEX_URL: urlField().or(z.literal("")).optional(), CONVEX_INTERNAL_URL: urlField().or(z.literal("")).optional(), DATABASE_URL: stringField().or(z.literal("")).optional(), NEXT_PUBLIC_APP_URL: urlField().or(z.literal("")).optional(), MACHINE_PROVISIONING_SECRET: z.string().optional(), MACHINE_TOKEN_TTL_MS: z.coerce.number().optional(), FLEET_SYNC_SECRET: z.string().optional(), SMTP_ADDRESS: z.string().optional(), SMTP_PORT: z.coerce.number().optional(), SMTP_DOMAIN: z.string().optional(), SMTP_USERNAME: z.string().optional(), SMTP_PASSWORD: z.string().optional(), SMTP_AUTHENTICATION: z.string().optional(), SMTP_ENABLE_STARTTLS_AUTO: z.string().optional(), SMTP_TLS: z.string().optional(), MAILER_SENDER_EMAIL: z.string().optional(), REPORTS_CRON_SECRET: z.string().optional(), INTERNAL_HEALTH_TOKEN: z.string().optional(), REPORTS_CRON_BASE_URL: urlField().or(z.literal("")).optional(), ARCHIVE_DIR: stringField().or(z.literal("")).optional(), }) const parsed = envSchema.safeParse(process.env) if (!parsed.success) { console.error("Failed to parse environment variables", parsed.error.flatten().fieldErrors) throw new Error("Invalid environment configuration") } const isBuildPhase = process.env.NEXT_PHASE === "phase-production-build" function resolveSecret(value: string | undefined, key: string) { if (value && value.length > 0) return value const fallback = isBuildPhase ? `build-placeholder-${key.toLowerCase()}` : `dev-${key.toLowerCase()}` if (process.env.NODE_ENV === "production" && !isBuildPhase) { throw new Error(`${key} must be set in production runtime environment`) } if (!isBuildPhase) { console.warn(`ENV ${key} not set; using fallback value only for development.`) } return fallback } export const env = { BETTER_AUTH_SECRET: resolveSecret(parsed.data.BETTER_AUTH_SECRET, "BETTER_AUTH_SECRET"), BETTER_AUTH_URL: parsed.data.BETTER_AUTH_URL ?? parsed.data.NEXT_PUBLIC_APP_URL ?? "http://localhost:3000", NEXT_PUBLIC_CONVEX_URL: parsed.data.NEXT_PUBLIC_CONVEX_URL, CONVEX_INTERNAL_URL: parsed.data.CONVEX_INTERNAL_URL, DATABASE_URL: parsed.data.DATABASE_URL, NEXT_PUBLIC_APP_URL: parsed.data.NEXT_PUBLIC_APP_URL, MACHINE_PROVISIONING_SECRET: parsed.data.MACHINE_PROVISIONING_SECRET, MACHINE_TOKEN_TTL_MS: parsed.data.MACHINE_TOKEN_TTL_MS, FLEET_SYNC_SECRET: parsed.data.FLEET_SYNC_SECRET, REPORTS_CRON_SECRET: parsed.data.REPORTS_CRON_SECRET, INTERNAL_HEALTH_TOKEN: parsed.data.INTERNAL_HEALTH_TOKEN, REPORTS_CRON_BASE_URL: parsed.data.REPORTS_CRON_BASE_URL, ARCHIVE_DIR: parsed.data.ARCHIVE_DIR ?? "./archives", SMTP: parsed.data.SMTP_ADDRESS && parsed.data.SMTP_USERNAME && parsed.data.SMTP_PASSWORD ? { host: parsed.data.SMTP_ADDRESS, port: parsed.data.SMTP_PORT ?? 465, domain: parsed.data.SMTP_DOMAIN, username: parsed.data.SMTP_USERNAME, password: parsed.data.SMTP_PASSWORD, tls: (parsed.data.SMTP_TLS ?? "true").toLowerCase() === "true", starttls: (parsed.data.SMTP_ENABLE_STARTTLS_AUTO ?? "false").toLowerCase() === "true", auth: parsed.data.SMTP_AUTHENTICATION ?? "login", from: parsed.data.MAILER_SENDER_EMAIL ?? "no-reply@example.com", } : null, }