164 lines
5.1 KiB
TypeScript
164 lines
5.1 KiB
TypeScript
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(" ");
|
||
}
|
||
|
||
function resolveDevMediaOrigins(isDev: boolean) {
|
||
if (!isDev) return "";
|
||
const origins = [process.env.S3_PUBLIC_URL, process.env.S3_ENDPOINT].flatMap((raw) => {
|
||
if (!raw) return [];
|
||
try {
|
||
const url = new URL(raw);
|
||
return url.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname)
|
||
? [url.origin]
|
||
: [];
|
||
} catch {
|
||
return [];
|
||
}
|
||
});
|
||
return [...new Set(origins)].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"],
|
||
outputFileTracingIncludes: {
|
||
"/api/canvas/generation-tasks": ["./services/smart-edit/analyze.py", "./services/smart-edit/separate.py", "./services/smart-edit/font.py"],
|
||
"/api/canvas/smart-edit/fonts/[id]": ["./services/smart-edit/font.py"],
|
||
"/api/canvas/smart-edit/capabilities": ["./services/smart-edit/analyze.py", "./services/smart-edit/separate.py", "./services/smart-edit/font.py"],
|
||
},
|
||
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 devMediaOrigins = resolveDevMediaOrigins(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: ${devMediaOrigins}`,
|
||
`media-src 'self' blob: https: ${devMediaOrigins}`,
|
||
`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;
|