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
185 lines
9.3 KiB
JavaScript
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
|