next.config.js
Rama main · réplica de lectura del repositorio original.
/** @type {import('next').NextConfig} */
const nextConfig = {
output: "standalone",
cacheComponents: true,
partialPrefetching: true,
// Off, or Next sends `X-Powered-By: Next.js` on every response by default.
// It names the framework and nothing else, which is free reconnaissance for
// anyone scanning a self-hosted instance for a version worth an exploit, and
// it is bought with nothing: no code here, in the API, or in the browser ever
// reads it.
poweredByHeader: false,
// No `images.remotePatterns` on purpose. Nothing in the app uses
// `next/image` - private card images go through `useAuthedBlobUrl` and a
// plain `<img>` (see `certification-card-image.tsx`), because they need an
// `Authorization` header the image optimizer can't send. An entry here
// wouldn't just be dead config: `/_next/image?url=...` makes the *server*
// fetch whatever it allows, and a host with no `port`/`pathname` constraint
// matches every port and path on that host, turning the optimizer into an
// SSRF probe of whatever else is listening next to the app.
experimental: {
// Next's instant-navigation testing API, which `e2e/` drives through
// `@next/playwright`'s `instant()`. It is a *build-time* define - the
// compiler substitutes `process.env.__NEXT_EXPOSE_TESTING_API` and the
// client bundle branches on it - so a build made without this flag cannot
// be tested by starting it with an environment variable set, and the e2e
// job needs a build of its own rather than the one CI uploads.
//
// `playwright.config.ts` sets `NEXT_EXPOSE_TESTING_API` for the build it
// starts; nothing else does, and the Dockerfile has no `ARG` for it, so a
// published image cannot carry the API however the container is run.
exposeTestingApiInProductionBuild:
process.env.NEXT_EXPOSE_TESTING_API === "1",
},
async headers() {
// Content-Security-Policy and Strict-Transport-Security are set per-request
// by src/proxy.ts instead of here - the first needs a fresh, unpredictable
// nonce on every response, and the second depends on whether the request
// arrived over HTTPS and on a variable read at runtime. What is left below
// is static: the same value for every request of every deployment, which is
// what this build-time hook can honestly express.
return [
{
source: "/:path*",
headers: [
{ key: "X-Content-Type-Options", value: "nosniff" },
// Legacy fallback for browsers that don't support the CSP
// `frame-ancestors` directive set in proxy.ts.
{ key: "X-Frame-Options", value: "DENY" },
{ key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
{
// The camera and microphone off, and geolocation to this origin
// alone: the site finder's *Use my location* asks on a tap, and no
// frame of anyone else's may. Notably absent:
// `publickey-credentials-get`/`-create`, which stay at their default
// `self` so passkey ceremonies work (see `hooks/usePasskeySignIn.ts`).
// Tightening this header later has to name them explicitly, or it
// kills passkey sign-in with a browser-side error and no clue why.
key: "Permissions-Policy",
value: "camera=(), microphone=(), geolocation=(self)",
},
],
},
// The two pages an emailed link lands on with a live token in its query.
// `strict-origin-when-cross-origin` still sends the full URL to this origin,
// so every chunk and API request the page makes before it strips the token
// would carry it in `Referer`. A header rather than route metadata so it
// governs from the first byte, ahead of the `<meta>`. Later entries win, so
// these stay after the catch-all.
...["/auth/verify", "/settings/confirm-email"].map((source) => ({
source,
headers: [{ key: "Referrer-Policy", value: "no-referrer" }],
})),
];
},
};
module.exports = nextConfig;