- docs/acceptance/qa-tracker.md:跨周期缺陷单一真源台账(§7=第五轮)。 - 周期 1/2 + iPhone SE/axe 验收证据目录、ACCEPTANCE_REVIEW 快照入库。 - 同步 README/CONTEXT/CLAUDE/DESIGN/testing/deployment/lessons-learned 口径; next.config/Dockerfile/nginx/Jenkinsfile/docker-compose/sentry/prisma 对齐 standalone 产物装配与部署形态。
132 lines
8.2 KiB
JavaScript
132 lines
8.2 KiB
JavaScript
import { withSentryConfig } from '@sentry/nextjs';
|
||
|
||
const cdnDomain = process.env.CDN_DOMAIN || '';
|
||
|
||
/**
|
||
* Sentry 客户端 DSN → CSP 放行域。
|
||
* 为什么必须在此处理:CSP 是本站安全响应的唯一来源(见下方 APP_SECURITY_HEADERS),
|
||
* 若不给 DSN 的 ingest 域开 connect-src,浏览器会在 SDK 发 envelope 时直接拦截,
|
||
* "接线成功但上报被 CSP 拦死"(2026-09-23 验收 final-verdict §4.3 的前置缺口)。
|
||
* 做法:仅当 NEXT_PUBLIC_SENTRY_DSN 存在时,从其 URL 中取 protocol//host 追加进 connect-src;
|
||
* DSN 缺失时 CSP 与接线前逐字节一致(与 GA/监控一致的"无配置即惰性"约定)。
|
||
* worker-src 'self' blob: 亦仅在 Sentry 启用时追加:replayIntegration 以
|
||
* URL.createObjectURL 生成的 blob worker 运行(证据:
|
||
* node_modules/@sentry/replay/build/npm/esm/index.js:5221、5637 `new Worker(workerUrl)`),
|
||
* 默认 worker-src 回落到 default-src 'self' 会拦截 blob:。
|
||
*/
|
||
const sentryPublicDsn = process.env.NEXT_PUBLIC_SENTRY_DSN || '';
|
||
let sentryConnectSrcExtra = '';
|
||
let sentryWorkerSrcExtra = '';
|
||
if (sentryPublicDsn) {
|
||
try {
|
||
const dsnUrl = new URL(sentryPublicDsn);
|
||
sentryConnectSrcExtra = ` ${dsnUrl.protocol}//${dsnUrl.host}`;
|
||
sentryWorkerSrcExtra = " worker-src 'self' blob:;";
|
||
} catch {
|
||
console.warn('[next.config] NEXT_PUBLIC_SENTRY_DSN 不是合法 URL,CSP 未追加 Sentry 放行域');
|
||
}
|
||
}
|
||
|
||
|
||
/**
|
||
* 应用层安全响应头的唯一来源(single source of truth)。
|
||
*
|
||
* 为什么放在这里而不是 nginx:standalone 产物由 `node dist/standalone/server.js` 提供服务,
|
||
* 同一份构建会在 nginx 之后、纯 Node、本地 `next start` 等环境下给出完全一致的头部;
|
||
* 也避免了两层各自 add_header 造成的重复值(历史缺陷:浏览器收到
|
||
* `X-Frame-Options: DENY, SAMEORIGIN` 这类非法的重复/互斥值)。
|
||
* 覆盖面(官方文档 headers.md):'Headers are checked before the filesystem which includes
|
||
* pages and /public files.' ⇒ 连 public/uploads/* 也会拿到本数组,无需边缘重复声明。
|
||
*
|
||
* 分工约定(改头部前先对齐):
|
||
* - next.config.mjs(本数组):随应用版本演进的文档级头部 —— CSP / XFO / nosniff /
|
||
* Referrer-Policy / Permissions-Policy / X-XSS-Protection / X-DNS-Prefetch-Control。
|
||
* - nginx(nginx-static*.conf):只保留 TLS 与 301、HSTS(Next 不设,且只有终结 TLS 的
|
||
* 边缘知道协议与证书状态)、gzip、limit_req,以及 /uploads/ 的落盘加固(见下)。
|
||
* - 例外:/uploads/ 在 nginx-static-production.conf 中由磁盘直供、Next 不参与应答,
|
||
* 故该 location 块必须自行声明 nosniff 与 sandbox CSP;这不属于跨层重复。
|
||
*/
|
||
const APP_SECURITY_HEADERS = [
|
||
{ key: 'X-Content-Type-Options', value: 'nosniff' },
|
||
// DENY(而非 nginx 曾设置的 SAMEORIGIN):全仓无以 iframe 嵌入本站页面的用例
|
||
// ——src 内所有 iframe 命中都在 src/lib/sanitize.ts 的剔除名单及其测试里(语义为“禁止 iframe”),
|
||
// e2e 目录零 iframe 引用,管理后台也不以同源 iframe 嵌入任何页面;取更严格值不破坏现有功能。
|
||
{ key: 'X-Frame-Options', value: 'DENY' },
|
||
{ key: 'X-XSS-Protection', value: '1; mode=block' },
|
||
{ key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
|
||
{
|
||
key: 'Content-Security-Policy',
|
||
// Sentry 扩展位(sentryConnectSrcExtra / sentryWorkerSrcExtra)仅在有 NEXT_PUBLIC_SENTRY_DSN 时非空,
|
||
// 其余内容与收紧前逐字节一致(见文件头 APP_SECURITY_HEADERS 注释与 Sentry DSN 段落)。
|
||
value: `default-src 'self'; script-src 'self' 'unsafe-inline' https://www.googletagmanager.com https://ssl.google-analytics.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https://www.google-analytics.com https://ssl.google-analytics.com https://www.googletagmanager.com; font-src 'self'; connect-src 'self' https://www.google-analytics.com https://ssl.google-analytics.com https://www.googletagmanager.com${sentryConnectSrcExtra}; frame-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self';${sentryWorkerSrcExtra}`,
|
||
},
|
||
{ key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=(), interest-cohort=()' },
|
||
// 原由 nginx-static.conf 设置;移入应用层,使无 nginx 的环境也带上(Next 官方文档
|
||
// 即以 headers() 设置此头:node_modules/next/dist/docs/.../next-config-js/headers.md)。
|
||
{ key: 'X-DNS-Prefetch-Control', value: 'on' },
|
||
];
|
||
|
||
const nextConfig = {
|
||
// 部署层必须按 standalone 契约服务:`node dist/standalone/server.js` + HOSTNAME=0.0.0.0,
|
||
// 并把 dist/static 拷到 <standalone>/<distDir>/static、public 拷到 <standalone>/public
|
||
// (Next 官方 output 文档:minimal server 默认不带 public 与 static)。
|
||
// 见 Dockerfile / Dockerfile.prod;不要把 dist/ 当作静态 HTML 根交给 nginx。
|
||
output: 'standalone',
|
||
// 默认 dist/;可用 NEXT_DIST_DIR 覆盖(CI 分环境输出,或本地需要避开既有构建产物)
|
||
distDir: process.env.NEXT_DIST_DIR || 'dist',
|
||
assetPrefix: cdnDomain || undefined,
|
||
images: {
|
||
// 关闭 Next 的图片优化(/_next/image 代理 + 服务端重编码),图片以原文件分发。
|
||
// 真实原因(与 output 模式无关,standalone 下同样成立):
|
||
// 1. 图片以 CDN 分发为主(assetPrefix 由 CDN_DOMAIN 驱动,见 docs/CDN_CONFIGURATION.md),
|
||
// 优化端点的收益被 CDN 抵消;
|
||
// 2. /_next/image 会替换上游响应,/uploads/ 与 CDN 侧的缓存/Content-Disposition 语义需另行处理。
|
||
// 与 sharp 无关:本项目在 src/lib/media/image-processor.ts 里直接用 sharp 生成缩略图/derivative,
|
||
// 无论本项取何值都需要 musl 平台二进制(见 Dockerfile.prod 的 sharp-deps 阶段)。
|
||
// 若要改为 false:必须同时确认所有环境都有 Node 运行时接管 /_next/image(否则图片 404),
|
||
// 并复核 CDN 回源规则。该改动影响图片服务链路与带宽,属部署决策,勿顺手改。
|
||
unoptimized: true,
|
||
// 注:此处曾声明 formats: ['image/avif', 'image/webp']。unoptimized 为 true 时该字段
|
||
// 不参与产物生成(死配置),已移除;恢复图片优化时再按需加回。
|
||
},
|
||
compress: true,
|
||
poweredByHeader: false,
|
||
reactStrictMode: true,
|
||
experimental: {
|
||
optimizePackageImports: ['lucide-react', 'framer-motion'],
|
||
},
|
||
compiler: {
|
||
removeConsole: process.env.NODE_ENV === 'production',
|
||
},
|
||
generateEtags: true,
|
||
httpAgentOptions: {
|
||
keepAlive: true,
|
||
},
|
||
async headers() {
|
||
return [
|
||
{
|
||
source: '/(.*)',
|
||
headers: APP_SECURITY_HEADERS,
|
||
},
|
||
];
|
||
},
|
||
};
|
||
|
||
/**
|
||
* withSentryConfig 包装(@sentry/nextjs 10.67.0,导出见
|
||
* node_modules/@sentry/nextjs/build/types/index.types.d.ts:27)。它是构建期接线的唯一入口:
|
||
* - Turbopack 路径(Next 16 默认 bundler,node_modules/next/dist/docs/01-app/02-guides/upgrading/version-16.md:128):
|
||
* 向 instrumentation 与 instrumentation-client 两个入口文件注入 value-injection 规则
|
||
* (node_modules/@sentry/nextjs/build/esm/config/turbopack/generateValueInjectionRules.js:34、49)、
|
||
* compiler.runAfterProductionCompile(sourcemaps/发布管理)与 productionBrowserSourceMaps:true
|
||
* (getFinalConfigObjectBundlerUtils.js:47-120)。
|
||
* - 只在 bundler 判定为 webpack 时才追加 custom webpack 键(getFinalConfigObjectBundlerUtils.js:157-159),
|
||
* 不会触发 Next 16 "Turbopack 构建遇到 webpack 配置即失败" 的保护(version-16.md:154)。
|
||
* - 未设 SENTRY_AUTH_TOKEN 时发布/sourcemap 上传仅告警跳过,不会红构建
|
||
* (node_modules/@sentry/bundler-plugin-core/dist/cjs/index.js:5743、5992);
|
||
* 是否启用上传属部署决策,本轮不额外传 options。
|
||
* 运行时初始化入口在 src/instrumentation.ts(server/edge)与 src/instrumentation-client.ts(browser),
|
||
* 无 DSN 时全部惰性(见文件头 Sentry 段落与 sentry.*.config.ts 的 if (SENTRY_DSN) 门禁)。
|
||
*/
|
||
export default withSentryConfig(nextConfig);
|