@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/dist/config/index.d.mts +36 -0
- package/dist/config/index.mjs +71 -34
- package/dist/config/index.mjs.map +1 -1
- package/dist/index.mjs +71 -34
- package/dist/index.mjs.map +1 -1
- package/package.json +9 -9
- package/src/config/createNextConfig.ts +129 -40
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/nextjs",
|
|
3
|
-
"version": "2.1.
|
|
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.
|
|
148
|
-
"@djangocfg/i18n": "^2.1.
|
|
149
|
-
"@djangocfg/ui-core": "^2.1.
|
|
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.
|
|
172
|
-
"@djangocfg/i18n": "^2.1.
|
|
173
|
-
"@djangocfg/layouts": "^2.1.
|
|
174
|
-
"@djangocfg/typescript-config": "^2.1.
|
|
175
|
-
"@djangocfg/ui-core": "^2.1.
|
|
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`
|
|
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
|
-
?
|
|
229
|
-
:
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
headers
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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
|
|
268
|
-
|
|
269
|
-
|
|
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
|
|