const { z } = require("zod"); const booleanString = z.enum(["true", "false"]).default("false").transform((value) => value === "true"); const envSchema = z.object({ NODE_ENV: z.enum(["development", "test", "production"]).default("development"), PORT: z.coerce.number().int().min(1).max(65535).default(3070), DB_HOST: z.string().min(1), DB_PORT: z.coerce.number().int().min(1).max(65535).default(3306), DB_NAME: z.string().min(1), DB_USER: z.string().min(1), DB_PASSWORD: z.string(), JWT_SECRET: z.string().min(32, "JWT_SECRET must contain at least 32 characters"), JWT_ISSUER: z.string().min(1).default("zumri-api"), JWT_AUDIENCE: z.string().min(1).default("zumri-clients"), ACCESS_TOKEN_TTL: z.string().default("15m"), REFRESH_TOKEN_TTL_DAYS: z.coerce.number().int().positive().default(7), REMEMBER_ME_REFRESH_TOKEN_TTL_DAYS: z.coerce.number().int().positive().default(30), LOGIN_OTP_TTL_SECONDS: z.coerce.number().int().positive().default(900), LOGIN_OTP_MAX_ATTEMPTS: z.coerce.number().int().min(1).max(10).default(5), REDIS_HOST: z.string().min(1), REDIS_PORT: z.coerce.number().int().min(1).max(65535).default(6379), REDIS_PASSWORD: z.string().optional(), FRONTEND_URL: z.string().url(), TRUST_PROXY: z.coerce.number().int().min(0).max(10).default(0), JSON_BODY_LIMIT: z.string().default("1mb"), API_RATE_LIMIT_WINDOW_MS: z.coerce.number().int().positive().default(900000), API_RATE_LIMIT_MAX: z.coerce.number().int().positive().default(300), SENSITIVE_RATE_LIMIT_WINDOW_MS: z.coerce.number().int().positive().default(900000), SENSITIVE_RATE_LIMIT_MAX: z.coerce.number().int().positive().default(20), RUN_CRON: booleanString, ENABLE_MAIL: booleanString, ENABLE_S3: booleanString, SHUTDOWN_TIMEOUT_MS: z.coerce.number().int().positive().default(10000), CACHE: booleanString, MAIL_HOST: z.string().min(1).optional(), MAIL_PORT: z.coerce.number().int().positive().optional(), MAIL_SECURE: z.enum(["true", "false"]).optional(), MAIL_USER: z.string().optional(), MAIL_PASS: z.string().optional(), MAIL_FROM: z.string().optional(), AWS_REGION: z.string().optional(), AWS_ACCESS_KEY_ID: z.string().optional(), AWS_SECRET_ACCESS_KEY: z.string().optional(), AWS_S3_BUCKET_NAME: z.string().optional(), S3_ENDPOINT: z.string().url().optional(), S3_FORCE_PATH_STYLE: booleanString, S3_SIGNED_URL_TTL_SECONDS: z.coerce.number().int().min(60).max(86400).default(900), S3_MAX_UPLOAD_BYTES: z.coerce.number().int().positive().default(5242880), EMAIL_QUEUE_CONCURRENCY: z.coerce.number().int().positive().default(5), DOCUMENT_QUEUE_CONCURRENCY: z.coerce.number().int().positive().default(2), NOTIFICATION_RETENTION_DAYS: z.coerce.number().int().positive().default(90), INVENTORY_RESERVATION_TTL_MINUTES: z.coerce.number().int().positive().default(15), CHECKOUT_TTL_MINUTES: z.coerce.number().int().positive().default(15), CART_MAX_ITEM_QUANTITY: z.coerce.number().int().positive().default(100), RETURN_WINDOW_DAYS: z.coerce.number().int().positive().default(30), PAYHERE_MERCHANT_ID: z.string().optional(), PAYHERE_MERCHANT_SECRET: z.string().optional(), PAYHERE_NOTIFY_URL: z.string().url().optional(), PAYHERE_RETURN_URL: z.string().url().optional(), PAYHERE_CANCEL_URL: z.string().url().optional(), LOYALTY_RECONCILIATION_BATCH_SIZE: z.coerce.number().int().positive().max(1000).default(100), LOG_RETENTION_DAYS: z.coerce.number().int().positive().default(30), DOCS_USER: z.string().optional(), DOCS_PASS: z.string().optional(), GOOGLE_CLIENT_ID: z.string().optional(), APPLE_CLIENT_ID: z.string().optional(), }).superRefine((env, context) => { const requireFeature = (enabled, names) => { if (!enabled) return; for (const name of names) { if (!env[name]) context.addIssue({ code: "custom", path: [name], message: `${name} is required when enabled` }); } }; requireFeature(env.ENABLE_MAIL, ["MAIL_HOST", "MAIL_PORT", "MAIL_USER", "MAIL_PASS", "MAIL_FROM"]); requireFeature(env.ENABLE_S3, ["AWS_REGION", "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_S3_BUCKET_NAME"]); }); let validatedEnv; const validateEnvironment = (source = process.env) => { const result = envSchema.safeParse(source); if (!result.success) { const names = [...new Set(result.error.issues.map((issue) => issue.path.join(".") || "environment"))]; throw new Error(`Invalid environment configuration: ${names.join(", ")}`); } validatedEnv = result.data; return validatedEnv; }; const getEnvironment = () => validatedEnv || validateEnvironment(); const getOptionalFeatureStatus = (env = process.env) => ({ mail: Boolean(env.MAIL_HOST && env.MAIL_PORT && env.MAIL_USER && env.MAIL_PASS && env.MAIL_FROM), s3: Boolean(env.AWS_REGION && env.AWS_ACCESS_KEY_ID && env.AWS_SECRET_ACCESS_KEY && env.AWS_S3_BUCKET_NAME), docs: Boolean(env.DOCS_USER && env.DOCS_PASS), }); module.exports = { validateEnvironment, getEnvironment, getOptionalFeatureStatus };