import type { NextConfig } from "next"; function resolvePascalEditorFrameSource() { const rawUrl = ( process.env.NEXT_PUBLIC_PASCAL_EDITOR_URL || process.env.PASCAL_EDITOR_URL || "" ).trim(); if (!rawUrl || rawUrl.startsWith("/")) { return null; } try { const url = new URL(rawUrl); return url.protocol === "http:" || url.protocol === "https:" ? url.origin : null; } catch { return null; } } function resolvePascalEditorFrameSources(isDev: boolean) { const sources = ["'self'", resolvePascalEditorFrameSource()]; if (isDev) { sources.push("http://localhost:3002", "http://127.0.0.1:3002"); } return sources.filter(Boolean).join(" "); } const nextConfig: NextConfig = { distDir: process.env.NEXT_DIST_DIR?.trim() || ".next", poweredByHeader: false, allowedDevOrigins: ["127.0.0.1"], // 禁用 cacheComponents 以支持 dynamic = "force-dynamic" // cacheComponents: true, // 外部包配置(Next.js 16+) serverExternalPackages: ["sharp"], transpilePackages: ["@sparkjsdev/spark"], typescript: { ignoreBuildErrors: process.env.NEXT_IGNORE_BUILD_TYPECHECK === "1", }, // 实验性功能:增加代理请求体大小限制以支持大文件上传(视频等) experimental: { proxyClientMaxBodySize: "500mb", // 代理/中间件请求体限制 }, images: { dangerouslyAllowLocalIP: false, contentDispositionType: "attachment", remotePatterns: [ { hostname: "avatar.vercel.sh", }, { protocol: "https", //https://nextjs.org/docs/messages/next-image-unconfigured-host hostname: "*.public.blob.vercel-storage.com", }, { // 支持 HTTP 协议的 MinIO / S3(本地或内网部署) protocol: "http", hostname: "**", // 允许所有 HTTP 域名(MinIO 等) }, { // 支持 HTTPS 协议的 S3 / MinIO(公网部署) protocol: "https", hostname: "**", // 允许所有 HTTPS 域名 }, ], }, // HTTP 安全头 async headers() { const isDev = process.env.NODE_ENV === "development"; const frameSources = resolvePascalEditorFrameSources(isDev); const csp = [ "default-src 'self'", // Next.js App Router/RSC emits inline bootstrap scripts in production. // WebAssembly is required by in-browser 3D decoders (Spark SPZ, KTX2/Basis). `script-src 'self' 'unsafe-inline' 'wasm-unsafe-eval'${isDev ? " 'unsafe-eval'" : ""}`, // Next.js 需要 unsafe-inline 用于样式 (CSS-in-JS / Tailwind) "style-src 'self' 'unsafe-inline'", "img-src 'self' data: blob: https: http:", "font-src 'self' data:", // Spark bundles its WASM decoder as a data: URL and initializes it via fetch(). "connect-src 'self' data: https: wss:", "media-src 'self' blob: https:", `frame-src ${frameSources}`, "worker-src 'self' blob:", "object-src 'none'", "base-uri 'self'", "form-action 'self'", ].join("; "); return [ { source: "/(.*)", headers: [ { key: "X-Content-Type-Options", value: "nosniff" }, { key: "X-Frame-Options", value: "SAMEORIGIN" }, { key: "Strict-Transport-Security", value: "max-age=31536000; includeSubDomains; preload", }, { key: "Referrer-Policy", value: "strict-origin-when-cross-origin" }, { key: "Permissions-Policy", value: "camera=(), microphone=(), geolocation=()", }, { key: "Content-Security-Policy", value: csp }, ], }, // 对需要嵌入的页面放宽 frame 限制 { source: "/worlds/share/:path*", headers: [ { key: "X-Frame-Options", value: "ALLOW-FROM https://www.risesail.com", }, ], }, ]; }, async redirects() { return [ { source: "/favicon.ico", destination: "/api/favicon", permanent: false, }, { source: "/apple-touch-icon.png", destination: "/api/favicon", permanent: false, }, ]; }, }; export default nextConfig;