next.config.mjs
Rama main · réplica de lectura del repositorio original.
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;