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,
},
};
});