-
Notifications
You must be signed in to change notification settings - Fork 14
Expand file tree
/
Copy pathnext.config.mjs
More file actions
151 lines (148 loc) · 8.07 KB
/
Copy pathnext.config.mjs
File metadata and controls
151 lines (148 loc) · 8.07 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
import { execSync } from "node:child_process";
// 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 → 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 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: {
// proxy.ts clones request bodies; the default clone cap is 10MB, which
// silently truncated large /api/v1/fs/upload payloads. Raise it so big
// media uploads land intact (proxy* is the Next 16 name of the option).
// Kept just above the upload route's 200 MiB running cap (which streams to
// disk and returns 413 past it) so the route — not the proxy — owns rejection.
proxyClientMaxBodySize: "256mb",
// Tree-shake heavy icon/radix barrels — keeps the OS shell bundle lean.
optimizePackageImports: [
"lucide-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;