@djangocfg/nextjs 2.1.427 → 2.1.429

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/README.md CHANGED
@@ -27,6 +27,8 @@
27
27
  - **i18n** — full next-intl integration with routing, middleware, and components
28
28
  - **PWA** — zero-config service worker and manifest
29
29
  - **Base Next.js Config** — reusable `createBaseNextConfig()` factory for monorepos
30
+ - **Security headers + CSP** — baseline Content-Security-Policy and hardening headers on every route, dev-aware (see below)
31
+ - **DPoP auth** — one-flag opt-in to RFC 9449 sender-constrained tokens (`dpop: true`) — see [`@docs/DPOP.md`](./@docs/DPOP.md)
30
32
  - **Sitemap** — paginated sitemap-index backed by `django_cfg.modules.django_sitemap` (handles 2M+ URLs)
31
33
  - **Health Checks** — production-ready health monitoring endpoints
32
34
  - **Navigation** — route definitions, menu generation, and active-state helpers
@@ -112,6 +114,37 @@ export default withBundleAnalyzer(createBaseNextConfig({
112
114
  }));
113
115
  ```
114
116
 
117
+ ### Security headers & CSP
118
+
119
+ `createBaseNextConfig()` adds baseline security headers to **every route** out of
120
+ the box — no setup required:
121
+
122
+ - `Content-Security-Policy` — a pragmatic baseline that blocks the high-value
123
+ injection vectors (`object-src 'none'`, `base-uri 'self'`, `form-action 'self'`,
124
+ `frame-ancestors`) while keeping the inline/eval scripts a Next.js app needs.
125
+ **Dev-aware:** `connect-src`/`img-src` allow `http:`/`ws:` in development (local
126
+ Django/Centrifugo) and tighten to `https:`/`wss:` only in production.
127
+ - `X-Content-Type-Options: nosniff`, `Referrer-Policy: strict-origin-when-cross-origin`
128
+ - `X-Frame-Options: DENY` (or `SAMEORIGIN` when `allowIframeFrom` is set)
129
+
130
+ A fully strict nonce-based `script-src` is a planned follow-up (needs middleware +
131
+ per-request nonce). To allow iframe embedding, pass `allowIframeFrom: ['https://example.com']`.
132
+
133
+ ### DPoP — sender-constrained tokens (RFC 9449)
134
+
135
+ Make a stolen access token useless, **without a BFF/proxy** — opt in with one flag:
136
+
137
+ ```tsx
138
+ export default createBaseNextConfig({
139
+ dpop: true, // inlines NEXT_PUBLIC_DPOP_ENABLED='true'
140
+ });
141
+ ```
142
+
143
+ The browser holds a non-extractable key and signs a per-request `DPoP` proof; the
144
+ backend binds the token to that key (`cnf.jkt`) and rejects replays. Requires
145
+ `JWTConfig(dpop_enabled=True)` on the Django side. Full guide:
146
+ [`@docs/DPOP.md`](./@docs/DPOP.md).
147
+
115
148
  ### PWA (Progressive Web App)
116
149
 
117
150
  **Zero-config PWA** - Works out of the box! Service worker and offline support included automatically.
@@ -50,6 +50,15 @@ interface BaseNextConfigOptions {
50
50
  * Set to ['*'] to allow all origins, or specify domains like ['https://djangocfg.com']
51
51
  */
52
52
  allowIframeFrom?: string[];
53
+ /**
54
+ * Enable DPoP (RFC 9449) sender-constrained tokens on the generated API
55
+ * client. When true, the client holds a non-extractable key and signs a
56
+ * per-request `DPoP` proof so a stolen token is useless. Must be paired with
57
+ * `jwt = JWTConfig(dpop_enabled=True)` on the Django backend. Default: false.
58
+ *
59
+ * Inlines `NEXT_PUBLIC_DPOP_ENABLED='true'` so the generated auth.ts activates.
60
+ */
61
+ dpop?: boolean;
53
62
  /**
54
63
  * PWA configuration options
55
64
  * Set to false to disable PWA, or provide custom options
@@ -103,9 +112,9 @@ declare function createBaseNextConfig(options?: BaseNextConfigOptions): NextConf
103
112
  */
104
113
  declare const PACKAGE_NAME = "@djangocfg/nextjs";
105
114
  declare const DJANGO_CFG_BANNER = "\n\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557\n\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D \u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557 \u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\n\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2554\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551 \u2588\u2588\u2588\u2557\n\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2551\u255A\u2588\u2588\u2557\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u255D \u2588\u2588\u2551 \u2588\u2588\u2551\n\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u255A\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2551\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D \u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\n\u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D\n";
106
- declare const DJANGOCFG_PACKAGES: readonly ["@djangocfg/ui-core", "@djangocfg/ui-nextjs", "@djangocfg/layouts", "@djangocfg/nextjs", "@djangocfg/api", "@djangocfg/centrifugo", "@djangocfg/eslint-config", "@djangocfg/typescript-config"];
107
- declare const DEFAULT_TRANSPILE_PACKAGES: readonly ["@djangocfg/i18n", "@djangocfg/ui-core", "@djangocfg/ui-nextjs", "@djangocfg/layouts", "@djangocfg/ui-tools", "@djangocfg/api", "@djangocfg/centrifugo", "@djangocfg/debuger", "@djangocfg/monitor", "@djangocfg/ext-support", "@djangocfg/ext-payments"];
108
- declare const DEFAULT_OPTIMIZE_PACKAGES: readonly ["@djangocfg/ui-core", "@djangocfg/ui-nextjs", "@djangocfg/layouts", "lucide-react", "recharts"];
115
+ declare const DJANGOCFG_PACKAGES: readonly ["@djangocfg/ui-core", "@djangocfg/layouts", "@djangocfg/nextjs", "@djangocfg/api", "@djangocfg/centrifugo", "@djangocfg/eslint-config", "@djangocfg/typescript-config"];
116
+ declare const DEFAULT_TRANSPILE_PACKAGES: readonly ["@djangocfg/i18n", "@djangocfg/ui-core", "@djangocfg/layouts", "@djangocfg/ui-tools", "@djangocfg/api", "@djangocfg/centrifugo", "@djangocfg/debuger", "@djangocfg/monitor"];
117
+ declare const DEFAULT_OPTIMIZE_PACKAGES: readonly ["@djangocfg/ui-core", "@djangocfg/layouts", "lucide-react", "recharts"];
109
118
 
110
119
  /**
111
120
  * Deep Merge Utility
@@ -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.427",
17
+ version: "2.1.429",
18
18
  description: "Next.js server utilities: sitemap, health, OG images, contact forms, navigation, config",
19
19
  keywords: [
20
20
  "nextjs",
@@ -263,7 +263,6 @@ var DJANGO_CFG_BANNER = `
263
263
  `;
264
264
  var DJANGOCFG_PACKAGES = [
265
265
  "@djangocfg/ui-core",
266
- "@djangocfg/ui-nextjs",
267
266
  "@djangocfg/layouts",
268
267
  "@djangocfg/nextjs",
269
268
  "@djangocfg/api",
@@ -274,20 +273,15 @@ var DJANGOCFG_PACKAGES = [
274
273
  var DEFAULT_TRANSPILE_PACKAGES = [
275
274
  "@djangocfg/i18n",
276
275
  "@djangocfg/ui-core",
277
- "@djangocfg/ui-nextjs",
278
276
  "@djangocfg/layouts",
279
277
  "@djangocfg/ui-tools",
280
278
  "@djangocfg/api",
281
279
  "@djangocfg/centrifugo",
282
280
  "@djangocfg/debuger",
283
- "@djangocfg/monitor",
284
- // Extensions (for source imports without build)
285
- "@djangocfg/ext-support",
286
- "@djangocfg/ext-payments"
281
+ "@djangocfg/monitor"
287
282
  ];
288
283
  var DEFAULT_OPTIMIZE_PACKAGES = [
289
284
  "@djangocfg/ui-core",
290
- "@djangocfg/ui-nextjs",
291
285
  "@djangocfg/layouts",
292
286
  "lucide-react",
293
287
  "recharts"
@@ -1353,6 +1347,10 @@ function createBaseNextConfig(options = {}) {
1353
1347
  const baseConfig = {
1354
1348
  reactStrictMode: true,
1355
1349
  trailingSlash: true,
1350
+ // Dev indicator position — set explicitly so it's managed from one place
1351
+ // across every @djangocfg app. Apps can override via `devIndicators` in
1352
+ // their options (deep-merged over this). Kept bottom-left for now.
1353
+ devIndicators: { position: "bottom-left" },
1356
1354
  // Static export configuration
1357
1355
  ...isStaticBuild && {
1358
1356
  output: "export",
@@ -1372,6 +1370,9 @@ function createBaseNextConfig(options = {}) {
1372
1370
  NEXT_PUBLIC_BASE_PATH: basePath,
1373
1371
  NEXT_PUBLIC_API_URL: apiUrl,
1374
1372
  NEXT_PUBLIC_SITE_URL: siteUrl,
1373
+ // DPoP toggle for the generated auth client (RFC 9449). Only inlined when
1374
+ // explicitly enabled, so non-DPoP apps stay on the plain Bearer path.
1375
+ ...options.dpop ? { NEXT_PUBLIC_DPOP_ENABLED: "true" } : {},
1375
1376
  // Disable Next.js telemetry (Next.js 15+ uses env var instead of config option)
1376
1377
  NEXT_TELEMETRY_DISABLED: "1",
1377
1378
  ...options.env
@@ -1392,19 +1393,35 @@ function createBaseNextConfig(options = {}) {
1392
1393
  ]
1393
1394
  }
1394
1395
  ];
1395
- if (options.allowIframeFrom && options.allowIframeFrom.length > 0) {
1396
- const frameAncestors = options.allowIframeFrom.includes("*") ? "*" : `'self' ${options.allowIframeFrom.join(" ")}`;
1397
- headers.push({
1398
- source: "/:path*",
1399
- headers: [
1400
- // Content-Security-Policy frame-ancestors directive
1401
- { key: "Content-Security-Policy", value: `frame-ancestors ${frameAncestors}` },
1402
- // X-Frame-Options for older browsers (ALLOW-FROM is deprecated, use CSP instead)
1403
- // Only set SAMEORIGIN if allowing all, otherwise browsers will use CSP
1404
- ...options.allowIframeFrom.includes("*") ? [] : [{ key: "X-Frame-Options", value: "SAMEORIGIN" }]
1405
- ]
1406
- });
1407
- }
1396
+ const hasIframeAllowlist = !!options.allowIframeFrom && options.allowIframeFrom.length > 0;
1397
+ const frameAncestors = hasIframeAllowlist ? options.allowIframeFrom.includes("*") ? "*" : `'self' ${options.allowIframeFrom.join(" ")}` : "'none'";
1398
+ const connectSrc = isDev ? "connect-src 'self' http: https: ws: wss:" : "connect-src 'self' https: wss:";
1399
+ const imgSrc = isDev ? "img-src 'self' data: blob: http: https:" : "img-src 'self' data: blob: https:";
1400
+ const cspDirectives = [
1401
+ "default-src 'self'",
1402
+ // Next.js requires inline + eval for its runtime/HMR. Tighten to nonces later.
1403
+ "script-src 'self' 'unsafe-inline' 'unsafe-eval'",
1404
+ "style-src 'self' 'unsafe-inline'",
1405
+ imgSrc,
1406
+ "font-src 'self' data:",
1407
+ // API/websocket calls go cross-origin to Django/Centrifugo.
1408
+ connectSrc,
1409
+ "object-src 'none'",
1410
+ "base-uri 'self'",
1411
+ "form-action 'self'",
1412
+ `frame-ancestors ${frameAncestors}`
1413
+ ];
1414
+ headers.push({
1415
+ source: "/:path*",
1416
+ headers: [
1417
+ { key: "X-Content-Type-Options", value: "nosniff" },
1418
+ { key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
1419
+ { key: "Content-Security-Policy", value: cspDirectives.join("; ") },
1420
+ // X-Frame-Options for older browsers. Omitted when a wildcard iframe
1421
+ // allowlist is set (browsers then rely on CSP frame-ancestors).
1422
+ ...hasIframeAllowlist && options.allowIframeFrom.includes("*") ? [] : [{ key: "X-Frame-Options", value: hasIframeAllowlist ? "SAMEORIGIN" : "DENY" }]
1423
+ ]
1424
+ });
1408
1425
  const userHeaders = options.headers ? await options.headers() : [];
1409
1426
  return [...headers, ...userHeaders];
1410
1427
  },
@@ -1506,6 +1523,7 @@ function createBaseNextConfig(options = {}) {
1506
1523
  checkPackages: checkPackages2,
1507
1524
  autoInstall,
1508
1525
  allowIframeFrom,
1526
+ dpop,
1509
1527
  ...nextConfigOptions
1510
1528
  } = options;
1511
1529
  let finalConfig = deepMerge(baseConfig, nextConfigOptions);