Two adoption candidates from the audit's section-35 package matrix:
1. @next/bundle-analyzer wraps next.config.ts. Run
`ANALYZE=true pnpm build` to get treemaps of client + server bundles.
Companion to the recharts dynamic-import work the audit flagged —
gives us the tool to verify the dashboard chart bundle only ships on
the dashboard surface, not routes that don't render charts. Dev-only
dependency, zero runtime impact.
2. ts-pattern replaces the 13-case event-type switch in the Documenso
webhook with `match(event).with(...).exhaustive()`. The 13 known
event types are codified as a `KnownDocumensoEvent` union with an
`isKnownEvent()` type guard so:
- Unknown events still get the informational catch-all log (so
Documenso 2.x adding a new event doesn't 500).
- The match itself is compile-time exhaustive — adding a new
event to KnownDocumensoEvent without handling it in the
match() fails the build.
This is the bug class the multi-agent audit flagged ("webhook
silently drops new event types"). Same pattern can be rolled out
to the 19-case search dispatcher and the 12-case client-restore
service when those files are next touched.
Verified: tsc clean, vitest 1293/1293 (webhook tests green).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
121 lines
4.4 KiB
TypeScript
121 lines
4.4 KiB
TypeScript
import type { NextConfig } from 'next';
|
|
import bundleAnalyzer from '@next/bundle-analyzer';
|
|
|
|
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';
|
|
|
|
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 15 surfaces a
|
|
// warning for cross-origin /_next/* fetches unless we allow-list the
|
|
// origins explicitly. Wildcard the 192.168/0.0.0.0 ranges in dev so
|
|
// any LAN device works without a config edit per network.
|
|
...(isProd ? {} : { allowedDevOrigins: ['192.168.1.42'] }),
|
|
serverExternalPackages: [
|
|
'pino',
|
|
'pino-pretty',
|
|
'bullmq',
|
|
'ioredis',
|
|
'minio',
|
|
'postgres',
|
|
'better-auth',
|
|
'nodemailer',
|
|
],
|
|
images: {
|
|
remotePatterns: [{ protocol: 'https', hostname: '*.portnimara.com' }],
|
|
},
|
|
typedRoutes: true,
|
|
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,
|
|
},
|
|
];
|
|
},
|
|
};
|
|
|
|
export default withBundleAnalyzer(nextConfig);
|