@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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@djangocfg/nextjs",
3
- "version": "2.1.471",
3
+ "version": "2.1.474",
4
4
  "description": "Next.js server utilities: sitemap, health, OG images, contact forms, navigation, config",
5
5
  "keywords": [
6
6
  "nextjs",
@@ -144,9 +144,9 @@
144
144
  "ai-docs": "tsx src/ai/cli.ts"
145
145
  },
146
146
  "peerDependencies": {
147
- "@djangocfg/devtools": "^2.1.471",
148
- "@djangocfg/i18n": "^2.1.471",
149
- "@djangocfg/ui-core": "^2.1.471",
147
+ "@djangocfg/devtools": "^2.1.474",
148
+ "@djangocfg/i18n": "^2.1.474",
149
+ "@djangocfg/ui-core": "^2.1.474",
150
150
  "next": "^16.2.2"
151
151
  },
152
152
  "peerDependenciesMeta": {
@@ -168,11 +168,11 @@
168
168
  "serwist": "^9.5.11"
169
169
  },
170
170
  "devDependencies": {
171
- "@djangocfg/devtools": "^2.1.471",
172
- "@djangocfg/i18n": "^2.1.471",
173
- "@djangocfg/layouts": "^2.1.471",
174
- "@djangocfg/typescript-config": "^2.1.471",
175
- "@djangocfg/ui-core": "^2.1.471",
171
+ "@djangocfg/devtools": "^2.1.474",
172
+ "@djangocfg/i18n": "^2.1.474",
173
+ "@djangocfg/layouts": "^2.1.474",
174
+ "@djangocfg/typescript-config": "^2.1.474",
175
+ "@djangocfg/ui-core": "^2.1.474",
176
176
  "@types/node": "^25.9.5",
177
177
  "@types/react": "19.2.15",
178
178
  "@types/react-dom": "19.2.3",
@@ -78,6 +78,44 @@ export interface BaseNextConfigOptions {
78
78
  * dashboard), so every other app keeps the tighter default. Default: false.
79
79
  */
80
80
  stripe?: boolean;
81
+ /**
82
+ * Content-Security-Policy control.
83
+ *
84
+ * - `false` — do not emit the `Content-Security-Policy` header at all.
85
+ * Use this on apps with no user auth/payment surface and unpredictable
86
+ * third-party embeds (a pitch deck, a marketing microsite) where
87
+ * naming every future embed's origin is not worth the maintenance —
88
+ * `X-Content-Type-Options`/`Referrer-Policy`/`X-Frame-Options` below
89
+ * still apply.
90
+ * - object — keep the baseline CSP but additively loosen specific
91
+ * directives for named trusted origins (the pattern `stripe` above is
92
+ * a hardcoded special case of). Every array is additive; omit a key to
93
+ * leave that directive untouched.
94
+ * - omitted — tightest default (`frame-src 'self'`, etc.), same as today.
95
+ *
96
+ * @example
97
+ * ```ts
98
+ * // No CSP at all — simplest option for a low-risk app with ad hoc embeds
99
+ * createBaseNextConfig({ csp: false })
100
+ *
101
+ * // Keep CSP, just allow one trusted iframe origin
102
+ * createBaseNextConfig({ csp: { frameSrc: ['https://www.youtube-nocookie.com'] } })
103
+ * ```
104
+ */
105
+ csp?:
106
+ | false
107
+ | {
108
+ /** Extra `frame-src` origins (iframes this app embeds). */
109
+ frameSrc?: string[];
110
+ /** Extra `script-src` origins (third-party SDK scripts). */
111
+ scriptSrc?: string[];
112
+ /** Extra `connect-src` origins (XHR/fetch/WebSocket to a third party). */
113
+ connectSrc?: string[];
114
+ /** Extra `img-src` origins (hotlinked third-party images). */
115
+ imgSrc?: string[];
116
+ /** Extra `font-src` origins (third-party web fonts). */
117
+ fontSrc?: string[];
118
+ };
81
119
  /**
82
120
  * PWA configuration options
83
121
  * Set to false to disable PWA, or provide custom options
@@ -209,65 +247,115 @@ export function createBaseNextConfig(
209
247
  : `'self' ${options.allowIframeFrom!.join(' ')}`)
210
248
  : "'none'";
211
249
 
250
+ // `csp: false` opts an app out of the CSP header entirely (see the
251
+ // option's doc comment) — the other baseline headers still apply.
252
+ const cspDisabled = options.csp === false;
253
+ const cspExtra = typeof options.csp === 'object' ? options.csp : undefined;
254
+
212
255
  // In dev the backend (Django, Centrifugo) is plain http/ws on localhost,
213
256
  // so connect/img must allow http:/ws:. Production stays strict (https/wss
214
257
  // only) to forbid downgrade/cleartext exfiltration.
215
258
  // Stripe.js needs its SDK host in script-src, its iframe hosts in
216
259
  // frame-src, and its API host in connect-src. Opt-in per app via
217
- // `options.stripe` so non-payment apps keep the tighter baseline.
260
+ // `options.stripe` — one hardcoded special case of the general
261
+ // `options.csp` additive-origin escape hatch below.
218
262
  const stripeScript = options.stripe ? ' https://js.stripe.com' : '';
219
263
  const stripeConnect = options.stripe ? ' https://api.stripe.com' : '';
220
264
  const stripeFrame = options.stripe
221
265
  ? ' https://js.stripe.com https://checkout.stripe.com'
222
266
  : '';
223
267
 
268
+ const extraScript = cspExtra?.scriptSrc?.length ? ' ' + cspExtra.scriptSrc.join(' ') : '';
269
+ const extraConnect = cspExtra?.connectSrc?.length ? ' ' + cspExtra.connectSrc.join(' ') : '';
270
+ const extraFrame = cspExtra?.frameSrc?.length ? ' ' + cspExtra.frameSrc.join(' ') : '';
271
+ const extraImg = cspExtra?.imgSrc?.length ? ' ' + cspExtra.imgSrc.join(' ') : '';
272
+ const extraFont = cspExtra?.fontSrc?.length ? ' ' + cspExtra.fontSrc.join(' ') : '';
273
+
224
274
  const connectSrc = isDev
225
- ? `connect-src 'self' http: https: ws: wss:${stripeConnect}`
226
- : `connect-src 'self' https: wss:${stripeConnect}`;
275
+ ? `connect-src 'self' http: https: ws: wss:${stripeConnect}${extraConnect}`
276
+ : `connect-src 'self' https: wss:${stripeConnect}${extraConnect}`;
227
277
  const imgSrc = isDev
228
- ? "img-src 'self' data: blob: http: https:"
229
- : "img-src 'self' data: blob: https:";
230
-
231
- const cspDirectives = [
232
- "default-src 'self'",
233
- // Next.js requires inline + eval for its runtime/HMR. Tighten to nonces later.
234
- `script-src 'self' 'unsafe-inline' 'unsafe-eval'${stripeScript}`,
235
- "style-src 'self' 'unsafe-inline'",
236
- imgSrc,
237
- "font-src 'self' data:",
238
- // API/websocket calls go cross-origin to Django/Centrifugo.
239
- connectSrc,
240
- // Stripe Elements/Checkout mount iframes; add their hosts when enabled.
241
- `frame-src 'self'${stripeFrame}`,
242
- "object-src 'none'",
243
- "base-uri 'self'",
244
- "form-action 'self'",
245
- `frame-ancestors ${frameAncestors}`,
246
- ];
247
-
248
- headers.push({
249
- source: '/:path*',
250
- headers: [
251
- { key: 'X-Content-Type-Options', value: 'nosniff' },
252
- { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
253
- { key: 'Content-Security-Policy', value: cspDirectives.join('; ') },
254
- // X-Frame-Options for older browsers. Omitted when a wildcard iframe
255
- // allowlist is set (browsers then rely on CSP frame-ancestors).
256
- ...(hasIframeAllowlist && options.allowIframeFrom!.includes('*')
257
- ? []
258
- : [{ key: 'X-Frame-Options', value: hasIframeAllowlist ? 'SAMEORIGIN' : 'DENY' }]),
259
- ],
260
- });
278
+ ? `img-src 'self' data: blob: http: https:${extraImg}`
279
+ : `img-src 'self' data: blob: https:${extraImg}`;
280
+
281
+ if (!cspDisabled) {
282
+ const cspDirectives = [
283
+ "default-src 'self'",
284
+ // Next.js requires inline + eval for its runtime/HMR. Tighten to nonces later.
285
+ `script-src 'self' 'unsafe-inline' 'unsafe-eval'${stripeScript}${extraScript}`,
286
+ "style-src 'self' 'unsafe-inline'",
287
+ imgSrc,
288
+ `font-src 'self' data:${extraFont}`,
289
+ // API/websocket calls go cross-origin to Django/Centrifugo.
290
+ connectSrc,
291
+ // Stripe Elements/Checkout and any app-declared trusted origins
292
+ // (options.csp.frameSrc) mount iframes; add their hosts when enabled.
293
+ `frame-src 'self'${stripeFrame}${extraFrame}`,
294
+ "object-src 'none'",
295
+ "base-uri 'self'",
296
+ "form-action 'self'",
297
+ `frame-ancestors ${frameAncestors}`,
298
+ ];
299
+
300
+ headers.push({
301
+ source: '/:path*',
302
+ headers: [
303
+ { key: 'X-Content-Type-Options', value: 'nosniff' },
304
+ { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
305
+ { key: 'Content-Security-Policy', value: cspDirectives.join('; ') },
306
+ // X-Frame-Options for older browsers. Omitted when a wildcard iframe
307
+ // allowlist is set (browsers then rely on CSP frame-ancestors).
308
+ ...(hasIframeAllowlist && options.allowIframeFrom!.includes('*')
309
+ ? []
310
+ : [{ key: 'X-Frame-Options', value: hasIframeAllowlist ? 'SAMEORIGIN' : 'DENY' }]),
311
+ ],
312
+ });
313
+ } else {
314
+ // Still ship the non-CSP baseline headers even with CSP disabled.
315
+ headers.push({
316
+ source: '/:path*',
317
+ headers: [
318
+ { key: 'X-Content-Type-Options', value: 'nosniff' },
319
+ { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
320
+ ...(hasIframeAllowlist && options.allowIframeFrom!.includes('*')
321
+ ? []
322
+ : [{ key: 'X-Frame-Options', value: hasIframeAllowlist ? 'SAMEORIGIN' : 'DENY' }]),
323
+ ],
324
+ });
325
+ }
261
326
 
262
327
  // Merge with user-provided headers
263
328
  const userHeaders = options.headers ? await options.headers() : [];
264
329
  return [...headers, ...userHeaders];
265
330
  },
266
331
 
267
- // Rewrites (user-provided only)
268
- ...(options.rewrites && {
269
- rewrites: options.rewrites,
270
- }),
332
+ // Rewrites — first-party analytics ingest proxy, then user rewrites.
333
+ //
334
+ // @djangocfg/analytics POSTs to a RELATIVE /cfg/analytics/collect/ so the beacon
335
+ // stays same-origin (sendBeacon is unreliable cross-origin). On a split-domain
336
+ // deploy (Next on one host, Django on `apiUrl`) that same-origin path has no
337
+ // route here, so every beacon 404s and nothing is ingested — unless we proxy it.
338
+ // Same-domain and static builds keep apiUrl empty and need no proxy.
339
+ ...(((apiUrl && !isStaticBuild) || options.rewrites)
340
+ ? {
341
+ async rewrites() {
342
+ const analyticsProxy =
343
+ apiUrl && !isStaticBuild
344
+ ? [{ source: '/cfg/analytics/:path*', destination: `${apiUrl}/cfg/analytics/:path*` }]
345
+ : [];
346
+ const user = options.rewrites ? await options.rewrites() : [];
347
+ // Ingest must resolve before any page/filesystem route can shadow it.
348
+ if (Array.isArray(user)) {
349
+ return { beforeFiles: analyticsProxy, afterFiles: user, fallback: [] };
350
+ }
351
+ return {
352
+ beforeFiles: [...analyticsProxy, ...(user?.beforeFiles ?? [])],
353
+ afterFiles: user?.afterFiles ?? [],
354
+ fallback: user?.fallback ?? [],
355
+ };
356
+ },
357
+ }
358
+ : {}),
271
359
 
272
360
  // Redirects for URL normalization
273
361
  // Fixes "Failed to construct 'URL': Invalid URL" error with double slashes
@@ -388,6 +476,7 @@ export function createBaseNextConfig(
388
476
  allowIframeFrom,
389
477
  dpop,
390
478
  stripe,
479
+ csp,
391
480
  ...nextConfigOptions
392
481
  } = options;
393
482