import { withSentryConfig } from "@sentry/nextjs"; const __dirname = import.meta.dirname; function trimTrailingSlashes(value) { let end = value.length; while (end > 0 && value[end - 1] === "/") { end -= 1; } return value.slice(0, end); } const c15tBackendUrl = process.env.NEXT_PUBLIC_C15T_URL ? trimTrailingSlashes(process.env.NEXT_PUBLIC_C15T_URL) : undefined; export function buildContentSecurityPolicy({ isDevelopment = false } = {}) { // Monaco Editor compiles language workers via `new Function()`, which // requires 'unsafe-eval'. Next.js also still injects inline bootstrap / // hydration scripts, so 'unsafe-inline' is required until we migrate to a // nonce-based CSP. `isDevelopment` is retained so we can layer additional // dev-only sources (e.g. HMR) without duplicating the allowlist. const scriptSrc = [ "script-src 'self' 'unsafe-inline' 'unsafe-eval' blob: https://va.vercel-scripts.com", ]; const directives = [ "default-src 'self'", "base-uri 'self'", "frame-ancestors 'none'", "form-action 'self'", "object-src 'none'", scriptSrc.join(" "), "style-src 'self' 'unsafe-inline'", "img-src 'self' data: blob: https:", "font-src 'self' data: https:", "connect-src 'self' https: ws: wss:", "frame-src 'self' https:", "worker-src 'self' blob:", "manifest-src 'self'", ]; // Local dev runs over plain HTTP; this directive upgrades same-origin // localhost fetches to HTTPS and masks the real route behavior. if (!isDevelopment) { directives.push("upgrade-insecure-requests"); } return directives.join("; "); } const contentSecurityPolicy = buildContentSecurityPolicy({ isDevelopment: process.env.NODE_ENV === "development", }); const allowedDevOrigins = process.env.MOGPLEX_ALLOWED_DEV_ORIGINS ? process.env.MOGPLEX_ALLOWED_DEV_ORIGINS.split(",") .map((origin) => origin.trim()) .filter(Boolean) : undefined; const sentryOrg = process.env.SENTRY_ORG; const sentryProject = process.env.SENTRY_PROJECT; const sentryAuthToken = process.env.SENTRY_AUTH_TOKEN; const shouldWrapWithSentry = Boolean( sentryOrg && sentryProject && sentryAuthToken ); // SENTRY_TUNNEL_ROUTE in lib/auth-route-policy.ts; keep aligned. Empty when the // build installs no tunnel rewrite, so the proxy 404s the whole path. const sentryTunnelRoute = shouldWrapWithSentry && process.env.NODE_ENV !== "development" ? "/monitoring" : ""; /** @type {import('next').NextConfig} */ const nextConfig = { env: { MOGPLEX_SENTRY_TUNNEL_ROUTE: sentryTunnelRoute, // Compile the serving release into browser code, so an old tab keeps its // own ID after a deploy. Git builds use Vercel's ID; prebuilt builds use ours. NEXT_PUBLIC_MOGPLEX_DEPLOYMENT_ID: process.env.MOGPLEX_DEPLOYMENT_ID || process.env.VERCEL_DEPLOYMENT_ID || "", }, // GitHub Actions builds before Vercel assigns an ID. The CLI registers this // custom ID from the prebuilt output so client requests stay on that release. ...(process.env.MOGPLEX_DEPLOYMENT_ID ? { deploymentId: process.env.MOGPLEX_DEPLOYMENT_ID } : {}), ...(allowedDevOrigins ? { allowedDevOrigins } : {}), // Standalone output is only for the self-hosted Docker image (see // Dockerfile). Vercel builds must keep the default output mode, so this // is opt-in via env rather than set unconditionally. ...(process.env.NEXT_OUTPUT === "standalone" ? { output: "standalone" } : {}), typescript: { ignoreBuildErrors: false, }, images: { unoptimized: true, }, experimental: { optimizePackageImports: ["iconoir-react", "lucide-react"], }, turbopack: { root: __dirname, resolveAlias: { // pg reaches browser chunks through client-reachable modules that lazily // import lib/supabase/admin (→ the Neon PostgREST shim). Its node // builtins (dns, fs, net) don't resolve for the browser target, so the // browser condition swaps in a throwing stub; the server build is // untouched. pg: { browser: "./lib/db/pg-browser-stub.ts" }, }, }, async redirects() { return [ { source: "/favicon.png", destination: "/favicon.ico", permanent: true, }, { source: "/apple-touch-icon.png", destination: "/apple-icon.png", permanent: true, }, { source: "/apple-touch-icon-precomposed.png", destination: "/apple-icon.png", permanent: true, }, { source: "/spaces", destination: "/control", permanent: true, }, { source: "/spaces/sandboxes", destination: "/sandboxes", permanent: true, }, { source: "/projects/repositories/sandboxes", destination: "/sandboxes", permanent: true, }, { source: "/:scope/projects/repositories/sandboxes", destination: "/:scope/sandboxes", permanent: true, }, // The Repositories page was retired; Command Center is the repo home. { source: "/projects/repositories", destination: "/control", permanent: true, }, { source: "/:scope/projects/repositories", destination: "/:scope/control", permanent: true, }, ]; }, async rewrites() { if (!c15tBackendUrl) { return []; } return [ { source: "/api/c15t/:path*", destination: `${c15tBackendUrl}/:path*`, }, ]; }, async headers() { return [ { source: "/(.*)", headers: [ { key: "X-Frame-Options", value: "DENY" }, { key: "X-Content-Type-Options", value: "nosniff" }, { key: "Referrer-Policy", value: "strict-origin-when-cross-origin" }, { key: "Content-Security-Policy", value: contentSecurityPolicy, }, { key: "Permissions-Policy", value: "camera=(), microphone=(), geolocation=()", }, { key: "Strict-Transport-Security", value: "max-age=63072000; includeSubDomains; preload", }, ], }, ]; }, }; export default shouldWrapWithSentry ? withSentryConfig(nextConfig, { org: sentryOrg, project: sentryProject, authToken: sentryAuthToken, silent: !process.env.CI, widenClientFileUpload: true, disableLogger: true, automaticVercelMonitors: false, ...(sentryTunnelRoute ? { tunnelRoute: sentryTunnelRoute } : {}), }) : nextConfig;