veza/apps/web/src/config/env.ts
senke aa7980ab71 fix(streaming): ensure HLS audio chain works end-to-end
- HAProxy: route /hls to stream server
- Vite proxy: /ws, /stream, /hls for dev
- HLS_BASE_URL: empty when STREAM_URL relative (proxy)
- FEATURE_STATUS: HLS_STREAMING operational
2026-02-18 12:42:42 +01:00

137 lines
5.1 KiB
TypeScript

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_HLS_BASE_URL: urlOrPathSchema.optional(),
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_HLS_BASE_URL: import.meta.env.VITE_HLS_BASE_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
}
}
}
// HLS base URL: explicit or derived from STREAM_URL (ws://host:port -> http://host:port)
// When STREAM_URL is relative (/stream), use '' so HLS URLs are relative (/hls/...) and get proxied
const deriveHLSBaseURL = (): string => {
if (validatedEnv.VITE_HLS_BASE_URL) return validatedEnv.VITE_HLS_BASE_URL;
const streamUrl = validatedEnv.VITE_STREAM_URL;
if (streamUrl.startsWith('/')) return ''; // Relative: /hls/... will be proxied
if (streamUrl.startsWith('ws://')) return streamUrl.replace('ws://', 'http://').replace(/\/stream.*$/, '');
if (streamUrl.startsWith('wss://')) return streamUrl.replace('wss://', 'https://').replace(/\/stream.*$/, '');
return `http://${domain}:8082`;
};
// 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,
HLS_BASE_URL: deriveHLSBaseURL(),
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;