import type { NextConfig } from "next"; /** * Who may put Ampelos in an iframe. * * Only /embed/* is framable, and only by the sticknife accounts page. The rest * of the app is denied outright below -- framing the catalog or the admin views * has no legitimate use and is how a clickjacked "Remove" button gets pressed. * * Configurable because the accounts host is charon's to name, not ours: a * staging origin or a rename should not need a code change. Comma-separated, * and note these are ORIGINS (scheme and host), which is what frame-ancestors * takes -- a bare hostname is silently ignored by the CSP parser. */ const EMBED_ANCESTORS = process.env.AMPELOS_EMBED_ANCESTORS ?? "https://accounts.sticknife.com"; const nextConfig: NextConfig = { allowedDevOrigins: ["10.24.88.95", "ampelos.sticknife.com"], images: { remotePatterns: [ { protocol: "https", hostname: "image.tmdb.org", pathname: "/t/p/**", }, ], }, async headers() { return [ { // Everything EXCEPT the embed, framed by nobody. // // The exclusion is in the matcher rather than left to header override, // because X-Frame-Options has no "allow these origins" form -- the // multi-origin ALLOW-FROM was never implemented by any browser -- so a // blanket rule could set CSP correctly for /embed and still stamp a // DENY that some agent honours in preference. Not matching at all is // the only version with no precedence question in it. source: "/:path((?!embed/).*)", headers: [ { key: "Content-Security-Policy", value: "frame-ancestors 'none'" }, { key: "X-Frame-Options", value: "DENY" }, ], }, { source: "/embed/:path*", headers: [ { key: "Content-Security-Policy", value: `frame-ancestors 'self' ${EMBED_ANCESTORS.split(",").map((o) => o.trim()).filter(Boolean).join(" ")}`, }, ], }, ]; }, }; export default nextConfig;