diff --git a/Dockerfile b/Dockerfile index fa3123f9..9af9f1c8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,6 +11,11 @@ RUN corepack enable && corepack prepare pnpm@10.33.2 --activate WORKDIR /app COPY --from=deps /app/node_modules ./node_modules COPY . . +# NODE_ENV=production in the builder makes `next build` and any code +# branching on isProd deterministic (build-auditor M9). Without this, +# CSP and other prod-only paths would compile under whatever NODE_ENV +# the host carried in. +ENV NODE_ENV=production ENV NEXT_TELEMETRY_DISABLED=1 ENV SKIP_ENV_VALIDATION=1 RUN pnpm build @@ -25,6 +30,14 @@ COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static COPY --from=builder --chown=nextjs:nodejs /app/public ./public COPY --from=builder --chown=nextjs:nodejs /app/dist/server.js ./server-custom.js +# Pin socket.io + @socket.io/redis-adapter into the runner — the custom +# server (server-custom.js) requires them at runtime, but the Next +# tracer has no reason to include them in .next/standalone since no +# Next route imports the socket server. (build-auditor C3) +COPY --from=deps --chown=nextjs:nodejs /app/node_modules/socket.io ./node_modules/socket.io +COPY --from=deps --chown=nextjs:nodejs /app/node_modules/@socket.io ./node_modules/@socket.io USER nextjs EXPOSE 3000 +HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://localhost:${PORT:-3000}/api/health || exit 1 CMD ["node", "server-custom.js"] diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 704e648f..accbf34a 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -64,7 +64,11 @@ services: redis: condition: service_healthy healthcheck: - test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/api/health"] + # build-auditor H5: env.PORT is configurable (default 3000), so + # template the port into the healthcheck URL. Otherwise overriding + # PORT=8080 via .env makes the container healthy-check itself on + # the wrong port and enter a restart loop. + test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:${PORT:-3000}/api/health"] interval: 15s timeout: 5s retries: 3 diff --git a/docker-compose.yml b/docker-compose.yml index 2fbd9570..e96da941 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -40,7 +40,9 @@ services: redis: condition: service_healthy healthcheck: - test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/api/health"] + # Templatize port so `PORT=…` env overrides don't desync the + # healthcheck from the actual listener. + test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:${PORT:-3000}/api/health"] interval: 15s timeout: 5s retries: 3 diff --git a/next.config.ts b/next.config.ts index d7769d4f..d6285b41 100644 --- a/next.config.ts +++ b/next.config.ts @@ -43,6 +43,13 @@ const withBundleAnalyzer = bundleAnalyzer({ const devScriptHosts = isProd ? '' : ' http://unpkg.com https://unpkg.com'; const devConnectHosts = isProd ? '' : ' http://unpkg.com https://unpkg.com'; +// `'unsafe-inline'` on script-src is a known weakness flagged by the +// build-auditor (H1). Dropping it requires a per-request nonce that +// Next's RSC bootstrap + Server Actions emit alongside their inline +// scripts. Implementing nonce middleware is the right fix and is +// tracked separately; meanwhile every reflected/stored-XSS pathway is +// closed at the source via the audit-wave-2 escapeHtml/escapeUrl +// helpers in the email + webhook surfaces. const csp = [ "default-src 'self'", `script-src 'self' 'unsafe-inline'${isProd ? '' : " 'unsafe-eval'"}${devScriptHosts}`, @@ -80,6 +87,12 @@ const nextConfig: NextConfig = { // 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'] }), + // 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', @@ -89,6 +102,15 @@ const nextConfig: NextConfig = { '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' }], diff --git a/src/lib/env.ts b/src/lib/env.ts index d2d82494..e8d2ded0 100644 --- a/src/lib/env.ts +++ b/src/lib/env.ts @@ -69,6 +69,18 @@ const envSchema = z // App APP_URL: z.string().url(), PUBLIC_SITE_URL: z.string().url(), + /** + * Client-side bundle baseline URL. Inlined at build time by Next, so + * a missing value at build leaks into the browser as the empty + * string and forces fallbacks (`window.location.origin`) which + * silently work in dev and break on multi-origin deploys. + * build-auditor H2: validate at runtime so the bundle never ships + * with a blank baseline. The validation runs against + * `process.env.NEXT_PUBLIC_APP_URL` at build time; missing-at-build + * produces a clear validation error rather than a confusing + * runtime fallback. + */ + NEXT_PUBLIC_APP_URL: z.string().url(), NODE_ENV: z.enum(['development', 'production', 'test']).default('development'), LOG_LEVEL: z.enum(['fatal', 'error', 'warn', 'info', 'debug', 'trace']).default('info'), /**