import { defineConfig, type Connect } from "vite"; import react from "@vitejs/plugin-react"; // Dev-only middleware that lets the in-OS browser embed external pages. // X-Frame-Options / CSP frame-ancestors are enforced by the BROWSER based on the // target's response headers and cannot be bypassed client-side. We fetch the page // server-side (where those headers are inert), strip the framing headers, inject a // tag so relative assets resolve, and keep link navigation inside the proxy. function browserProxyPlugin() { const handler: Connect.NextHandleFunction = async (req, res, next) => { if (!req.url || !req.url.startsWith("/__proxy")) return next(); const target = new URL(req.url, "http://localhost").searchParams.get("url"); if (!target || !/^https?:\/\//i.test(target)) { res.statusCode = 400; res.end("Missing or invalid ?url="); return; } try { const upstream = await fetch(target, { redirect: "follow", headers: { "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36", accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8", "accept-language": "en,de;q=0.8", }, }); const ctype = upstream.headers.get("content-type") || "application/octet-stream"; // pass through most headers, but drop framing/security/encoding ones const DROP = new Set([ "x-frame-options", "content-security-policy", "content-security-policy-report-only", "content-encoding", "content-length", "transfer-encoding", "strict-transport-security", "set-cookie", ]); upstream.headers.forEach((v, k) => { if (!DROP.has(k.toLowerCase())) res.setHeader(k, v); }); res.statusCode = upstream.status; if (ctype.includes("text/html")) { let html = await upstream.text(); const baseHref = upstream.url || target; // remove any CSP set via html = html.replace(/]+http-equiv=["']?content-security-policy["']?[^>]*>/gi, ""); const inject = `` + ``; html = /]*>/i.test(html) ? html.replace(/]*>/i, (m) => m + inject) : inject + html; res.setHeader("content-type", "text/html; charset=utf-8"); res.end(html); } else { const buf = new Uint8Array(await upstream.arrayBuffer()); res.setHeader("content-type", ctype); res.end(buf); } } catch (e) { res.statusCode = 502; res.setHeader("content-type", "text/html; charset=utf-8"); res.end( `` + `

⚠️ Proxy could not load this page

` + `

${(e as Error).message}

${target}

` ); } }; return { name: "in-os-browser-proxy", configureServer(server: { middlewares: Connect.Server }) { server.middlewares.use(handler); }, configurePreviewServer(server: { middlewares: Connect.Server }) { server.middlewares.use(handler); }, }; } // Dynamic AI proxy: the browser POSTs to "/__ai/proxy" with the full upstream // endpoint in the "x-llm-url" header. The dev server forwards the request // server-side and streams the response back same-origin. This sidesteps CORS // for every provider (Anthropic, OpenAI, OpenRouter, Grok, Cerebras) and lets // the active provider be switched at runtime without restarting Vite. function aiProxyPlugin() { // Only these headers are forwarded upstream — auth + protocol, nothing else. const FORWARD = new Set([ "content-type", "accept", "authorization", "x-api-key", "anthropic-version", "anthropic-dangerous-direct-browser-access", "http-referer", "x-title", ]); const handler: Connect.NextHandleFunction = async (req, res, next) => { if (!req.url || !req.url.startsWith("/__ai/proxy")) return next(); const target = (req.headers["x-llm-url"] as string) || ""; if (!/^https?:\/\//i.test(target)) { res.statusCode = 400; res.end("Missing or invalid x-llm-url header"); return; } try { const chunks: Buffer[] = []; for await (const c of req) chunks.push(c as Buffer); const body = Buffer.concat(chunks); const headers: Record = {}; for (const [k, v] of Object.entries(req.headers)) { if (FORWARD.has(k.toLowerCase()) && typeof v === "string") headers[k] = v; } const upstream = await fetch(target, { method: req.method || "POST", headers, body: req.method === "GET" || req.method === "HEAD" ? undefined : body, }); res.statusCode = upstream.status; const ctype = upstream.headers.get("content-type"); if (ctype) res.setHeader("content-type", ctype); if (upstream.body) { const reader = upstream.body.getReader(); while (true) { const { done, value } = await reader.read(); if (done) break; if (value) res.write(Buffer.from(value)); } } res.end(); } catch (e) { res.statusCode = 502; res.setHeader("content-type", "application/json"); res.end(JSON.stringify({ error: { message: (e as Error).message } })); } }; return { name: "fluid-os-ai-proxy", configureServer(server: { middlewares: Connect.Server }) { server.middlewares.use(handler); }, configurePreviewServer(server: { middlewares: Connect.Server }) { server.middlewares.use(handler); }, }; } export default defineConfig(() => { return { plugins: [react(), browserProxyPlugin(), aiProxyPlugin()], server: { port: 5173, }, }; });