leanos/vite.config.ts
Thomas Lutz Kolter ee203fd9fb leanOS: initial import (basiert auf NEON NOODLE OS)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-15 14:58:54 +02:00

168 lines
6.4 KiB
TypeScript

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
// <base> 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 <meta>
html = html.replace(/<meta[^>]+http-equiv=["']?content-security-policy["']?[^>]*>/gi, "");
const inject =
`<base href="${baseHref}">` +
`<script>(function(){var B=${JSON.stringify(baseHref)};` +
`function px(u){try{return "/__proxy?url="+encodeURIComponent(new URL(u,B).href);}catch(e){return u;}}` +
`document.addEventListener("click",function(e){var a=e.target.closest&&e.target.closest("a[href]");` +
`if(!a)return;var h=a.getAttribute("href");if(!h||h[0]==="#"||/^(javascript|mailto|tel):/i.test(h))return;` +
`var abs;try{abs=new URL(h,B).href;}catch(_){return;}if(!/^https?:/i.test(abs))return;` +
`e.preventDefault();window.top.location.href=px(abs);},true);})();</script>`;
html = /<head[^>]*>/i.test(html)
? html.replace(/<head[^>]*>/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(
`<body style="font-family:sans-serif;padding:24px;color:#333">` +
`<h3>⚠️ Proxy could not load this page</h3>` +
`<p>${(e as Error).message}</p><p style="color:#888">${target}</p></body>`
);
}
};
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<string, string> = {};
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,
},
};
});