import { z } from 'zod'; import { logger } from '@/utils/logger'; // Schéma de validation pour les variables d'environnement // Aligné avec FRONTEND_INTEGRATION.md // Support URLs relatives (commençant par /) ou absolues const urlOrPathSchema = z.string().refine( (val) => { if (!val) return false; // Accepter les URLs absolues (http://, https://, ws://, wss://) if (/^https?:\/\//.test(val) || /^wss?:\/\//.test(val)) { try { new URL(val); return true; } catch { return false; } } // Accepter les chemins relatifs (commençant par /) return val.startsWith('/'); }, { message: 'Must be a valid URL or a path starting with /' } ); // --- Domain (single source of truth for frontend URLs) --- // Change VITE_DOMAIN in .env.local to switch domain everywhere. const domain = import.meta.env.VITE_DOMAIN || 'veza.fr'; const envSchema = z.object({ VITE_DOMAIN: z.string().default('veza.fr'), VITE_API_URL: urlOrPathSchema.default('/api/v1'), VITE_WS_URL: urlOrPathSchema.default(`ws://${domain}:8081/ws`), VITE_STREAM_URL: urlOrPathSchema.default(`ws://${domain}:8082/stream`), VITE_UPLOAD_URL: urlOrPathSchema.default('/upload'), VITE_APP_NAME: z.string().default('Veza'), VITE_API_VERSION: z.string().default('v1'), VITE_DEBUG: z .string() .transform((val) => val === 'true' || val === '1') .default('false'), VITE_USE_MSW: z .string() .transform((val) => val === '1' || val === 'true') .default('0'), VITE_HYPERSWITCH_PUBLISHABLE_KEY: z.string().optional(), VITE_FCM_VAPID_KEY: z.string().optional(), // FIX #20: Configuration Sentry pour error tracking VITE_SENTRY_DSN: z.string().url().optional(), }); // Validation et parsing des variables d'environnement const parseEnv = () => { try { return envSchema.parse({ VITE_DOMAIN: import.meta.env.VITE_DOMAIN, VITE_API_URL: import.meta.env.VITE_API_URL, VITE_WS_URL: import.meta.env.VITE_WS_URL, VITE_STREAM_URL: import.meta.env.VITE_STREAM_URL, VITE_UPLOAD_URL: import.meta.env.VITE_UPLOAD_URL, VITE_APP_NAME: import.meta.env.VITE_APP_NAME, VITE_API_VERSION: import.meta.env.VITE_API_VERSION, VITE_DEBUG: import.meta.env.VITE_DEBUG, VITE_USE_MSW: import.meta.env.VITE_USE_MSW, VITE_HYPERSWITCH_PUBLISHABLE_KEY: import.meta.env.VITE_HYPERSWITCH_PUBLISHABLE_KEY, VITE_FCM_VAPID_KEY: import.meta.env.VITE_FCM_VAPID_KEY, VITE_SENTRY_DSN: import.meta.env.VITE_SENTRY_DSN, }); } catch (error) { if (error instanceof z.ZodError) { logger.error('❌ Invalid environment variables', { errors: error.errors, }); throw new Error( `Environment variables validation failed: ${error.errors .map((e) => `${e.path.join('.')}: ${e.message}`) .join(', ')}`, ); } throw error; } }; // Variables d'environnement validées const validatedEnv = parseEnv(); // En dev, alerter si l'API est en cross-origin : les cookies ne seront pas envoyés (SameSite), // ce qui provoque 401 après login et redirections en boucle. Utiliser VITE_API_URL=/api/v1 (proxy). if (import.meta.env.DEV && typeof window !== 'undefined') { const apiUrl = validatedEnv.VITE_API_URL; if (apiUrl.startsWith('http')) { try { const apiOrigin = new URL(apiUrl).origin; if (window.location.origin !== apiOrigin) { logger.warn( '[Config] API is cross-origin: cookies will not be sent, login may fail or redirect in a loop. Use VITE_API_URL=/api/v1 so the Vite proxy is used (same origin).', { apiOrigin, pageOrigin: window.location.origin } ); } } catch { // ignore invalid URL } } } // Export de l'objet env avec types export const env = { DOMAIN: validatedEnv.VITE_DOMAIN, API_URL: validatedEnv.VITE_API_URL, WS_URL: validatedEnv.VITE_WS_URL, STREAM_URL: validatedEnv.VITE_STREAM_URL, UPLOAD_URL: validatedEnv.VITE_UPLOAD_URL, APP_NAME: validatedEnv.VITE_APP_NAME, API_VERSION: validatedEnv.VITE_API_VERSION, DEBUG: validatedEnv.VITE_DEBUG, USE_MSW: validatedEnv.VITE_USE_MSW, FCM_VAPID_KEY: validatedEnv.VITE_FCM_VAPID_KEY, // FIX #20: Configuration Sentry SENTRY_DSN: validatedEnv.VITE_SENTRY_DSN, } as const; // Type pour les variables d'environnement export type Env = typeof env;