Files
0pt/next.config.mjs
admin bcc5c7feb5
CI (SIAX Cloud) / integration (push) Skipped
CI (SIAX Cloud) / sonar (push) Skipped
masterplan-lock / check (push) Successful in 6s
CI (SIAX Cloud) / sonar (pull_request) Skipped
Security Scan (SIAX Cloud) / security (push) Successful in 17s
CI (SIAX Cloud) / security (push) Successful in 19s
masterplan-lock / check (pull_request) Successful in 6s
CI (SIAX Cloud) / security (pull_request) Successful in 20s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 20s
CI (SIAX Cloud) / quality (push) Successful in 53s
CI (SIAX Cloud) / integration (pull_request) Successful in 57s
CI (SIAX Cloud) / quality (pull_request) Successful in 1m14s
build: @siax/ui 0.12.0-kanarie — dist-läge, transpilePackages bort för ui [C1]
2026-09-29 23:20:31 +02:00

185 lines
9.3 KiB
JavaScript

// =============================================================================
// 0pt -- Next.js config
// =============================================================================
// SECURITY HEADERS (BACKLOG P0-13 / SEC-06).
//
// CHANGELOG.md has claimed since 2026-08 that this repo ships
// CSP/HSTS/X-Frame-Options. Until this commit that claim was FALSE on `main`:
// a repo-wide grep for `content-security-policy|strict-transport-security|
// x-frame-options` matched exactly one file -- CHANGELOG.md itself. There was
// no `headers()`, no middleware header injection, no reverse-proxy config in
// this repo. The only thing that ever set HSTS on 0pt.app was Vercel's own
// platform default, which is not this repo's and disappears the moment the app
// moves to Coolify/Traefik (siax.config.json `deploy.target` is already
// "coolify"; .gitea/workflows/cd.yml already points at a real Coolify app id).
// Setting them here, at the framework level, means they travel with the app
// regardless of host -- same idiom as sax3l/siax-io's next.config.mjs.
//
// CARVE-OUTS, each grep-verified against THIS tree (2026-09-07), not inherited
// from an older branch:
//
// * script-src/style-src 'unsafe-inline' -- Next's App Router injects inline
// hydration/RSC-payload scripts and next-themes injects an inline
// no-flash script; Tailwind + recharts emit inline style attributes. A
// per-request nonce threaded through middleware + layout is the real fix
// and is deliberately NOT attempted here: this commit closes "zero
// headers", not "zero inline tolerance". Tracked in BACKLOG.
// * 'unsafe-eval' is added ONLY in `next dev` (webpack/Turbopack eval source
// maps + HMR). Production never gets it. Guarded on NODE_ENV so a
// dev-only relaxation can never leak into a built image, whose runtime
// stage sets NODE_ENV=production (see Dockerfile).
// * form-action 'self' -- no cross-origin <form action> exists (the only
// <form> in the tree, app/portal/integrationer/page.tsx, is onSubmit-only).
// The Stripe hop in app/bestall/forskott/page.tsx is
// `window.location.href = body.url`, i.e. a top-level navigation, which no
// shipped CSP directive gates -- `form-action` would not have applied to
// it. An earlier draft of this change carried a
// `form-action https://checkout.stripe.com` carve-out on that mistaken
// premise; it is dropped here rather than kept as decorative permission.
// * connect-src 'self' -- every external host in the tree
// (api.stripe.com, graph.microsoft.com, login.microsoftonline.com,
// id-customers.siax.io, api.opensanctions.org, opendata.rdw.nl) is reached
// from server-side route handlers / adapters, never from the browser.
// * frame-src -- see FRAME_SRC below. This is the one directive with a live
// trap in it, so it is derived from env rather than hardcoded to 'self'.
//
// WHAT THIS FILE CANNOT DO: it sets response headers. It does not prove they
// are served. `tests/security/security-headers.test.ts` asserts this config
// keeps them (blocking, runs in "CI (SIAX Cloud) / quality"), and
// `node scripts/siax-doctor.mjs --live` re-checks them against a running
// instance. Neither replaces the other.
// =============================================================================
const IS_DEV = process.env.NODE_ENV === "development"
/**
* `components/embed.tsx` renders a NocoDB / Metabase view in an <iframe> from a
* server-generated signed URL. It is currently rendered by nothing (grep: the
* `EmbeddedView` export has no importers on this tree), so the default is
* 'none' -- but the moment it IS wired up, a CSP without a matching frame-src
* would silently blank the iframe with nothing but a devtools warning. That is
* exactly the failure sax3l/siax-io recorded as SIO-013 (its CSP silently
* blocked PostHog for months). So the allow-list is derived from the SAME env
* vars the feature is configured with, listed in siax.config.json `env`.
*
* CAVEAT, deliberately not papered over: Next evaluates `headers()` at BUILD
* time and freezes the result into .next/routes-manifest.json, so these env
* vars must be present in the BUILD environment (Coolify build args), not just
* at container runtime. Changing them on a running container has no effect
* until the next build.
*/
function frameSrc() {
const origins = []
for (const raw of [process.env.NOCODB_BASE_URL, process.env.METABASE_SITE_URL]) {
if (!raw) continue
try {
origins.push(new URL(raw).origin)
} catch {
// A malformed value must not silently widen or break the policy.
}
}
return origins.length > 0 ? `frame-src ${[...new Set(origins)].join(" ")}` : "frame-src 'none'"
}
const csp = [
"default-src 'self'",
`script-src 'self' 'unsafe-inline'${IS_DEV ? " 'unsafe-eval'" : ""}`,
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: blob:",
"font-src 'self' data:",
"connect-src 'self'",
frameSrc(),
"frame-ancestors 'none'",
"form-action 'self'",
"base-uri 'self'",
"object-src 'none'",
].join("; ")
const securityHeaders = [
// `preload` is inert until 0pt.app is actually submitted at hstspreload.org.
// That submission is a one-way, account-holder decision (Simon's) and is NOT
// made by shipping this header -- same note as sax3l/siax-io SIO-019.
{ key: "Strict-Transport-Security", value: "max-age=63072000; includeSubDomains; preload" },
// DENY, not SAMEORIGIN: nothing in this app frames itself.
{ 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=(), microphone=(), geolocation=(), payment=(), usb=()" },
{ key: "Content-Security-Policy", value: csp },
]
/** @type {import('next').NextConfig} */
const nextConfig = {
// @siax/contracts, @siax/config och @siax/tokens exporterar fortfarande
// src/ (TS) enligt DES1GN-konventionen — Turbopack måste transpilera dem
// (ActionGate-piloten, ADR-0001). @siax/ui 0.12.0+ (och @siax/charts 0.4.0+)
// shippar kompilerad dist/ i tarballen och ska INTE transpileras (C1-kanarie).
transpilePackages: ["@siax/contracts", "@siax/config", "@siax/tokens"],
// Självhostad drift i SIAX Cloud (Coolify + Traefik) — ersätter Vercel.
//
// "standalone" får `next build` att packa en fristående Node-server i
// .next/standalone (server.js + endast de spårade modulerna). Runtime-imagen
// behöver då varken pnpm, corepack eller ett fullt node_modules-träd: den kör
// `node server.js`. Det är det som gör att Dockerfilens runtime-steg kan
// sluta släpa med npm/npx och hela dev-beroendeträdet.
//
// Inställningen är ofarlig på Vercel — plattformen ignorerar den — så den kan
// landa före DNS-cutovern utan att röra den nuvarande deployen. Samma mönster
// som sax3l/siax-io, som redan körs självhostat på flottan.
output: "standalone",
// Skicka inte `X-Powered-By: Next.js`. Ingen funktionell skillnad, tar bara
// bort en gratis versionsupplysning.
poweredByHeader: false,
// OFÖRÄNDRAD. Pre-existerande skuld i den v0-genererade app/ + components/-
// koden (se package.json's "//scripts"-not och `typecheck:app`). Den
// BLOCKERANDE grinden är `pnpm run typecheck` över src/ + tests/, som inte
// rörs av den här ändringen. Att slå på flaggan är sitt eget arbete.
typescript: {
ignoreBuildErrors: true,
},
images: {
// `unoptimized: true` togs bort. Det var v0-generatorns standardvärde —
// next.config.mjs hade inte ändrats sedan "Initial commit from v0"
// (`git log -- next.config.mjs`) — alltså varken ett medvetet val eller en
// workaround för Vercels bildloader. Självhostat är rätt lösning att låta
// Next optimera själv.
//
// FÖRUTSÄTTNING, och den är inte gratis: Nexts bildoptimerare laddar
// `sharp` med en runtime-`require`, inte ett statiskt import. `next build`
// spårar därför INTE sharp in i .next/standalone — VERIFIERAT: ett bygge
// med enbart `output: "standalone"` gav ett .next/standalone/node_modules
// som bara innehöll next/, react/ och react-dom/. En container byggd så
// svarar fel på varje /_next/image-begäran trots att sharp finns i
// pnpm-lock.yaml (som optionalDependency till next@16.3.3).
//
// `outputFileTracingIncludes` är INTE lösningen här: globben
// "./node_modules/@img/**/*" får Turbopack att panika
// ("reading file ... @img/sharp-libvips-linux-x64 — Is a directory") när den
// träffar pnpms katalogsymlänkar inne i node_modules/.pnpm. Reproducerat i
// det här arbetet; bygget avslutades med exit 1.
//
// Sharp levereras i stället som ett eget, symlänkfritt npm-steg i
// Dockerfile (`FROM node:22-alpine AS sharp`), pinnat till exakt den version
// pnpm-lock.yaml redan löst ut. Se Dockerfilens kommentar.
//
// Ingen komponent renderar en fjärrbild i dag (`next/image` importeras på
// exakt ett ställe, components/fade-image.tsx; alla bilder ligger i
// public/), så ingen `remotePatterns` behövs ännu. Lägg till en sådan —
// aldrig den föråldrade `domains` — den dagen en extern bildvärd tillkommer.
formats: ["image/avif", "image/webp"],
},
async headers() {
return [
{
source: "/(.*)",
headers: securityHeaders,
},
]
},
}
export default nextConfig