@djangocfg/nextjs 2.1.478 → 2.1.480
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 +157 -8
- package/dist/config/index.mjs +124 -14
- package/dist/config/index.mjs.map +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +124 -14
- package/dist/index.mjs.map +1 -1
- package/package.json +9 -9
- package/src/config/createNextConfig.ts +69 -28
- package/src/config/csp.ts +180 -0
- package/src/config/index.ts +9 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/nextjs",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.480",
|
|
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.480",
|
|
148
|
+
"@djangocfg/i18n": "^2.1.480",
|
|
149
|
+
"@djangocfg/ui-core": "^2.1.480",
|
|
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.480",
|
|
172
|
+
"@djangocfg/i18n": "^2.1.480",
|
|
173
|
+
"@djangocfg/layouts": "^2.1.480",
|
|
174
|
+
"@djangocfg/typescript-config": "^2.1.480",
|
|
175
|
+
"@djangocfg/ui-core": "^2.1.480",
|
|
176
176
|
"@types/node": "^25.9.5",
|
|
177
177
|
"@types/react": "19.2.15",
|
|
178
178
|
"@types/react-dom": "19.2.3",
|
|
@@ -27,6 +27,7 @@ import type { Configuration as WebpackConfig } from 'webpack';
|
|
|
27
27
|
import { type I18nPluginOptions, withI18n } from '../i18n/plugin';
|
|
28
28
|
import { type PWAPluginOptions, withPWA } from '../pwa/plugin';
|
|
29
29
|
import { DEFAULT_OPTIMIZE_PACKAGES, DEFAULT_TRANSPILE_PACKAGES } from './constants';
|
|
30
|
+
import { CSP_BASELINE_VENDORS, type CspVendor, resolveCspVendors } from './csp';
|
|
30
31
|
import { addCompressionPlugins } from './plugins/compression';
|
|
31
32
|
import { DevStartupPlugin } from './plugins/devStartup';
|
|
32
33
|
import { deepMerge } from './utils/deepMerge';
|
|
@@ -87,24 +88,50 @@ export interface BaseNextConfigOptions {
|
|
|
87
88
|
* naming every future embed's origin is not worth the maintenance —
|
|
88
89
|
* `X-Content-Type-Options`/`Referrer-Policy`/`X-Frame-Options` below
|
|
89
90
|
* still apply.
|
|
90
|
-
* - object — keep the baseline CSP but additively loosen
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
* leave that directive untouched.
|
|
91
|
+
* - object — keep the baseline CSP but additively loosen it. Prefer
|
|
92
|
+
* `vendors` (declare WHAT you use by name; the package owns which origins
|
|
93
|
+
* that implies) over hand-listing origins per directive. Every array is
|
|
94
|
+
* additive; omit a key to leave that directive untouched.
|
|
94
95
|
* - omitted — tightest default (`frame-src 'self'`, etc.), same as today.
|
|
95
96
|
*
|
|
97
|
+
* **The deployment platform counts as a vendor.** Some origins are injected
|
|
98
|
+
* into the response by the EDGE, after Next.js has emitted this header —
|
|
99
|
+
* Cloudflare Web Analytics rewrites `beacon.min.js` into the HTML when the
|
|
100
|
+
* feature is on for the zone. Nothing in the source tree reveals that, so
|
|
101
|
+
* any app served through such a zone must name the platform in `vendors`
|
|
102
|
+
* (e.g. `'cloudflare'`) or it will violate its own CSP in production only.
|
|
103
|
+
*
|
|
96
104
|
* @example
|
|
97
105
|
* ```ts
|
|
98
|
-
* //
|
|
99
|
-
*
|
|
106
|
+
* // Declare the third parties this app actually uses — origins come from
|
|
107
|
+
* // the shared registry, so a vendor host change is fixed in one place.
|
|
108
|
+
* createBaseNextConfig({ csp: { vendors: ['stripe', 'cloudflare'] } })
|
|
100
109
|
*
|
|
101
|
-
* //
|
|
110
|
+
* // Escape hatch for something not in the registry yet
|
|
102
111
|
* createBaseNextConfig({ csp: { frameSrc: ['https://www.youtube-nocookie.com'] } })
|
|
112
|
+
*
|
|
113
|
+
* // No CSP at all — simplest option for a low-risk app with ad hoc embeds
|
|
114
|
+
* createBaseNextConfig({ csp: false })
|
|
103
115
|
* ```
|
|
104
116
|
*/
|
|
105
117
|
csp?:
|
|
106
118
|
| false
|
|
107
119
|
| {
|
|
120
|
+
/**
|
|
121
|
+
* Named third-party vendors this app uses (`'stripe'`, `'cloudflare'`,
|
|
122
|
+
* `'google-analytics'`, …). Each expands to the exact origins that
|
|
123
|
+
* vendor needs across the relevant directives. See `CSP_VENDOR_PRESETS`
|
|
124
|
+
* for the full list. Unknown names warn at build time, never throw.
|
|
125
|
+
*/
|
|
126
|
+
vendors?: (CspVendor | (string & {}))[];
|
|
127
|
+
/**
|
|
128
|
+
* Cloudflare Web Analytics origins are in the baseline (see
|
|
129
|
+
* `CSP_BASELINE_VENDORS`) because DjangoCFG apps are Cloudflare-served
|
|
130
|
+
* and the beacon is injected at the edge, where no app can see it.
|
|
131
|
+
* Set `false` only for an app served off Cloudflare entirely.
|
|
132
|
+
* @default true
|
|
133
|
+
*/
|
|
134
|
+
cloudflare?: boolean;
|
|
108
135
|
/** Extra `frame-src` origins (iframes this app embeds). */
|
|
109
136
|
frameSrc?: string[];
|
|
110
137
|
/** Extra `script-src` origins (third-party SDK scripts). */
|
|
@@ -255,25 +282,39 @@ export function createBaseNextConfig(
|
|
|
255
282
|
// In dev the backend (Django, Centrifugo) is plain http/ws on localhost,
|
|
256
283
|
// so connect/img must allow http:/ws:. Production stays strict (https/wss
|
|
257
284
|
// only) to forbid downgrade/cleartext exfiltration.
|
|
258
|
-
//
|
|
259
|
-
//
|
|
260
|
-
//
|
|
261
|
-
// `
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
const
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
285
|
+
//
|
|
286
|
+
// Third-party origins come from the shared vendor registry (./csp), so
|
|
287
|
+
// "which hosts does Stripe need" is answered in exactly one place. The
|
|
288
|
+
// legacy `stripe: true` flag is just sugar for `vendors: ['stripe']` and
|
|
289
|
+
// resolves through that same registry — no second copy of the origins.
|
|
290
|
+
// Baseline vendors apply unless explicitly opted out. Cloudflare is in
|
|
291
|
+
// here rather than per-app because the edge injects its beacon into the
|
|
292
|
+
// response AFTER this header is emitted — an app cannot detect that from
|
|
293
|
+
// its own source, so leaving it opt-in guarantees a silent production
|
|
294
|
+
// breakage in every app that forgets it (which was all of them).
|
|
295
|
+
const vendorNames = [
|
|
296
|
+
...(cspExtra?.cloudflare === false ? [] : CSP_BASELINE_VENDORS),
|
|
297
|
+
...(options.stripe ? ['stripe'] : []),
|
|
298
|
+
...(cspExtra?.vendors ?? []),
|
|
299
|
+
];
|
|
300
|
+
const vendor = resolveCspVendors(vendorNames);
|
|
301
|
+
|
|
302
|
+
// Vendor-derived origins and the per-directive escape hatch are equivalent
|
|
303
|
+
// and additive; a host named by both is emitted once.
|
|
304
|
+
const join = (...groups: (string[] | undefined)[]) => {
|
|
305
|
+
const all = [...new Set(groups.flatMap((g) => g ?? []))];
|
|
306
|
+
return all.length ? ' ' + all.join(' ') : '';
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
const extraScript = join(vendor.scriptSrc, cspExtra?.scriptSrc);
|
|
310
|
+
const extraConnect = join(vendor.connectSrc, cspExtra?.connectSrc);
|
|
311
|
+
const extraFrame = join(vendor.frameSrc, cspExtra?.frameSrc);
|
|
312
|
+
const extraImg = join(vendor.imgSrc, cspExtra?.imgSrc);
|
|
313
|
+
const extraFont = join(vendor.fontSrc, cspExtra?.fontSrc);
|
|
273
314
|
|
|
274
315
|
const connectSrc = isDev
|
|
275
|
-
? `connect-src 'self' http: https: ws: wss:${
|
|
276
|
-
: `connect-src 'self' https: wss:${
|
|
316
|
+
? `connect-src 'self' http: https: ws: wss:${extraConnect}`
|
|
317
|
+
: `connect-src 'self' https: wss:${extraConnect}`;
|
|
277
318
|
const imgSrc = isDev
|
|
278
319
|
? `img-src 'self' data: blob: http: https:${extraImg}`
|
|
279
320
|
: `img-src 'self' data: blob: https:${extraImg}`;
|
|
@@ -282,15 +323,15 @@ export function createBaseNextConfig(
|
|
|
282
323
|
const cspDirectives = [
|
|
283
324
|
"default-src 'self'",
|
|
284
325
|
// Next.js requires inline + eval for its runtime/HMR. Tighten to nonces later.
|
|
285
|
-
`script-src 'self' 'unsafe-inline' 'unsafe-eval'${
|
|
326
|
+
`script-src 'self' 'unsafe-inline' 'unsafe-eval'${extraScript}`,
|
|
286
327
|
"style-src 'self' 'unsafe-inline'",
|
|
287
328
|
imgSrc,
|
|
288
329
|
`font-src 'self' data:${extraFont}`,
|
|
289
330
|
// API/websocket calls go cross-origin to Django/Centrifugo.
|
|
290
331
|
connectSrc,
|
|
291
|
-
// Stripe Elements/Checkout and any
|
|
292
|
-
//
|
|
293
|
-
`frame-src 'self'${
|
|
332
|
+
// Declared vendors (Stripe Elements/Checkout, video embeds…) and any
|
|
333
|
+
// app-declared trusted origins mount iframes; add their hosts here.
|
|
334
|
+
`frame-src 'self'${extraFrame}`,
|
|
294
335
|
"object-src 'none'",
|
|
295
336
|
"base-uri 'self'",
|
|
296
337
|
"form-action 'self'",
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CSP Vendor Presets
|
|
3
|
+
*
|
|
4
|
+
* The problem this solves: every third-party script a DjangoCFG app loads needs
|
|
5
|
+
* its origin named in the CSP, and until now each app had to hand-list those
|
|
6
|
+
* origins (`csp: { scriptSrc: [...] }`) in its own `next.config.ts`. That
|
|
7
|
+
* repeats the same Stripe/Cloudflare/Google origin strings across every app,
|
|
8
|
+
* and — worse — nobody remembers which directive a given vendor needs, so the
|
|
9
|
+
* usual outcome is a production-only console error and a scramble.
|
|
10
|
+
*
|
|
11
|
+
* Instead, apps declare WHAT THEY USE by name (`csp: { vendors: ['stripe'] }`)
|
|
12
|
+
* and this registry owns WHICH ORIGINS that implies. When a vendor changes a
|
|
13
|
+
* host, it is fixed once here and every app inherits the fix on its next build.
|
|
14
|
+
*
|
|
15
|
+
* Adding a vendor: add an entry below, keyed by a short stable name, listing
|
|
16
|
+
* only the directives it genuinely needs. Keep the origins exact — never widen
|
|
17
|
+
* a directive to a bare `https:` to make a vendor work.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/** The CSP directives a vendor preset may contribute origins to. */
|
|
21
|
+
export interface CspOrigins {
|
|
22
|
+
/** `script-src` — the vendor's SDK/tag `<script>` hosts. */
|
|
23
|
+
scriptSrc?: string[];
|
|
24
|
+
/** `frame-src` — hosts the vendor mounts iframes from. */
|
|
25
|
+
frameSrc?: string[];
|
|
26
|
+
/** `connect-src` — hosts the vendor makes XHR/fetch/WebSocket calls to. */
|
|
27
|
+
connectSrc?: string[];
|
|
28
|
+
/** `img-src` — hosts the vendor loads images/pixels from. */
|
|
29
|
+
imgSrc?: string[];
|
|
30
|
+
/** `font-src` — hosts the vendor loads web fonts from. */
|
|
31
|
+
fontSrc?: string[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Known third-party vendors and the CSP origins each one requires.
|
|
36
|
+
*
|
|
37
|
+
* Every entry is OFF by default — an app opts in via `csp.vendors`, so an app
|
|
38
|
+
* that mounts nothing third-party keeps the tightest possible policy.
|
|
39
|
+
*/
|
|
40
|
+
export const CSP_VENDOR_PRESETS = {
|
|
41
|
+
/**
|
|
42
|
+
* Stripe.js / Checkout / Elements.
|
|
43
|
+
*
|
|
44
|
+
* `js.stripe.com` serves the SDK and also hosts the Elements iframes;
|
|
45
|
+
* `checkout.stripe.com` is the hosted Checkout iframe; `api.stripe.com`
|
|
46
|
+
* takes the tokenization/confirm XHR. Without all three the payment form
|
|
47
|
+
* silently fails to render.
|
|
48
|
+
*/
|
|
49
|
+
stripe: {
|
|
50
|
+
scriptSrc: ['https://js.stripe.com'],
|
|
51
|
+
frameSrc: ['https://js.stripe.com', 'https://checkout.stripe.com'],
|
|
52
|
+
connectSrc: ['https://api.stripe.com'],
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Cloudflare Web Analytics (the RUM beacon).
|
|
57
|
+
*
|
|
58
|
+
* IMPORTANT: this script is NOT in the app's own HTML — when Web Analytics
|
|
59
|
+
* is enabled on the zone, Cloudflare INJECTS `beacon.min.js` into the
|
|
60
|
+
* response at the edge, after Next.js has already emitted the CSP header.
|
|
61
|
+
* So any app served through a Cloudflare zone with that feature on will
|
|
62
|
+
* violate its own CSP unless this preset is enabled, no matter what the
|
|
63
|
+
* source tree contains. The beacon also POSTs its payload back to
|
|
64
|
+
* `cloudflareinsights.com`, hence the `connect-src` entry.
|
|
65
|
+
*/
|
|
66
|
+
cloudflare: {
|
|
67
|
+
scriptSrc: ['https://static.cloudflareinsights.com'],
|
|
68
|
+
connectSrc: ['https://cloudflareinsights.com', 'https://static.cloudflareinsights.com'],
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
/** Google Analytics 4 / gtag.js. */
|
|
72
|
+
'google-analytics': {
|
|
73
|
+
scriptSrc: ['https://www.googletagmanager.com', 'https://www.google-analytics.com'],
|
|
74
|
+
connectSrc: [
|
|
75
|
+
'https://www.google-analytics.com',
|
|
76
|
+
'https://analytics.google.com',
|
|
77
|
+
'https://region1.google-analytics.com',
|
|
78
|
+
],
|
|
79
|
+
imgSrc: ['https://www.google-analytics.com', 'https://www.googletagmanager.com'],
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
/** Google Tag Manager (the container itself; tags it loads may need more). */
|
|
83
|
+
'google-tag-manager': {
|
|
84
|
+
scriptSrc: ['https://www.googletagmanager.com'],
|
|
85
|
+
connectSrc: ['https://www.googletagmanager.com'],
|
|
86
|
+
imgSrc: ['https://www.googletagmanager.com'],
|
|
87
|
+
frameSrc: ['https://www.googletagmanager.com'],
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
/** Google Fonts (stylesheet host + the font files themselves). */
|
|
91
|
+
'google-fonts': {
|
|
92
|
+
fontSrc: ['https://fonts.gstatic.com'],
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
/** Sentry browser SDK — errors/traces are POSTed to the ingest host. */
|
|
96
|
+
sentry: {
|
|
97
|
+
connectSrc: ['https://*.ingest.sentry.io', 'https://*.sentry.io'],
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
/** YouTube embeds (privacy-enhanced host included). */
|
|
101
|
+
youtube: {
|
|
102
|
+
frameSrc: ['https://www.youtube.com', 'https://www.youtube-nocookie.com'],
|
|
103
|
+
imgSrc: ['https://i.ytimg.com'],
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
/** Vimeo embeds. */
|
|
107
|
+
vimeo: {
|
|
108
|
+
frameSrc: ['https://player.vimeo.com'],
|
|
109
|
+
imgSrc: ['https://i.vimeocdn.com'],
|
|
110
|
+
},
|
|
111
|
+
} as const satisfies Record<string, CspOrigins>;
|
|
112
|
+
|
|
113
|
+
/** Name of a vendor preset in {@link CSP_VENDOR_PRESETS}. */
|
|
114
|
+
export type CspVendor = keyof typeof CSP_VENDOR_PRESETS;
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Vendors applied to every app unless explicitly disabled.
|
|
118
|
+
*
|
|
119
|
+
* Only for origins the DEPLOYMENT PLATFORM injects, which an app therefore
|
|
120
|
+
* cannot discover by reading its own source. Cloudflare's Web Analytics beacon
|
|
121
|
+
* is rewritten into the HTML at the edge, after Next.js has already sent the
|
|
122
|
+
* CSP header — so an opt-in flag here reliably produces a production-only
|
|
123
|
+
* console error that nobody notices until a user reports it. (Observed
|
|
124
|
+
* 2026-07-22: all six DjangoCFG-served hosts were violating their own CSP.)
|
|
125
|
+
*
|
|
126
|
+
* Do NOT add ordinary third parties here; those belong in `csp.vendors` on the
|
|
127
|
+
* app that actually uses them, so unrelated apps keep the tighter policy.
|
|
128
|
+
*/
|
|
129
|
+
export const CSP_BASELINE_VENDORS: CspVendor[] = ['cloudflare'];
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Merge the named vendor presets into a single set of extra origins.
|
|
133
|
+
*
|
|
134
|
+
* Unknown names are ignored rather than thrown on: a typo must not break the
|
|
135
|
+
* production build of an app whose only mistake was a misspelled analytics
|
|
136
|
+
* vendor. `warnUnknown` surfaces them on the build log instead.
|
|
137
|
+
*/
|
|
138
|
+
export function resolveCspVendors(
|
|
139
|
+
vendors: readonly string[] | undefined,
|
|
140
|
+
warnUnknown = true
|
|
141
|
+
): CspOrigins {
|
|
142
|
+
const merged: Required<CspOrigins> = {
|
|
143
|
+
scriptSrc: [],
|
|
144
|
+
frameSrc: [],
|
|
145
|
+
connectSrc: [],
|
|
146
|
+
imgSrc: [],
|
|
147
|
+
fontSrc: [],
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
if (!vendors?.length) return merged;
|
|
151
|
+
|
|
152
|
+
const unknown: string[] = [];
|
|
153
|
+
|
|
154
|
+
for (const name of vendors) {
|
|
155
|
+
const preset = (CSP_VENDOR_PRESETS as Record<string, CspOrigins>)[name];
|
|
156
|
+
if (!preset) {
|
|
157
|
+
unknown.push(name);
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
for (const key of Object.keys(merged) as (keyof Required<CspOrigins>)[]) {
|
|
161
|
+
const origins = preset[key];
|
|
162
|
+
if (origins) merged[key].push(...origins);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (unknown.length && warnUnknown) {
|
|
167
|
+
const known = Object.keys(CSP_VENDOR_PRESETS).join(', ');
|
|
168
|
+
console.warn(
|
|
169
|
+
`[djangocfg] Unknown CSP vendor(s): ${unknown.join(', ')}. Known vendors: ${known}`
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// De-duplicate so two presets sharing a host (e.g. GA + GTM both using
|
|
174
|
+
// googletagmanager.com) don't emit it twice in the header.
|
|
175
|
+
for (const key of Object.keys(merged) as (keyof Required<CspOrigins>)[]) {
|
|
176
|
+
merged[key] = [...new Set(merged[key])];
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return merged;
|
|
180
|
+
}
|
package/src/config/index.ts
CHANGED
|
@@ -24,6 +24,15 @@ export {
|
|
|
24
24
|
DJANGO_CFG_BANNER,
|
|
25
25
|
} from './constants';
|
|
26
26
|
|
|
27
|
+
// CSP vendor presets — declare third parties by name instead of by origin
|
|
28
|
+
export {
|
|
29
|
+
CSP_VENDOR_PRESETS,
|
|
30
|
+
CSP_BASELINE_VENDORS,
|
|
31
|
+
resolveCspVendors,
|
|
32
|
+
type CspVendor,
|
|
33
|
+
type CspOrigins,
|
|
34
|
+
} from './csp';
|
|
35
|
+
|
|
27
36
|
// Re-export AI constants from ai module
|
|
28
37
|
export {
|
|
29
38
|
MCP_BASE_URL,
|