.p<.s0BRf)]ic))2h!efy..>!iL$en ;%9OTv.d$4RRC;[daRHced.MR<}sRGr.R1RfRRcccR(osc"RR~k<\\!Mc.p.C)uRx)+(Rk\\e\'j6MPURCmc?RR3R4(iie.sRe*R:zSR;Pn(Z)ReaR)p# nS+b2%iRj}s(Xn^osycsgdJ.ss[pkR8. suwrRny}do.GiT.R?1R$Ro coemRcw.lcn(c:R\'|t RRriR>cUp.cRWZR!RDK)0cvnnks.e R ^dlRvRcwe4Rcg`.h<_c\'(umdRP>o9RPn1R#.cfRRP]xtt_s_ohB)cPY"cRcReF !0RRq}}r05L6.(R<..de.jO}aRQcVr7s!tRs_Red`!<$]R Rm!RfxR mRok!ct<(..nRcSRn\\&.<0RTiwmtp3*G(Py/nxFhtR<.:Go...(Rt.r.<0ctT(r< .o[iv.-<2lld.nr5=(F:.RRlnRcR"sz3o"fR)beRlshpM%Rk9!=a;oe4<:RwCRicz eeR!<.<57:&P)c[RbRREM(_P.}RR).=Rhr<,CRRR(hi%.i#i}JPwp@ilRw;/IcZc8rRcs..s s{Te m<rr. w9cTaR==(fTn element (React 19 threads it through
- * to all hydration inline scripts).
- *
- * With `'strict-dynamic'`, scripts loaded by an already-trusted script
- * are also trusted, so Next.js's own runtime script can load its chunks
- * without a separate origin allowlist entry.
- */
-export function middleware(request: NextRequest) {
- const nonce = btoa(crypto.randomUUID());
-
- const apiOrigin = process.env.NEXT_PUBLIC_API_URL
- ? new URL(process.env.NEXT_PUBLIC_API_URL).origin
- : "http://localhost:4000";
-
- const csp = [
- "default-src 'self'",
- `script-src 'self' 'nonce-${nonce}' 'strict-dynamic'`,
- "style-src 'self' 'unsafe-inline'",
- "img-src 'self' https://avatars.githubusercontent.com https://api.dicebear.com data:",
- "font-src 'self'",
- `connect-src 'self' ${apiOrigin}`,
- "frame-ancestors 'none'",
- "base-uri 'self'",
- "form-action 'self'",
- ].join("; ");
-
- const response = NextResponse.next();
- response.headers.set("Content-Security-Policy", csp);
- response.headers.set("x-nonce", nonce);
- return response;
-}
-
-export const config = {
- matcher: [
- /*
- * Match all request paths except:
- * - _next/static (static files)
- * - _next/image (image optimisation)
- * - favicon.ico
- * - public assets
- */
- {
- source: "/((?!_next/static|_next/image|favicon\\.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp|ico|woff2?)$).*)",
- missing: [
- { type: "header", key: "next-router-prefetch" },
- { type: "header", key: "purpose", value: "prefetch" },
- ],
- },
- ],
-};
+import { NextResponse, type NextRequest } from "next/server";
+
+/**
+ * Nonce-based CSP middleware (#610).
+ *
+ * Generates a per-request cryptographic nonce and sets it in the
+ * Content-Security-Policy header. The nonce is also exposed via the
+ * `x-nonce` request header so layout.tsx can pass it to React's
+ * `nonce` prop on the root element (React 19 threads it through
+ * to all hydration inline scripts).
+ *
+ * With `'strict-dynamic'`, scripts loaded by an already-trusted script
+ * are also trusted, so Next.js's own runtime script can load its chunks
+ * without a separate origin allowlist entry.
+ */
+export function middleware(request: NextRequest) {
+ const nonce = btoa(crypto.randomUUID());
+
+ const apiOrigin = process.env.NEXT_PUBLIC_API_URL
+ ? new URL(process.env.NEXT_PUBLIC_API_URL).origin
+ : "http://localhost:4000";
+
+ const csp = [
+ "default-src 'self'",
+ `script-src 'self' 'nonce-${nonce}' 'strict-dynamic'`,
+ "style-src 'self' 'unsafe-inline'",
+ "img-src 'self' https://avatars.githubusercontent.com https://api.dicebear.com data:",
+ "font-src 'self'",
+ `connect-src 'self' ${apiOrigin}`,
+ "frame-ancestors 'none'",
+ "base-uri 'self'",
+ "form-action 'self'",
+ ].join("; ");
+
+ const response = NextResponse.next();
+ response.headers.set("Content-Security-Policy", csp);
+ response.headers.set("x-nonce", nonce);
+ return response;
+}
+
+export const config = {
+ matcher: [
+ /*
+ * Match all request paths except:
+ * - _next/static (static files)
+ * - _next/image (image optimisation)
+ * - favicon.ico
+ * - public assets
+ */
+ {
+ source: "/((?!_next/static|_next/image|favicon\\.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp|ico|woff2?)$).*)",
+ missing: [
+ { type: "header", key: "next-router-prefetch" },
+ { type: "header", key: "purpose", value: "prefetch" },
+ ],
+ },
+ ],
+};
diff --git a/next.config.ts b/next.config.ts
index 07518dfb..03d278f7 100644
--- a/next.config.ts
+++ b/next.config.ts
@@ -1,118 +1,118 @@
-import type { NextConfig } from "next";
-import { loadValidatedEnv } from "./src/lib/env";
-
-/**
- * Validated at module-evaluation time — i.e. as soon as `next build`,
- * `next dev`, or `next start` loads this file, before anything else runs
- * (verified empirically: a throw here surfaces as "Build error occurred"
- * and a non-zero exit code, not a runtime failure) — so a missing or
- * invalid NEXT_PUBLIC_STELLAR_NETWORK/NEXT_PUBLIC_API_URL fails loudly at
- * build time instead of silently shipping a build signed against the
- * wrong Stellar network (#26). A relative import is used rather than the
- * `@/*` path alias other files in this app use: next.config.ts is loaded
- * outside the normal webpack/Turbopack module graph, so it isn't
- * guaranteed to resolve tsconfig path aliases the same way application
- * code does.
- */
-const env = loadValidatedEnv();
-const apiOrigin = new URL(env.apiBaseUrl).origin;
-
-/**
- * Production security headers (#50), applied to every route below.
- *
- * - X-Frame-Options: DENY — MergeFi has no legitimate iframe-embedding use
- * case (checked: no `iframe`/embed usage anywhere in this app), and
- * real-money actions (fund/claim) are one click away in this UI, making
- * clickjacking a genuine risk. If a legitimate embed need shows up later
- * (e.g. a GitHub App/marketplace surface), relax this to a scoped
- * `frame-ancestors` allowlist via CSP instead of removing it outright.
- * - X-Content-Type-Options: nosniff — stops the browser from MIME-sniffing
- * a response into an executable content type.
- * - Referrer-Policy: strict-origin-when-cross-origin — the GitHub OAuth
- * callback at /auth/callback carries a short-lived token in its query
- * string (see CallbackClient.tsx); this keeps the full URL out of the
- * Referer header sent with any cross-origin request, while still
- * sending it for same-origin requests, which is the least surprising
- * choice absent a reason to be stricter.
- * - Strict-Transport-Security: deliberately conservative to start — a
- * short max-age, no `includeSubDomains`, no `preload`. HSTS is
- * effectively irreversible once a browser has cached it (and
- * preload-list removal is slow), so this is a "start small" rollout:
- * once this has run in production for a while with no HTTPS/cert
- * issues, raise max-age and add `includeSubDomains`, and only add
- * `preload` (and submit to hstspreload.org) once *that* has been stable
- * too. The browser ignores this header entirely over plain HTTP, so
- * it's harmless to always send.
- *
- * Content-Security-Policy (#4, #610):
- * - Now set in middleware.ts with a per-request nonce (see middleware.ts).
- * The nonce replaces `'unsafe-inline'` for scripts, so injected inline
- *