@djangocfg/nextjs 2.1.481 → 2.1.482

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.481",
3
+ "version": "2.1.482",
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.481",
148
- "@djangocfg/i18n": "^2.1.481",
149
- "@djangocfg/ui-core": "^2.1.481",
147
+ "@djangocfg/devtools": "^2.1.482",
148
+ "@djangocfg/i18n": "^2.1.482",
149
+ "@djangocfg/ui-core": "^2.1.482",
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.481",
172
- "@djangocfg/i18n": "^2.1.481",
173
- "@djangocfg/layouts": "^2.1.481",
174
- "@djangocfg/typescript-config": "^2.1.481",
175
- "@djangocfg/ui-core": "^2.1.481",
171
+ "@djangocfg/devtools": "^2.1.482",
172
+ "@djangocfg/i18n": "^2.1.482",
173
+ "@djangocfg/layouts": "^2.1.482",
174
+ "@djangocfg/typescript-config": "^2.1.482",
175
+ "@djangocfg/ui-core": "^2.1.482",
176
176
  "@types/node": "^25.9.5",
177
177
  "@types/react": "19.2.15",
178
178
  "@types/react-dom": "19.2.3",
@@ -94,18 +94,19 @@ export interface BaseNextConfigOptions {
94
94
  * additive; omit a key to leave that directive untouched.
95
95
  * - omitted — tightest default (`frame-src 'self'`, etc.), same as today.
96
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.
97
+ * **Scripts nothing in this file can see are already handled.** Cloudflare
98
+ * Web Analytics (injected at the edge) and Google Analytics (injected at
99
+ * runtime by `BaseApp analytics={{ googleTrackingId }}`) are both switched on
100
+ * outside `next.config.ts`, so their origins are in the baseline and you do
101
+ * NOT need to list them — see `CSP_BASELINE_VENDORS`. Turn them off with
102
+ * `baseline: false`.
103
103
  *
104
104
  * @example
105
105
  * ```ts
106
106
  * // Declare the third parties this app actually uses — origins come from
107
107
  * // the shared registry, so a vendor host change is fixed in one place.
108
- * createBaseNextConfig({ csp: { vendors: ['stripe', 'cloudflare'] } })
108
+ * // (Cloudflare + GA are already in the baseline; no need to list them.)
109
+ * createBaseNextConfig({ csp: { vendors: ['stripe'] } })
109
110
  *
110
111
  * // Escape hatch for something not in the registry yet
111
112
  * createBaseNextConfig({ csp: { frameSrc: ['https://www.youtube-nocookie.com'] } })
@@ -125,13 +126,18 @@ export interface BaseNextConfigOptions {
125
126
  */
126
127
  vendors?: (CspVendor | (string & {}))[];
127
128
  /**
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.
129
+ * Drop the baseline vendors (see `CSP_BASELINE_VENDORS`: Cloudflare Web
130
+ * Analytics + Google Analytics). They are on by default because each is
131
+ * switched on somewhere `next.config.ts` cannot read — the Cloudflare
132
+ * beacon is injected at the edge, and gtag.js is injected at runtime by
133
+ * `BaseApp analytics={{ googleTrackingId }}` — so an opt-in flag would
134
+ * only ever be discovered as a production console error.
135
+ *
136
+ * Set `false` for an app you know mounts neither, to get back the
137
+ * tightest `script-src`.
132
138
  * @default true
133
139
  */
134
- cloudflare?: boolean;
140
+ baseline?: boolean;
135
141
  /** Extra `frame-src` origins (iframes this app embeds). */
136
142
  frameSrc?: string[];
137
143
  /** Extra `script-src` origins (third-party SDK scripts). */
@@ -287,13 +293,15 @@ export function createBaseNextConfig(
287
293
  // "which hosts does Stripe need" is answered in exactly one place. The
288
294
  // legacy `stripe: true` flag is just sugar for `vendors: ['stripe']` and
289
295
  // 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).
296
+ // Baseline vendors apply unless explicitly opted out. They are baseline
297
+ // rather than per-app because each is enabled somewhere this file cannot
298
+ // read — Cloudflare's beacon is injected by the edge after this header is
299
+ // emitted, and gtag.js is injected at runtime by `BaseApp analytics={{
300
+ // googleTrackingId }}` from the app's settings.ts. Leaving either opt-in
301
+ // guarantees a silent production breakage in every app that forgets it
302
+ // (which was all of them). See CSP_BASELINE_VENDORS for the full rule.
295
303
  const vendorNames = [
296
- ...(cspExtra?.cloudflare === false ? [] : CSP_BASELINE_VENDORS),
304
+ ...(cspExtra?.baseline === false ? [] : CSP_BASELINE_VENDORS),
297
305
  ...(options.stripe ? ['stripe'] : []),
298
306
  ...(cspExtra?.vendors ?? []),
299
307
  ];
package/src/config/csp.ts CHANGED
@@ -116,17 +116,50 @@ export type CspVendor = keyof typeof CSP_VENDOR_PRESETS;
116
116
  /**
117
117
  * Vendors applied to every app unless explicitly disabled.
118
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.)
119
+ * The rule for this list: an origin belongs here when the app CANNOT DECLARE IT
120
+ * FROM `next.config.ts`, because whatever turns the script on lives somewhere
121
+ * the config file cannot read. Leaving such a vendor opt-in does not produce a
122
+ * build error or a dev warning — it produces a production-only console error
123
+ * that nobody sees until a user reports a broken page.
125
124
  *
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.
125
+ * - `cloudflare` — the Web Analytics beacon is rewritten into the HTML AT THE
126
+ * EDGE, after Next.js has already sent the CSP header. Nothing in the source
127
+ * tree reveals it. (Observed 2026-07-22: all six DjangoCFG-served hosts were
128
+ * violating their own CSP.)
129
+ *
130
+ * - `google-analytics` — gtag.js is injected AT RUNTIME by `@djangocfg/layouts`
131
+ * (`BaseApp analytics={{ googleTrackingId }}` → the lazy `ga4()` transport).
132
+ * That id is an ordinary TS value in the app's `settings.ts`, not an env var,
133
+ * so `next.config.ts` — a different module, evaluated in a different process,
134
+ * before any of that code runs — has no way to see whether GA is on. The app
135
+ * author enables analytics in one file and silently breaks CSP in another.
136
+ * (Observed 2026-07-23: cmdop `apps/web` blocked on
137
+ * `googletagmanager.com/gtag/js?id=G-YJ0F48S83F`.)
138
+ *
139
+ * - `youtube` / `vimeo` — `VideoPlayer` (`@djangocfg/ui-tools`) mounts these
140
+ * iframes AT RUNTIME from a URL inside a chat message (see
141
+ * `chat/messages/blocks/renderers/VideoBlock.tsx`). The app author enables a
142
+ * chat; whether any user ever pastes a YouTube link is not knowable at build
143
+ * time, so no `next.config.ts` can declare it honestly.
144
+ *
145
+ * Note what these have in common: the script/frame is turned on by the RUNTIME
146
+ * (the edge, a package, or an end user), never by the app author editing the
147
+ * config. Stripe is the counter-example and deliberately NOT here — a developer
148
+ * decides to mount a payment form, knows they did, and can say `vendors:
149
+ * ['stripe']`; putting it in the baseline would hand `frame-src js.stripe.com`
150
+ * to marketing sites that never take a payment.
151
+ *
152
+ * Do NOT add ordinary third parties here — anything an app knowingly mounts and
153
+ * can name in its own config belongs in `csp.vendors`, so unrelated apps keep
154
+ * the tighter policy. The cost of an entry here is that every app permits these
155
+ * hosts; that is only worth paying when the alternative is silent breakage.
128
156
  */
129
- export const CSP_BASELINE_VENDORS: CspVendor[] = ['cloudflare'];
157
+ export const CSP_BASELINE_VENDORS: CspVendor[] = [
158
+ 'cloudflare',
159
+ 'google-analytics',
160
+ 'youtube',
161
+ 'vimeo',
162
+ ];
130
163
 
131
164
  /**
132
165
  * Merge the named vendor presets into a single set of extra origins.