import { existsSync } from 'node:fs'; import { resolve } from 'node:path'; import { z } from 'zod'; const emptyToUndefined = (v: unknown) => (v === '' ? undefined : v); export const envSchema = z .object({ NODE_ENV: z.enum(['development', 'production', 'test']).default('development'), PORT: z.coerce.number().int().positive().default(3000), // Orígenes permitidos por CORS, separados por coma. Vacío = mismo origen. CORS_ORIGINS: z .string() .default('') .transform((v) => v .split(',') .map((s) => s.trim()) .filter(Boolean), ), LOG_LEVEL: z.enum(['error', 'warn', 'log', 'debug', 'verbose']).default('log'), // Base SQLite local (permisos de menú). `:memory:` para pruebas. SQLITE_PATH: z.string().min(1).default('./data/app.sqlite'), // Usuarios web (U_WEB_USER) que siempre tienen acceso total ("*"); se // garantiza al arrancar para no quedar fuera por un error de permisos. ADMIN_WEB_USERS: z .string() .default('') .transform((v) => v .split(',') .map((s) => s.trim()) .filter(Boolean), ), // Inactividad tras la cual caduca la sesión web (deslizante). SESSION_TIMEOUT_MINUTES: z.coerce.number().int().positive().default(30), // --- SAP Business One Service Layer --- SAPB1_SL_URL: z .url() .transform((v) => v.replace(/\/+$/, '')), SAPB1_SL_COMPANYDB: z.string().min(1), SAPB1_SL_USER: z.string().min(1), SAPB1_SL_PASSWORD: z.string().min(1), SAPB1_SL_TIMEOUT_MS: z.coerce.number().int().positive().default(15000), // Peticiones simultáneas permitidas hacia el Service Layer; el resto se encola. // Medido en SAP10 (12 peticiones): con 1 -> 0.6 s en total; con 2 ya tarda 7.5 s y con // 4, 22 s (el Service Layer retrasa ~7 s por cada petición paralela extra). Default: 1. SAPB1_SL_MAX_CONCURRENCY: z.coerce.number().int().positive().default(1), // CA adicional de confianza (PEM): certificado autofirmado del Service // Layer o CA de un firewall que inspecciona SSL. Opcional. SAPB1_SL_CA_FILE: z.preprocess(emptyToUndefined, z.string().optional()), SAPB1_SL_VERIFY_SSL: z .enum(['true', 'false']) .default('true') .transform((v) => v === 'true'), }) .superRefine((env, ctx) => { if (env.SAPB1_SL_CA_FILE && !existsSync(resolve(env.SAPB1_SL_CA_FILE))) { ctx.addIssue({ code: 'custom', path: ['SAPB1_SL_CA_FILE'], message: `no existe el archivo ${resolve(env.SAPB1_SL_CA_FILE)}`, }); } if (env.NODE_ENV === 'production' && !env.SAPB1_SL_VERIFY_SSL) { ctx.addIssue({ code: 'custom', path: ['SAPB1_SL_VERIFY_SSL'], message: 'no se permite desactivar la validación SSL en producción; use SAPB1_SL_CA_FILE', }); } }); export type Env = z.infer; export function validateEnv(raw: Record): Env { const result = envSchema.safeParse(raw); if (!result.success) { const detail = result.error.issues .map((i) => `${i.path.join('.')}: ${i.message}`) .join('; '); throw new Error(`Configuración inválida (.env): ${detail}`); } return result.data; }