import { z } from 'zod'; /** * Helper to treat empty strings as undefined. */ const preprocessEmptyString = (val: unknown) => (val === '' ? undefined : val); /** * Environment variable schema. */ export const envSchema = z .object({ NODE_ENV: z.enum(['development', 'production', 'test']).default('development'), NEXT_PUBLIC_BASE_URL: z.preprocess(preprocessEmptyString, z.string().url()), NEXT_PUBLIC_TARGET: z.enum(['development', 'testing', 'staging', 'production']).optional(), // Analytics UMAMI_WEBSITE_ID: z.preprocess(preprocessEmptyString, z.string().optional()), UMAMI_API_ENDPOINT: z.preprocess( preprocessEmptyString, z.string().url().default('https://analytics.infra.mintel.me'), ), // Error Tracking SENTRY_DSN: z.preprocess(preprocessEmptyString, z.string().optional()), // Logging LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info'), // Mail MAIL_HOST: z.preprocess(preprocessEmptyString, z.string().optional()), MAIL_PORT: z.preprocess(preprocessEmptyString, z.coerce.number().default(587)), MAIL_USERNAME: z.preprocess(preprocessEmptyString, z.string().optional()), MAIL_PASSWORD: z.preprocess(preprocessEmptyString, z.string().optional()), MAIL_FROM: z.preprocess(preprocessEmptyString, z.string().optional()), MAIL_RECIPIENTS: z.preprocess( (val) => (typeof val === 'string' ? val.split(',').filter(Boolean) : val), z.array(z.string()).default([]), ), // Directus DIRECTUS_URL: z.preprocess( preprocessEmptyString, z.string().url().default('http://localhost:8055'), ), DIRECTUS_ADMIN_EMAIL: z.preprocess(preprocessEmptyString, z.string().optional()), DIRECTUS_ADMIN_PASSWORD: z.preprocess(preprocessEmptyString, z.string().optional()), DIRECTUS_API_TOKEN: z.preprocess(preprocessEmptyString, z.string().optional()), INTERNAL_DIRECTUS_URL: z.preprocess(preprocessEmptyString, z.string().url().optional()), // Deploy Target TARGET: z.enum(['development', 'testing', 'staging', 'production']).optional(), // Gotify GOTIFY_URL: z.preprocess(preprocessEmptyString, z.string().url().optional()), GOTIFY_TOKEN: z.preprocess(preprocessEmptyString, z.string().optional()), // Gatekeeper GATEKEEPER_URL: z.preprocess( preprocessEmptyString, z.string().url().default('http://gatekeeper:3000'), ), NEXT_PUBLIC_FEEDBACK_ENABLED: z.preprocess( (val) => val === 'true' || val === true, z.boolean().default(false) ), GATEKEEPER_BYPASS_ENABLED: z.preprocess( (val) => val === 'true' || val === true, z.boolean().default(false) ), INFRA_DIRECTUS_URL: z.preprocess(preprocessEmptyString, z.string().url().optional()), INFRA_DIRECTUS_TOKEN: z.preprocess(preprocessEmptyString, z.string().optional()), }) .superRefine((data, ctx) => { const target = data.NEXT_PUBLIC_TARGET || data.TARGET; const isDev = target === 'development' || !target; const isBuildTimeValidation = process.env.SKIP_RUNTIME_ENV_VALIDATION === 'true'; const isServer = typeof window === 'undefined'; // Only enforce server-only variables when running on the server. // In the browser, non-NEXT_PUBLIC_ variables are undefined and should not trigger validation errors. if (isServer && !isDev && !isBuildTimeValidation && !data.MAIL_HOST) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'MAIL_HOST is required in non-development environments', path: ['MAIL_HOST'], }); } }); export type Env = z.infer; /** * Collects all environment variables from the process. * Explicitly references NEXT_PUBLIC_ variables for Next.js inlining. */ export function getRawEnv() { return { NODE_ENV: process.env.NODE_ENV, NEXT_PUBLIC_BASE_URL: process.env.NEXT_PUBLIC_BASE_URL, NEXT_PUBLIC_TARGET: process.env.NEXT_PUBLIC_TARGET, UMAMI_WEBSITE_ID: process.env.UMAMI_WEBSITE_ID, UMAMI_API_ENDPOINT: process.env.UMAMI_API_ENDPOINT, SENTRY_DSN: process.env.SENTRY_DSN, LOG_LEVEL: process.env.LOG_LEVEL, MAIL_HOST: process.env.MAIL_HOST, MAIL_PORT: process.env.MAIL_PORT, MAIL_USERNAME: process.env.MAIL_USERNAME, MAIL_PASSWORD: process.env.MAIL_PASSWORD, MAIL_FROM: process.env.MAIL_FROM, MAIL_RECIPIENTS: process.env.MAIL_RECIPIENTS, DIRECTUS_URL: process.env.DIRECTUS_URL, DIRECTUS_ADMIN_EMAIL: process.env.DIRECTUS_ADMIN_EMAIL, DIRECTUS_ADMIN_PASSWORD: process.env.DIRECTUS_ADMIN_PASSWORD, DIRECTUS_API_TOKEN: process.env.DIRECTUS_API_TOKEN, INTERNAL_DIRECTUS_URL: process.env.INTERNAL_DIRECTUS_URL, TARGET: process.env.TARGET, GOTIFY_URL: process.env.GOTIFY_URL, GOTIFY_TOKEN: process.env.GOTIFY_TOKEN, GATEKEEPER_URL: process.env.GATEKEEPER_URL, NEXT_PUBLIC_FEEDBACK_ENABLED: process.env.NEXT_PUBLIC_FEEDBACK_ENABLED, GATEKEEPER_BYPASS_ENABLED: process.env.GATEKEEPER_BYPASS_ENABLED, INFRA_DIRECTUS_URL: process.env.INFRA_DIRECTUS_URL, INFRA_DIRECTUS_TOKEN: process.env.INFRA_DIRECTUS_TOKEN, }; }