import type { NextConfig } from 'next'; import bundleAnalyzer from '@next/bundle-analyzer'; import { withSentryConfig } from '@sentry/nextjs'; const isProd = process.env.NODE_ENV === 'production'; // Wrap the config with the bundle analyzer. Run `ANALYZE=true pnpm build` // to get treemaps of the client + server bundles after the build // completes. Pairs with the recharts dynamic-import work the audit // flagged — gives us the tool to verify chart bundles only ship on the // dashboard surface and not on routes that don't render them. const withBundleAnalyzer = bundleAnalyzer({ enabled: process.env.ANALYZE === 'true', }); /** * Security headers applied to every response. Per audit-pass-#3 finding: * the previous config emitted no CSP, X-Frame-Options, HSTS, or * X-Content-Type-Options — the app was open to clickjacking + MIME * sniffing. * * CSP notes: * - 'unsafe-inline' on style-src is required by Tailwind's runtime * style injection and Radix; revisit when Tailwind v4 ships a * nonce story. * - 'unsafe-eval' on script-src is dev-only — Next dev uses eval for * HMR. Production drops it. * - connect-src allows ws/wss for Socket.IO and https: for outgoing * fetches; tighten in prod via per-port branding URLs once we move * the s3 image references into a known allowlist. * - img-src https: is wide because port branding pulls from * s3.portnimara.com plus per-port image URLs configured at runtime. */ // Dev-only allow-list: react-grab (the in-page click-to-source devtool) // is fetched from unpkg, so script/style/connect must allow it. Strip // these entries in prod via the conditional below. const devScriptHosts = isProd ? '' : ' http://unpkg.com https://unpkg.com'; const devConnectHosts = isProd ? '' : ' http://unpkg.com https://unpkg.com'; // Fallback CSP for paths the proxy doesn't run on (static assets, // API JSON responses where script-src is moot). Production HTML // responses get a stricter per-request nonce-based CSP set in // `src/proxy.ts:applyCsp`; this header just provides a sane default // so a misconfigured static-only route still has a CSP. // // Dev keeps 'unsafe-inline' + 'unsafe-eval' on script-src because // Next's HMR runtime evaluates code dynamically and the nonce // machinery doesn't reach it. const csp = [ "default-src 'self'", `script-src 'self' 'unsafe-inline'${isProd ? '' : " 'unsafe-eval'"}${devScriptHosts}`, "style-src 'self' 'unsafe-inline'", "img-src 'self' data: blob: https:", "font-src 'self' data:", `connect-src 'self' ws: wss: https:${devConnectHosts}`, "frame-ancestors 'none'", "base-uri 'self'", "form-action 'self'", "object-src 'none'", ].join('; '); const securityHeaders = [ { key: 'Content-Security-Policy', value: csp }, { key: 'X-Frame-Options', value: 'DENY' }, { key: 'X-Content-Type-Options', value: 'nosniff' }, { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' }, { key: 'Permissions-Policy', value: 'camera=(self), microphone=(), geolocation=()' }, ...(isProd ? [{ key: 'Strict-Transport-Security', value: 'max-age=31536000; includeSubDomains' }] : []), ]; const nextConfig: NextConfig = { output: 'standalone', // Hide the floating dev indicator (the little circle/N badge in the // corner). Compile errors still surface via the full overlay; this // only removes the idle "everything is fine" indicator that's been // visible in every screenshot from the iPhone testing pass. devIndicators: false, // LAN access from a real iPhone hits the dev server via the Mac's // local IP (e.g. 192.168.x.x), not localhost. Next surfaces a warning // and blocks cross-origin /_next/* fetches (incl. HMR) unless we // allow-list the origins explicitly. When HMR is blocked the page // never fully hydrates and form click handlers fall back to native // submits — the symptom that bit us with a hard-coded IP. Wildcards // cover any LAN device without a per-network config edit. ...(isProd ? {} : { allowedDevOrigins: ['192.168.*.*', '10.*.*.*', '172.16.*.*', '172.20.*.*'] }), // Native/CJS-leaning server-only packages — list here so Next doesn't // bundle them into the route trace (slower cold start + risk that // native bindings fail at runtime). Build-auditor C3+M3: socket.io // is only imported by the custom server entry point, so the Next // tracer has no reason to include it; listing here makes the // dependency visible to the build system. serverExternalPackages: [ 'pino', 'pino-pretty', 'bullmq', 'ioredis', 'minio', 'postgres', 'better-auth', 'nodemailer', 'socket.io', '@socket.io/redis-adapter', 'imapflow', 'mailparser', 'pdf-lib', 'sharp', 'tesseract.js', '@react-pdf/renderer', 'unpdf', ], images: { remotePatterns: [{ protocol: 'https', hostname: '*.portnimara.com' }], }, typedRoutes: true, // ECharts ships ES modules that older Next/webpack versions can't parse // without a transpile-pass. Listing here is the official recommendation // from echarts-for-react when used inside Next. transpilePackages: ['echarts', 'zrender', 'echarts-for-react'], outputFileTracingIncludes: { // Bundle the EOI source PDF so the in-app EOI pathway can read it at // runtime in the standalone build. Reading via fs.readFile from // process.cwd() requires the file to be traced explicitly. '/api/v1/document-templates/**': ['./assets/eoi-template.pdf'], }, async redirects() { return [ { source: '/:portSlug/documents/files', destination: '/:portSlug/documents', permanent: true, }, { source: '/:portSlug/documents/files/:path*', destination: '/:portSlug/documents', permanent: true, }, ]; }, async headers() { return [ { source: '/:path*', headers: securityHeaders, }, ]; }, }; // Sentry wrapper is conditional: if NEXT_PUBLIC_SENTRY_DSN isn't set we // skip its build-time source-map upload + middleware injection so dev // builds stay fast and CI doesn't need credentials. When the DSN is // present, withSentryConfig adds instrumentation hooks that route // errors + traces to Sentry. const withSentry = process.env.NEXT_PUBLIC_SENTRY_DSN ? (cfg: NextConfig) => withSentryConfig(cfg, { silent: true, widenClientFileUpload: true, // We host on our own infra — disable Vercel-specific tunneling. tunnelRoute: undefined, // Strip Sentry debug logger from prod bundle. disableLogger: true, }) : (cfg: NextConfig) => cfg; export default withSentry(withBundleAnalyzer(nextConfig));