import path from "node:path"; import react from "@vitejs/plugin-react"; import { createLogger, defineConfig } from "vite"; import inlineEditPlugin from "./plugins/visual-editor/vite-plugin-react-inline-editor.js"; import editModeDevPlugin from "./plugins/visual-editor/vite-plugin-edit-mode.js"; import iframeRouteRestorationPlugin from "./plugins/vite-plugin-iframe-route-restoration.js"; import selectionModePlugin from "./plugins/selection-mode/vite-plugin-selection-mode.js"; const isDev = process.env.NODE_ENV !== "production"; // Configuration des origines autorisées pour postMessage // En production, définir via variable d'environnement const ALLOWED_PARENT_ORIGINS = isDev ? [ "http://localhost:3000", "http://localhost:5173", "https://prod.dataven.fr", ] : ( process.env.VITE_ALLOWED_PARENT_ORIGINS || "https://prod.dataven.fr" ).split(","); // Utilitaire pour postMessage sécurisé avec handshake const configSecurePostMessage = ` (function() { const ALLOWED_ORIGINS = ${JSON.stringify(ALLOWED_PARENT_ORIGINS)}; let verifiedParentOrigin = null; let isEmbedded = false; // Détecter si on est dans une iframe try { isEmbedded = window.self !== window.top; } catch (e) { isEmbedded = true; // Erreur de cross-origin = on est dans une iframe } // Écouter le handshake du parent window.addEventListener('message', (event) => { if (!ALLOWED_ORIGINS.includes(event.origin)) { return; // Ignorer les origines non autorisées } if (event.data?.type === 'HORIZONS_HANDSHAKE') { verifiedParentOrigin = event.origin; // Confirmer le handshake window.parent.postMessage({ type: 'HORIZONS_HANDSHAKE_ACK' }, event.origin); } }); // Fonction sécurisée pour envoyer des messages au parent window.__securePostMessage = function(payload) { if (!isEmbedded) return; if (!verifiedParentOrigin) { // En attente de handshake, stocker temporairement (optionnel) console.debug('[SecurePostMessage] No verified parent origin yet'); return; } // Sanitiser le payload avant envoi const sanitizedPayload = sanitizePayload(payload); window.parent.postMessage(sanitizedPayload, verifiedParentOrigin); }; // Sanitisation du payload function sanitizePayload(payload) { const sanitized = { ...payload }; ${ isDev ? ` // En DEV: conserver les détails pour le debugging return sanitized; ` : ` // En PROD: supprimer les infos sensibles if (sanitized.error) { // Remplacer la stack trace par un ID de corrélation sanitized.errorId = generateCorrelationId(); sanitized.error = sanitized.error.split('\\n')[0]; // Garder uniquement la première ligne delete sanitized.stack; } // Ne pas exposer les URLs internes ou les détails de réponse if (sanitized.url) { sanitized.url = '[REDACTED]'; } return sanitized; ` } } function generateCorrelationId() { return 'err_' + Date.now().toString(36) + Math.random().toString(36).substr(2, 9); } // Exposer pour vérification window.__isSecureMessagingReady = () => !!verifiedParentOrigin; })(); `; // Handler d'erreurs Vite sécurisé const configHorizonsViteErrorHandler = ` const observer = new MutationObserver((mutations) => { for (const mutation of mutations) { for (const addedNode of mutation.addedNodes) { if ( addedNode.nodeType === Node.ELEMENT_NODE && (addedNode.tagName?.toLowerCase() === 'vite-error-overlay' || addedNode.classList?.contains('backdrop')) ) { handleViteOverlay(addedNode); } } } }); observer.observe(document.documentElement, { childList: true, subtree: true }); function handleViteOverlay(node) { if (!node.shadowRoot) return; const backdrop = node.shadowRoot.querySelector('.backdrop'); if (!backdrop) return; const messageBodyElement = backdrop.querySelector('.message-body'); const messageText = messageBodyElement ? messageBodyElement.textContent.trim() : ''; // Utiliser la fonction sécurisée window.__securePostMessage?.({ type: 'horizons-vite-error', error: messageText, timestamp: Date.now() }); } `; // Handler d'erreurs runtime sécurisé const configHorizonsRuntimeErrorHandler = ` window.onerror = (message, source, lineno, colno, errorObj) => { ${ isDev ? ` // DEV: Détails complets pour debugging const errorDetails = errorObj ? { name: errorObj.name, message: errorObj.message, stack: errorObj.stack, source, lineno, colno, } : null; ` : ` // PROD: Informations minimales const errorDetails = errorObj ? { name: errorObj.name, message: errorObj.message?.substring(0, 100), // Tronquer } : null; ` } window.__securePostMessage?.({ type: 'horizons-runtime-error', message: typeof message === 'string' ? message.substring(0, 200) : 'Unknown error', error: errorDetails, timestamp: Date.now() }); }; `; // Handler console.error sécurisé const configHorizonsConsoleErrorHandler = ` const originalConsoleError = console.error; console.error = function(...args) { originalConsoleError.apply(console, args); ${ isDev ? ` // DEV: Logger les détails let errorString = ''; for (const arg of args) { if (arg instanceof Error) { errorString = arg.stack || \`\${arg.name}: \${arg.message}\`; break; } } if (!errorString) { errorString = args.map(arg => typeof arg === 'object' ? JSON.stringify(arg) : String(arg) ).join(' '); } ` : ` // PROD: Message générique uniquement let errorString = 'An error occurred'; for (const arg of args) { if (arg instanceof Error) { errorString = arg.name + ': ' + (arg.message || '').substring(0, 50); break; } } ` } window.__securePostMessage?.({ type: 'horizons-console-error', error: errorString, timestamp: Date.now() }); }; `; // Monkey patch fetch sécurisé const configWindowFetchMonkeyPatch = ` const originalFetch = window.fetch; window.fetch = function(...args) { const url = args[0] instanceof Request ? args[0].url : args[0]; // Skip WebSocket URLs if (url.startsWith('ws:') || url.startsWith('wss:')) { return originalFetch.apply(this, args); } return originalFetch.apply(this, args) .then(async response => { const contentType = response.headers.get('Content-Type') || ''; const isDocumentResponse = contentType.includes('text/html') || contentType.includes('application/xhtml+xml'); if (!response.ok && !isDocumentResponse) { ${ isDev ? ` // DEV: Logger le contenu de l'erreur const responseClone = response.clone(); const errorFromRes = await responseClone.text(); console.error(\`Fetch error from \${response.url}: \${errorFromRes}\`); ` : ` // PROD: Ne pas exposer le contenu de la réponse console.error(\`Fetch error: \${response.status}\`); ` } } return response; }) .catch(error => { if (!url.match(/\\.html?$/i)) { ${ isDev ? ` console.error(error); ` : ` // PROD: Ne pas exposer l'URL ou les détails console.error('Network request failed'); ` } } throw error; }); }; `; // Handler de navigation sécurisé const configNavigationHandler = ` if (window.navigation && window.self !== window.top) { window.navigation.addEventListener('navigate', (event) => { const url = event.destination.url; try { const destinationUrl = new URL(url); const currentOrigin = window.location.origin; if (destinationUrl.origin === currentOrigin) { return; } } catch (error) { return; } window.__securePostMessage?.({ type: 'horizons-navigation-error', ${isDev ? `url,` : `url: '[external]',`} timestamp: Date.now() }); }); } `; const addTransformIndexHtml = { name: "add-transform-index-html", transformIndexHtml(html) { const tags = [ // Le script de postMessage sécurisé doit être chargé EN PREMIER { tag: "script", attrs: { type: "module" }, children: configSecurePostMessage, injectTo: "head-prepend", // Avant tous les autres scripts }, { tag: "script", attrs: { type: "module" }, children: configHorizonsRuntimeErrorHandler, injectTo: "head", }, { tag: "script", attrs: { type: "module" }, children: configHorizonsViteErrorHandler, injectTo: "head", }, { tag: "script", attrs: { type: "module" }, children: configHorizonsConsoleErrorHandler, injectTo: "head", }, { tag: "script", attrs: { type: "module" }, children: configWindowFetchMonkeyPatch, injectTo: "head", }, { tag: "script", attrs: { type: "module" }, children: configNavigationHandler, injectTo: "head", }, ]; if ( !isDev && process.env.TEMPLATE_BANNER_SCRIPT_URL && process.env.TEMPLATE_REDIRECT_URL ) { tags.push({ tag: "script", attrs: { src: process.env.TEMPLATE_BANNER_SCRIPT_URL, "template-redirect-url": process.env.TEMPLATE_REDIRECT_URL, }, injectTo: "head", }); } return { html, tags }; }, }; console.warn = () => {}; const logger = createLogger(); const loggerError = logger.error; logger.error = (msg, options) => { if (options?.error?.toString().includes("CssSyntaxError: [postcss]")) { return; } loggerError(msg, options); }; export default defineConfig({ customLogger: logger, plugins: [ ...(isDev ? [ inlineEditPlugin(), editModeDevPlugin(), iframeRouteRestorationPlugin(), selectionModePlugin(), ] : []), react(), addTransformIndexHtml, ], server: { cors: true, headers: { "Cross-Origin-Embedder-Policy": "credentialless", // Ajouter CSP pour frame-ancestors si l'app ne doit pas être iframée // ou limiter aux origines autorisées ...(isDev ? {} : { "Content-Security-Policy": `frame-ancestors ${ALLOWED_PARENT_ORIGINS.join( " " )}`, "X-Frame-Options": "SAMEORIGIN", // Fallback pour anciens navigateurs }), }, allowedHosts: true, hmr: { overlay: false }, watch: { usePolling: true, ignored: ["**/.git/**"] }, }, optimizeDeps: { force: true }, // esbuild: { // loader: 'jsx', // include: /\.js$/, // }, resolve: { extensions: [".jsx", ".js", ".tsx", ".ts", ".json"], alias: { "@": path.resolve(__dirname, "./src") }, }, build: { rollupOptions: { external: [ "@babel/parser", "@babel/traverse", "@babel/generator", "@babel/types", ], }, }, cacheDir: "node_modules/.vite", }); // import path from 'node:path'; // import react from '@vitejs/plugin-react'; // import { createLogger, defineConfig } from 'vite'; // import inlineEditPlugin from './plugins/visual-editor/vite-plugin-react-inline-editor.js'; // import editModeDevPlugin from './plugins/visual-editor/vite-plugin-edit-mode.js'; // import iframeRouteRestorationPlugin from './plugins/vite-plugin-iframe-route-restoration.js'; // import selectionModePlugin from './plugins/selection-mode/vite-plugin-selection-mode.js'; // const isDev = process.env.NODE_ENV !== 'production'; // const configHorizonsViteErrorHandler = ` // const observer = new MutationObserver((mutations) => { // for (const mutation of mutations) { // for (const addedNode of mutation.addedNodes) { // if ( // addedNode.nodeType === Node.ELEMENT_NODE && // ( // addedNode.tagName?.toLowerCase() === 'vite-error-overlay' || // addedNode.classList?.contains('backdrop') // ) // ) { // handleViteOverlay(addedNode); // } // } // } // }); // observer.observe(document.documentElement, { // childList: true, // subtree: true // }); // function handleViteOverlay(node) { // if (!node.shadowRoot) { // return; // } // const backdrop = node.shadowRoot.querySelector('.backdrop'); // if (backdrop) { // const overlayHtml = backdrop.outerHTML; // const parser = new DOMParser(); // const doc = parser.parseFromString(overlayHtml, 'text/html'); // const messageBodyElement = doc.querySelector('.message-body'); // const fileElement = doc.querySelector('.file'); // const messageText = messageBodyElement ? messageBodyElement.textContent.trim() : ''; // const fileText = fileElement ? fileElement.textContent.trim() : ''; // const error = messageText + (fileText ? ' File:' + fileText : ''); // window.parent.postMessage({ // type: 'horizons-vite-error', // error, // }, '*'); // } // } // `; // const configHorizonsRuntimeErrorHandler = ` // window.onerror = (message, source, lineno, colno, errorObj) => { // const errorDetails = errorObj ? JSON.stringify({ // name: errorObj.name, // message: errorObj.message, // stack: errorObj.stack, // source, // lineno, // colno, // }) : null; // window.parent.postMessage({ // type: 'horizons-runtime-error', // message, // error: errorDetails // }, '*'); // }; // `; // const configHorizonsConsoleErrroHandler = ` // const originalConsoleError = console.error; // console.error = function(...args) { // originalConsoleError.apply(console, args); // let errorString = ''; // for (let i = 0; i < args.length; i++) { // const arg = args[i]; // if (arg instanceof Error) { // errorString = arg.stack || \`\${arg.name}: \${arg.message}\`; // break; // } // } // if (!errorString) { // errorString = args.map(arg => typeof arg === 'object' ? JSON.stringify(arg) : String(arg)).join(' '); // } // window.parent.postMessage({ // type: 'horizons-console-error', // error: errorString // }, '*'); // }; // `; // const configWindowFetchMonkeyPatch = ` // const originalFetch = window.fetch; // window.fetch = function(...args) { // const url = args[0] instanceof Request ? args[0].url : args[0]; // // Skip WebSocket URLs // if (url.startsWith('ws:') || url.startsWith('wss:')) { // return originalFetch.apply(this, args); // } // return originalFetch.apply(this, args) // .then(async response => { // const contentType = response.headers.get('Content-Type') || ''; // // Exclude HTML document responses // const isDocumentResponse = // contentType.includes('text/html') || // contentType.includes('application/xhtml+xml'); // if (!response.ok && !isDocumentResponse) { // const responseClone = response.clone(); // const errorFromRes = await responseClone.text(); // const requestUrl = response.url; // console.error(\`Fetch error from \${requestUrl}: \${errorFromRes}\`); // } // return response; // }) // .catch(error => { // if (!url.match(/\.html?$/i)) { // console.error(error); // } // throw error; // }); // }; // `; // const configNavigationHandler = ` // if (window.navigation && window.self !== window.top) { // window.navigation.addEventListener('navigate', (event) => { // const url = event.destination.url; // try { // const destinationUrl = new URL(url); // const destinationOrigin = destinationUrl.origin; // const currentOrigin = window.location.origin; // if (destinationOrigin === currentOrigin) { // return; // } // } catch (error) { // return; // } // window.parent.postMessage({ // type: 'horizons-navigation-error', // url, // }, '*'); // }); // } // `; // const addTransformIndexHtml = { // name: 'add-transform-index-html', // transformIndexHtml(html) { // const tags = [ // { // tag: 'script', // attrs: { type: 'module' }, // children: configHorizonsRuntimeErrorHandler, // injectTo: 'head', // }, // { // tag: 'script', // attrs: { type: 'module' }, // children: configHorizonsViteErrorHandler, // injectTo: 'head', // }, // { // tag: 'script', // attrs: {type: 'module'}, // children: configHorizonsConsoleErrroHandler, // injectTo: 'head', // }, // { // tag: 'script', // attrs: { type: 'module' }, // children: configWindowFetchMonkeyPatch, // injectTo: 'head', // }, // { // tag: 'script', // attrs: { type: 'module' }, // children: configNavigationHandler, // injectTo: 'head', // }, // ]; // if (!isDev && process.env.TEMPLATE_BANNER_SCRIPT_URL && process.env.TEMPLATE_REDIRECT_URL) { // tags.push( // { // tag: 'script', // attrs: { // src: process.env.TEMPLATE_BANNER_SCRIPT_URL, // 'template-redirect-url': process.env.TEMPLATE_REDIRECT_URL, // }, // injectTo: 'head', // } // ); // } // return { // html, // tags, // }; // }, // }; // console.warn = () => {}; // const logger = createLogger() // const loggerError = logger.error // logger.error = (msg, options) => { // if (options?.error?.toString().includes('CssSyntaxError: [postcss]')) { // return; // } // loggerError(msg, options); // } // export default defineConfig({ // customLogger: logger, // plugins: [ // ...(isDev ? [inlineEditPlugin(), editModeDevPlugin(), iframeRouteRestorationPlugin(), selectionModePlugin()] : []), // react(), // addTransformIndexHtml // ], // server: { // cors: true, // headers: { // 'Cross-Origin-Embedder-Policy': 'credentialless', // }, // allowedHosts: true, // // watch: { // // ignored: ['**/.git/**'] // // }, // hmr: { // overlay: true, // Afficher les erreurs en overlay // }, // watch: { // usePolling: true, // Utile sur certains systèmes // } // }, // optimizeDeps: { // force: true, // Force la ré-optimisation des dépendances // }, // resolve: { // extensions: ['.jsx', '.js', '.tsx', '.ts', '.json', ], // alias: { // '@': path.resolve(__dirname, './src'), // }, // }, // build: { // rollupOptions: { // external: [ // '@babel/parser', // '@babel/traverse', // '@babel/generator', // '@babel/types' // ] // } // }, // // optimizeDeps: { // // exclude: ['.git'] // // }, // // Vider le cache au démarrage si nécessaire // cacheDir: 'node_modules/.vite', // });