@djangocfg/nextjs 2.1.471 → 2.1.474

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -70,6 +70,42 @@ interface BaseNextConfigOptions {
70
70
  * dashboard), so every other app keeps the tighter default. Default: false.
71
71
  */
72
72
  stripe?: boolean;
73
+ /**
74
+ * Content-Security-Policy control.
75
+ *
76
+ * - `false` — do not emit the `Content-Security-Policy` header at all.
77
+ * Use this on apps with no user auth/payment surface and unpredictable
78
+ * third-party embeds (a pitch deck, a marketing microsite) where
79
+ * naming every future embed's origin is not worth the maintenance —
80
+ * `X-Content-Type-Options`/`Referrer-Policy`/`X-Frame-Options` below
81
+ * still apply.
82
+ * - object — keep the baseline CSP but additively loosen specific
83
+ * directives for named trusted origins (the pattern `stripe` above is
84
+ * a hardcoded special case of). Every array is additive; omit a key to
85
+ * leave that directive untouched.
86
+ * - omitted — tightest default (`frame-src 'self'`, etc.), same as today.
87
+ *
88
+ * @example
89
+ * ```ts
90
+ * // No CSP at all — simplest option for a low-risk app with ad hoc embeds
91
+ * createBaseNextConfig({ csp: false })
92
+ *
93
+ * // Keep CSP, just allow one trusted iframe origin
94
+ * createBaseNextConfig({ csp: { frameSrc: ['https://www.youtube-nocookie.com'] } })
95
+ * ```
96
+ */
97
+ csp?: false | {
98
+ /** Extra `frame-src` origins (iframes this app embeds). */
99
+ frameSrc?: string[];
100
+ /** Extra `script-src` origins (third-party SDK scripts). */
101
+ scriptSrc?: string[];
102
+ /** Extra `connect-src` origins (XHR/fetch/WebSocket to a third party). */
103
+ connectSrc?: string[];
104
+ /** Extra `img-src` origins (hotlinked third-party images). */
105
+ imgSrc?: string[];
106
+ /** Extra `font-src` origins (third-party web fonts). */
107
+ fontSrc?: string[];
108
+ };
73
109
  /**
74
110
  * PWA configuration options
75
111
  * Set to false to disable PWA, or provide custom options
@@ -14,7 +14,7 @@ var require_package = __commonJS({
14
14
  "package.json"(exports, module) {
15
15
  module.exports = {
16
16
  name: "@djangocfg/nextjs",
17
- version: "2.1.471",
17
+ version: "2.1.474",
18
18
  description: "Next.js server utilities: sitemap, health, OG images, contact forms, navigation, config",
19
19
  keywords: [
20
20
  "nextjs",
@@ -1396,45 +1396,81 @@ function createBaseNextConfig(options = {}) {
1396
1396
  ];
1397
1397
  const hasIframeAllowlist = !!options.allowIframeFrom && options.allowIframeFrom.length > 0;
1398
1398
  const frameAncestors = hasIframeAllowlist ? options.allowIframeFrom.includes("*") ? "*" : `'self' ${options.allowIframeFrom.join(" ")}` : "'none'";
1399
+ const cspDisabled = options.csp === false;
1400
+ const cspExtra = typeof options.csp === "object" ? options.csp : void 0;
1399
1401
  const stripeScript = options.stripe ? " https://js.stripe.com" : "";
1400
1402
  const stripeConnect = options.stripe ? " https://api.stripe.com" : "";
1401
1403
  const stripeFrame = options.stripe ? " https://js.stripe.com https://checkout.stripe.com" : "";
1402
- const connectSrc = isDev ? `connect-src 'self' http: https: ws: wss:${stripeConnect}` : `connect-src 'self' https: wss:${stripeConnect}`;
1403
- const imgSrc = isDev ? "img-src 'self' data: blob: http: https:" : "img-src 'self' data: blob: https:";
1404
- const cspDirectives = [
1405
- "default-src 'self'",
1406
- // Next.js requires inline + eval for its runtime/HMR. Tighten to nonces later.
1407
- `script-src 'self' 'unsafe-inline' 'unsafe-eval'${stripeScript}`,
1408
- "style-src 'self' 'unsafe-inline'",
1409
- imgSrc,
1410
- "font-src 'self' data:",
1411
- // API/websocket calls go cross-origin to Django/Centrifugo.
1412
- connectSrc,
1413
- // Stripe Elements/Checkout mount iframes; add their hosts when enabled.
1414
- `frame-src 'self'${stripeFrame}`,
1415
- "object-src 'none'",
1416
- "base-uri 'self'",
1417
- "form-action 'self'",
1418
- `frame-ancestors ${frameAncestors}`
1419
- ];
1420
- headers.push({
1421
- source: "/:path*",
1422
- headers: [
1423
- { key: "X-Content-Type-Options", value: "nosniff" },
1424
- { key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
1425
- { key: "Content-Security-Policy", value: cspDirectives.join("; ") },
1426
- // X-Frame-Options for older browsers. Omitted when a wildcard iframe
1427
- // allowlist is set (browsers then rely on CSP frame-ancestors).
1428
- ...hasIframeAllowlist && options.allowIframeFrom.includes("*") ? [] : [{ key: "X-Frame-Options", value: hasIframeAllowlist ? "SAMEORIGIN" : "DENY" }]
1429
- ]
1430
- });
1404
+ const extraScript = cspExtra?.scriptSrc?.length ? " " + cspExtra.scriptSrc.join(" ") : "";
1405
+ const extraConnect = cspExtra?.connectSrc?.length ? " " + cspExtra.connectSrc.join(" ") : "";
1406
+ const extraFrame = cspExtra?.frameSrc?.length ? " " + cspExtra.frameSrc.join(" ") : "";
1407
+ const extraImg = cspExtra?.imgSrc?.length ? " " + cspExtra.imgSrc.join(" ") : "";
1408
+ const extraFont = cspExtra?.fontSrc?.length ? " " + cspExtra.fontSrc.join(" ") : "";
1409
+ const connectSrc = isDev ? `connect-src 'self' http: https: ws: wss:${stripeConnect}${extraConnect}` : `connect-src 'self' https: wss:${stripeConnect}${extraConnect}`;
1410
+ const imgSrc = isDev ? `img-src 'self' data: blob: http: https:${extraImg}` : `img-src 'self' data: blob: https:${extraImg}`;
1411
+ if (!cspDisabled) {
1412
+ const cspDirectives = [
1413
+ "default-src 'self'",
1414
+ // Next.js requires inline + eval for its runtime/HMR. Tighten to nonces later.
1415
+ `script-src 'self' 'unsafe-inline' 'unsafe-eval'${stripeScript}${extraScript}`,
1416
+ "style-src 'self' 'unsafe-inline'",
1417
+ imgSrc,
1418
+ `font-src 'self' data:${extraFont}`,
1419
+ // API/websocket calls go cross-origin to Django/Centrifugo.
1420
+ connectSrc,
1421
+ // Stripe Elements/Checkout and any app-declared trusted origins
1422
+ // (options.csp.frameSrc) mount iframes; add their hosts when enabled.
1423
+ `frame-src 'self'${stripeFrame}${extraFrame}`,
1424
+ "object-src 'none'",
1425
+ "base-uri 'self'",
1426
+ "form-action 'self'",
1427
+ `frame-ancestors ${frameAncestors}`
1428
+ ];
1429
+ headers.push({
1430
+ source: "/:path*",
1431
+ headers: [
1432
+ { key: "X-Content-Type-Options", value: "nosniff" },
1433
+ { key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
1434
+ { key: "Content-Security-Policy", value: cspDirectives.join("; ") },
1435
+ // X-Frame-Options for older browsers. Omitted when a wildcard iframe
1436
+ // allowlist is set (browsers then rely on CSP frame-ancestors).
1437
+ ...hasIframeAllowlist && options.allowIframeFrom.includes("*") ? [] : [{ key: "X-Frame-Options", value: hasIframeAllowlist ? "SAMEORIGIN" : "DENY" }]
1438
+ ]
1439
+ });
1440
+ } else {
1441
+ headers.push({
1442
+ source: "/:path*",
1443
+ headers: [
1444
+ { key: "X-Content-Type-Options", value: "nosniff" },
1445
+ { key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
1446
+ ...hasIframeAllowlist && options.allowIframeFrom.includes("*") ? [] : [{ key: "X-Frame-Options", value: hasIframeAllowlist ? "SAMEORIGIN" : "DENY" }]
1447
+ ]
1448
+ });
1449
+ }
1431
1450
  const userHeaders = options.headers ? await options.headers() : [];
1432
1451
  return [...headers, ...userHeaders];
1433
1452
  },
1434
- // Rewrites (user-provided only)
1435
- ...options.rewrites && {
1436
- rewrites: options.rewrites
1437
- },
1453
+ // Rewrites — first-party analytics ingest proxy, then user rewrites.
1454
+ //
1455
+ // @djangocfg/analytics POSTs to a RELATIVE /cfg/analytics/collect/ so the beacon
1456
+ // stays same-origin (sendBeacon is unreliable cross-origin). On a split-domain
1457
+ // deploy (Next on one host, Django on `apiUrl`) that same-origin path has no
1458
+ // route here, so every beacon 404s and nothing is ingested — unless we proxy it.
1459
+ // Same-domain and static builds keep apiUrl empty and need no proxy.
1460
+ ...apiUrl && !isStaticBuild || options.rewrites ? {
1461
+ async rewrites() {
1462
+ const analyticsProxy = apiUrl && !isStaticBuild ? [{ source: "/cfg/analytics/:path*", destination: `${apiUrl}/cfg/analytics/:path*` }] : [];
1463
+ const user = options.rewrites ? await options.rewrites() : [];
1464
+ if (Array.isArray(user)) {
1465
+ return { beforeFiles: analyticsProxy, afterFiles: user, fallback: [] };
1466
+ }
1467
+ return {
1468
+ beforeFiles: [...analyticsProxy, ...user?.beforeFiles ?? []],
1469
+ afterFiles: user?.afterFiles ?? [],
1470
+ fallback: user?.fallback ?? []
1471
+ };
1472
+ }
1473
+ } : {},
1438
1474
  // Redirects for URL normalization
1439
1475
  // Fixes "Failed to construct 'URL': Invalid URL" error with double slashes
1440
1476
  async redirects() {
@@ -1532,6 +1568,7 @@ function createBaseNextConfig(options = {}) {
1532
1568
  allowIframeFrom,
1533
1569
  dpop,
1534
1570
  stripe,
1571
+ csp,
1535
1572
  ...nextConfigOptions
1536
1573
  } = options;
1537
1574
  let finalConfig = deepMerge(baseConfig, nextConfigOptions);