next.config.mjs
Rama main · réplica de lectura del repositorio original.
import { execSync } from "node:child_process";
import { PROXY_BODY_LIMIT } from "./scripts/server-body-policy.mjs";
// Build id baked into the client bundle so a redeploy can be detected.
const BUILD_ID =
process.env.NEXT_PUBLIC_BUILD_ID ||
process.env.NEXT_DEPLOYMENT_ID ||
String(Date.now());
// WHICH COMMIT THIS BUILD IS, baked in at build time — the only way the running app
// can know it, since `git rev-parse HEAD` at runtime answers for the CHECKOUT, which
// moves whenever someone pulls. Settings → Account → About compares the two: equal means the
// running build is current, different means a build is pending and the update panel
// offers the rebuild. Empty is fine and expected — scripts/verify-build.sh compiles
// a `git archive` export with no .git in it.
const BUILD_CPUS_RAW = Number(process.env.MSO_BUILD_CPUS || "");
const BUILD_CPUS =
Number.isInteger(BUILD_CPUS_RAW) && BUILD_CPUS_RAW > 0
? BUILD_CPUS_RAW
: undefined;
const LOW_MEMORY_BUILD = process.env.MSO_LOW_MEMORY_BUILD === "1";
const COMMIT_SHA =
process.env.NEXT_PUBLIC_COMMIT_SHA ||
(() => {
try {
return execSync("git rev-parse --short HEAD", { stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
} catch {
return "";
}
})();
/** @type {import('next').NextConfig} */
const nextConfig = {
// Don't advertise the framework/version — the shell is now public; minimize
// the fingerprint an unauthenticated visitor can use for CVE matching.
poweredByHeader: false,
// Cache Components off: mso is a fully dynamic app (auth-gated OS shell,
// no SSG marketing pages), so static prerendering adds no value and conflicts
// with the auth provider's cookie reads. Re-enable only if static routes land.
cacheComponents: false,
// The Browser app renders site favicons via Google's s2 service (fixed host)
// through next/Image, so the optimizer caches/resizes them. Host filesystem
// images + the live Playwright screenshot stream stay raw <img> on purpose —
// they're dynamic/auth'd bytes the optimizer can't help with.
images: {
remotePatterns: [
{ protocol: "https", hostname: "www.google.com", pathname: "/s2/favicons/**" },
],
},
// node-pty is a native addon (.node binary) — it must be require()'d from
// node_modules at runtime, never bundled, or the binding fails to load.
serverExternalPackages: ["node-pty"],
// Only ever an EXPLICIT deployment id — never BUILD_ID. This config is evaluated
// twice (once by `next build`, once when `next start` boots), so a Date.now()
// fallback produced two different ?dpl= values for the same chunk: the HTML
// referenced both variants and the browser downloaded, parsed and executed ~160 KB
// gzip of entry chunks twice on every cold load. Chunks are content-hashed, so no
// ?dpl= at all is correct.
//
// The key must be ABSENT, not undefined: `deploymentId: undefined` still counts as
// set, and Next then appends a literal empty `?dpl=` — which leaves `chunk.js` and
// `chunk.js?dpl=` as two distinct URLs and the double download intact. Hence the
// conditional spread. BUILD_ID below is unaffected: it is inlined at build time and
// the SW cache name + /api/health still need it.
...(process.env.NEXT_DEPLOYMENT_ID
? { deploymentId: process.env.NEXT_DEPLOYMENT_ID }
: {}),
env: { NEXT_PUBLIC_BUILD_ID: BUILD_ID, NEXT_PUBLIC_COMMIT_SHA: COMMIT_SHA },
experimental: {
// Constrained Android/PRoot builders can opt into bounded worker fan-out.
// Normal VPS/CI builds are unchanged unless these environment variables exist.
...(BUILD_CPUS
? {
cpus: BUILD_CPUS,
staticGenerationMaxConcurrency: BUILD_CPUS,
}
: {}),
...(LOW_MEMORY_BUILD
? {
parallelServerCompiles: false,
parallelServerBuildTraces: false,
turbopackMemoryEviction: "auto",
}
: {}),
// proxy.ts clones request bodies; the default clone cap is 10MB, which
// The authenticated upload route bypasses cloning and streams its own 200 MiB cap.
// Public middleware requests keep a small aggregate buffer ceiling.
proxyClientMaxBodySize: PROXY_BODY_LIMIT,
// Tree-shake heavy icon/radix barrels — keeps the OS shell bundle lean.
optimizePackageImports: [
"lucide-react",
"@phosphor-icons/react",
"@radix-ui/react-dialog",
"@radix-ui/react-dropdown-menu",
"@radix-ui/react-tooltip",
"@radix-ui/react-scroll-area",
"@radix-ui/react-select",
"@radix-ui/react-popover",
"@radix-ui/react-alert-dialog",
"@radix-ui/react-slider",
"@radix-ui/react-tabs",
],
},
// Serve the service worker at /sw.js (stable scope) from the /api/sw route
// handler. beforeFiles runs BEFORE page routing, so the optional catch-all
// [[...slug]] never swallows /sw.js (a route under /api is never caught by it).
async rewrites() {
return {
beforeFiles: [
{ source: "/sw.js", destination: "/api/sw" },
// NO camoufox rewrite here, deliberately. It used to live here and published
// websockify (127.0.0.1:6080, x11vnc behind it) to the public origin with no
// real gate. A config rewrite bypasses route handlers, so nothing downstream
// can authenticate it — proxy.ts owns that hop now and rewrites it itself,
// behind hasApprovedSession(). Do not add it back.
],
afterFiles: [],
fallback: [],
};
},
async headers() {
return [
{
source: "/(.*)",
headers: [
{ key: "X-Content-Type-Options", value: "nosniff" },
// Authenticated remote shell over HTTPS — refuse any plaintext leg so
// a MITM can't strip TLS and capture the session cookie before the
// redirect fires. 1 year. includeSubDomains omitted on purpose: not
// every configured cookie-domain subdomain is HTTPS-only.
{ key: "Strict-Transport-Security", value: "max-age=31536000" },
{ key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
// MSO is a private control plane even when reached through a temporary HTTPS tunnel.
{ key: "X-Robots-Tag", value: "noindex, nofollow, noarchive" },
// Public remote shell — never allow it to be framed (clickjacking on
// exec/fs buttons). The full Content-Security-Policy (incl. a nonce'd
// script-src) is set PER-REQUEST in proxy.ts so a fresh nonce can gate
// inline scripts; a static CSP here can't nonce, so it's intentionally
// omitted (proxy owns it).
{ key: "X-Frame-Options", value: "DENY" },
{
key: "Permissions-Policy",
value: "camera=(), microphone=(), geolocation=()",
},
],
},
{
source: "/camoufox-vnc/:path*",
headers: [{ key: "X-Frame-Options", value: "SAMEORIGIN" }],
},
// Private API responses must never be cached by any intermediary — they
// carry host bytes (fs/read), auth state, and per-session data.
{ source: "/api/:path*", headers: [{ key: "Cache-Control", value: "no-store" }] },
// Only the internal manager is same-origin embeddable; its nonce CSP also
// restricts frame-ancestors to self. The standalone page/shell retain DENY.
{ source: "/integrations", has: [{ type: "query", key: "embed", value: "shell" }], headers: [{ key: "X-Frame-Options", value: "SAMEORIGIN" }] },
{ source: "/integrations/manager", headers: [{ key: "X-Frame-Options", value: "SAMEORIGIN" }, { key: "Cache-Control", value: "no-store" }, { key: "Referrer-Policy", value: "no-referrer" }] },
{ source: "/integrations", headers: [{ key: "Cache-Control", value: "no-store" }, { key: "Referrer-Policy", value: "no-referrer" }] },
// Dedicated shell-only embed path. The global DENY remains authoritative for
// every ordinary page; this one path is still same-origin only and also has
// a route-level CSP `frame-ancestors 'self'`.
{ source: "/integrations/embed", headers: [{ key: "X-Frame-Options", value: "SAMEORIGIN" }, { key: "Cache-Control", value: "no-store" }, { key: "Referrer-Policy", value: "no-referrer" }] },
{ source: "/api/integrations/:path*", headers: [{ key: "Referrer-Policy", value: "no-referrer" }] },
// Named brand/wallpaper assets are effectively immutable but, unlike
// /_next chunks, are NOT content-hashed — rename or add ?v= if ever redrawn.
{ source: "/wallpapers/:path*", headers: [{ key: "Cache-Control", value: "public, max-age=31536000, immutable" }] },
{ source: "/icon.svg", headers: [{ key: "Cache-Control", value: "public, max-age=604800" }] },
{ source: "/icon-maskable.svg", headers: [{ key: "Cache-Control", value: "public, max-age=604800" }] },
];
},
};
export default nextConfig;