From 3518cecb731711e6dceec18a944a0e8c5f35a21e Mon Sep 17 00:00:00 2001 From: abhi1693 Date: Thu, 8 Oct 2026 10:51:47 +0530 Subject: [PATCH 1/3] Upgrade Next.js and enable 16.4 compiler and tooling features --- apps/admin/AGENTS.md | 9 + apps/admin/README.md | 24 ++ apps/admin/next.config.ts | 9 + apps/admin/package.json | 7 +- apps/admin/tests/browser/overview.mjs | 2 + apps/web/AGENTS.md | 9 + apps/web/README.md | 24 ++ apps/web/next.config.ts | 9 + apps/web/package.json | 7 +- apps/web/tests/browser/feed.mjs | 2 + package-lock.json | 580 ++++++++++++++++++++------ package.json | 7 +- packages/telemetry/package.json | 2 +- scripts/testing/nonce-csp.mjs | 40 ++ 14 files changed, 595 insertions(+), 136 deletions(-) create mode 100644 apps/admin/AGENTS.md create mode 100644 apps/web/AGENTS.md create mode 100644 scripts/testing/nonce-csp.mjs diff --git a/apps/admin/AGENTS.md b/apps/admin/AGENTS.md new file mode 100644 index 000000000..af3b7c841 --- /dev/null +++ b/apps/admin/AGENTS.md @@ -0,0 +1,9 @@ + + +## This is NOT the Next.js you know + +This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices. + +This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean. + + diff --git a/apps/admin/README.md b/apps/admin/README.md index 9de9102b5..d591cc235 100644 --- a/apps/admin/README.md +++ b/apps/admin/README.md @@ -88,3 +88,27 @@ Validate the page-kind form with `npm run admin:build` followed by legacy defaults, saved decisions, and desktop/mobile layout. Screenshots: [desktop](screenshots/article-page-kind-desktop.png), [mobile](screenshots/article-page-kind-mobile.png). + +## Next.js 16.4 tooling + +The app uses Next.js 16.4 and React 19.3. The Rust React Compiler optimizes +component rendering with Turbopack. Cache garbage collection, lazy client dynamic +imports, and worker-thread plugin execution are enabled for development/builds. +Next.js may fall back to child processes on Node.js versions affected by its +worker-thread compatibility checks. Upgrade reminders use the `latest` policy. + +Run `npm run admin:analyze` from the repository root for the interactive Turbopack +bundle analyzer, or `npm run admin:analyze -- --output` for an offline report under +`apps/admin/.next/`. Analysis does not replace a production build. Use the analyzer +to compare route sizes and identify large dependencies before making changes. +Save a named capture with `npm run admin:analyze -- --output --snapshot baseline` +and compare subsequent captures in the analyzer. + +Cache Components and Partial Prefetching remain disabled: both apps use a fresh +script nonce per response, and Next.js static shells are incompatible with this +[CSP model](https://nextjs.org/docs/app/guides/content-security-policy). +Consequently, `ensureStatic`, `navigation()`, and `prefetch()` are not applied to +these request-rendered routes. Adopting them requires a compatible CSP design +and verification of runtime settings, authentication, and personalized feeds. + +Validate with `npm run admin:lint`, `npm run admin:test`, and `npm run admin:build`. diff --git a/apps/admin/next.config.ts b/apps/admin/next.config.ts index e637db963..754d08b25 100644 --- a/apps/admin/next.config.ts +++ b/apps/admin/next.config.ts @@ -5,6 +5,15 @@ import path from "node:path"; const config: NextConfig = { env: { DEVFEED_BUILD_VERSION: version }, output: "standalone", + // Nonce-based CSP requires request-time rendering; keep Cache Components disabled. + reactCompiler: true, + experimental: { + agentUpgrade: "latest", + turbopackRustReactCompiler: true, + turbopackGc: true, + turbopackLazyDynamicImports: true, + turbopackPluginRuntimeStrategy: "workerThreads", + }, productionBrowserSourceMaps: true, serverExternalPackages: ["@pyroscope/nodejs", "@prometheus-io/client", "@opentelemetry/sdk-node"], outputFileTracingRoot: path.join(import.meta.dirname, "../.."), diff --git a/apps/admin/package.json b/apps/admin/package.json index 1b87dbb16..d4e465f3a 100644 --- a/apps/admin/package.json +++ b/apps/admin/package.json @@ -8,7 +8,8 @@ "start": "next start --port 3001", "lint": "eslint . && tsc --noEmit", "test": "vitest run", - "generate": "orval --config orval.config.ts" + "generate": "orval --config orval.config.ts", + "analyze": "next analyze" }, "dependencies": { "@chimely/client": "0.2.2", @@ -22,7 +23,7 @@ "cmdk": "^1.1.1", "cn": "^0.4.0", "lucide-react": "^1.49.0", - "next": "16.3.8", + "next": "16.4.0", "radix-ui": "^1.6.7", "react": "19.3.0", "react-dom": "19.3.0", @@ -41,7 +42,7 @@ "@types/react-dom": "^19.3.0", "@vitest/coverage-v8": "5.0.3", "eslint": "^9.39.5", - "eslint-config-next": "16.3.8", + "eslint-config-next": "16.4.0", "jsdom": "^30.1.1", "orval": "8.38.0", "shadcn": "4.21.0", diff --git a/apps/admin/tests/browser/overview.mjs b/apps/admin/tests/browser/overview.mjs index c88a446fa..8d4d75c72 100644 --- a/apps/admin/tests/browser/overview.mjs +++ b/apps/admin/tests/browser/overview.mjs @@ -1,3 +1,4 @@ +import { checkNonceCsp } from "../../../../scripts/testing/nonce-csp.mjs"; import assert from "node:assert/strict"; import { createServer } from "node:http"; import { spawn } from "node:child_process"; @@ -215,6 +216,7 @@ try { }); }); await context.addCookies([{ name: "devfeed_admin_session", value: "fixture", url: origin }]); + await checkNonceCsp(context, origin); const page = await context.newPage(); page.setDefaultTimeout(15000); const errors = []; diff --git a/apps/web/AGENTS.md b/apps/web/AGENTS.md new file mode 100644 index 000000000..af3b7c841 --- /dev/null +++ b/apps/web/AGENTS.md @@ -0,0 +1,9 @@ + + +## This is NOT the Next.js you know + +This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices. + +This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean. + + diff --git a/apps/web/README.md b/apps/web/README.md index 201f36597..0bedef70f 100644 --- a/apps/web/README.md +++ b/apps/web/README.md @@ -282,3 +282,27 @@ browser checks on the website and built Chrome and Edge extensions. The matrix s long titles, unbroken topic names, long publisher names, and large engagement counts. It checks column adaptation, overflow, title space, footer fit, 44px bookmark targets, and the container-query boundary. Screenshots remain in ignored reports/build output. + +## Next.js 16.4 tooling + +The app uses Next.js 16.4 and React 19.3. The Rust React Compiler optimizes +component rendering with Turbopack. Cache garbage collection, lazy client dynamic +imports, and worker-thread plugin execution are enabled for development/builds. +Next.js may fall back to child processes on Node.js versions affected by its +worker-thread compatibility checks. Upgrade reminders use the `latest` policy. + +Run `npm run web:analyze` from the repository root for the interactive Turbopack +bundle analyzer, or `npm run web:analyze -- --output` for an offline report under +`apps/web/.next/`. Analysis does not replace a production build. Use the analyzer +to compare route sizes and identify large dependencies before making changes. +Save a named capture with `npm run web:analyze -- --output --snapshot baseline` +and compare subsequent captures in the analyzer. + +Cache Components and Partial Prefetching remain disabled: both apps use a fresh +script nonce per response, and Next.js static shells are incompatible with this +[CSP model](https://nextjs.org/docs/app/guides/content-security-policy). +Consequently, `ensureStatic`, `navigation()`, and `prefetch()` are not applied to +these request-rendered routes. Adopting them requires a compatible CSP design +and verification of runtime settings, authentication, and personalized feeds. + +Validate with `npm run web:lint`, `npm run web:test`, and `npm run web:build`. diff --git a/apps/web/next.config.ts b/apps/web/next.config.ts index 9911b04af..53874f866 100644 --- a/apps/web/next.config.ts +++ b/apps/web/next.config.ts @@ -5,6 +5,15 @@ import { withDualmark } from "@dualmark/nextjs"; const config: NextConfig = { output: "standalone", + // Nonce-based CSP requires request-time rendering; keep Cache Components disabled. + reactCompiler: true, + experimental: { + agentUpgrade: "latest", + turbopackRustReactCompiler: true, + turbopackGc: true, + turbopackLazyDynamicImports: true, + turbopackPluginRuntimeStrategy: "workerThreads", + }, productionBrowserSourceMaps: true, serverExternalPackages: ["@pyroscope/nodejs", "@prometheus-io/client", "@opentelemetry/sdk-node"], outputFileTracingRoot: path.join(import.meta.dirname, "../.."), diff --git a/apps/web/package.json b/apps/web/package.json index c1b1138b2..770ae0e44 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -7,7 +7,8 @@ "build": "next build", "start": "next start --hostname 0.0.0.0 --port 3000", "lint": "eslint . && tsc --noEmit", - "test": "vitest run" + "test": "vitest run", + "analyze": "next analyze" }, "dependencies": { "@chimely/client": "0.2.2", @@ -19,7 +20,7 @@ "@microsoft/clarity": "1.0.2", "@tanstack/react-query": "5.104.1", "lucide-react": "^1.49.0", - "next": "16.3.8", + "next": "16.4.0", "radix-ui": "1.6.7", "react": "19.3.0", "react-dom": "19.3.0", @@ -36,7 +37,7 @@ "@types/react-dom": "^19.3.0", "@vitest/coverage-v8": "5.0.3", "eslint": "^9.39.5", - "eslint-config-next": "16.3.8", + "eslint-config-next": "16.4.0", "jsdom": "^30.0.1", "libxml2-wasm": "0.7.2", "typescript": "^7.0.2", diff --git a/apps/web/tests/browser/feed.mjs b/apps/web/tests/browser/feed.mjs index 52968f979..93bfd24dd 100644 --- a/apps/web/tests/browser/feed.mjs +++ b/apps/web/tests/browser/feed.mjs @@ -1,3 +1,4 @@ +import { checkNonceCsp } from "../../../../scripts/testing/nonce-csp.mjs"; import { checkArticleGrid } from "../../../../scripts/testing/article-grid.mjs"; import { checkArticleViews, compactListArticle, compactLoadingGate } from "./article-views.mjs"; import { checkCompactLoading } from "../../../../scripts/testing/compact-loading.mjs"; @@ -486,6 +487,7 @@ try { context.on("request", (request) => { if (request.headers()["next-router-prefetch"] === "1") prefetchedRoutes.push(request.url()); }); + await checkNonceCsp(context, `${origin}/latest`); const page = await context.newPage(); await page.addInitScript(() => { window.__cspViolations = []; diff --git a/package-lock.json b/package-lock.json index d54685954..2581418e4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -48,7 +48,7 @@ "cmdk": "^1.1.1", "cn": "^0.4.0", "lucide-react": "^1.49.0", - "next": "16.3.8", + "next": "16.4.0", "radix-ui": "^1.6.7", "react": "19.3.0", "react-dom": "19.3.0", @@ -67,7 +67,7 @@ "@types/react-dom": "^19.3.0", "@vitest/coverage-v8": "5.0.3", "eslint": "^9.39.5", - "eslint-config-next": "16.3.8", + "eslint-config-next": "16.4.0", "jsdom": "^30.1.1", "orval": "8.38.0", "shadcn": "4.21.0", @@ -77,6 +77,89 @@ "vitest": "^5.0.3" } }, + "apps/admin/node_modules/@next/eslint-plugin-next": { + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.4.0.tgz", + "integrity": "sha512-ffeflB/dL8RrvWejQximw01pOpKHN692Y3FjDB82DCd0YVvXvBD6vY7mcZp27yvSVFVM6wA73xVodEKF4spizA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "4.9.1", + "fast-glob": "3.3.1" + } + }, + "apps/admin/node_modules/@next/eslint-plugin-next/node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "apps/admin/node_modules/eslint-config-next": { + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.4.0.tgz", + "integrity": "sha512-2RHLYswzc+hNRktWAjIL6XAvwb1cKcjLawJ5KWywAxn/JivNHtG42ZigL1yobVXDqsimuuL2F9jcEcnHFzbppQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@next/eslint-plugin-next": "16.4.0", + "eslint-import-resolver-node": "^0.3.6", + "eslint-import-resolver-typescript": "^3.5.2", + "eslint-plugin-import": "^2.32.0", + "eslint-plugin-jsx-a11y": "^6.10.0", + "eslint-plugin-react": "^7.37.0", + "eslint-plugin-react-hooks": "^7.1.0", + "globals": "16.4.0", + "typescript-eslint": "^8.56.0" + }, + "peerDependencies": { + "eslint": ">=9.0.0", + "typescript": ">=3.3.1" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "apps/admin/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "apps/admin/node_modules/globals": { + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz", + "integrity": "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "apps/admin/node_modules/typescript": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz", @@ -125,7 +208,7 @@ "@microsoft/clarity": "1.0.2", "@tanstack/react-query": "5.104.1", "lucide-react": "^1.49.0", - "next": "16.3.8", + "next": "16.4.0", "radix-ui": "1.6.7", "react": "19.3.0", "react-dom": "19.3.0", @@ -142,13 +225,96 @@ "@types/react-dom": "^19.3.0", "@vitest/coverage-v8": "5.0.3", "eslint": "^9.39.5", - "eslint-config-next": "16.3.8", + "eslint-config-next": "16.4.0", "jsdom": "^30.0.1", "libxml2-wasm": "0.7.2", "typescript": "^7.0.2", "vitest": "^5.0.3" } }, + "apps/web/node_modules/@next/eslint-plugin-next": { + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.4.0.tgz", + "integrity": "sha512-ffeflB/dL8RrvWejQximw01pOpKHN692Y3FjDB82DCd0YVvXvBD6vY7mcZp27yvSVFVM6wA73xVodEKF4spizA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "4.9.1", + "fast-glob": "3.3.1" + } + }, + "apps/web/node_modules/@next/eslint-plugin-next/node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "apps/web/node_modules/eslint-config-next": { + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.4.0.tgz", + "integrity": "sha512-2RHLYswzc+hNRktWAjIL6XAvwb1cKcjLawJ5KWywAxn/JivNHtG42ZigL1yobVXDqsimuuL2F9jcEcnHFzbppQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@next/eslint-plugin-next": "16.4.0", + "eslint-import-resolver-node": "^0.3.6", + "eslint-import-resolver-typescript": "^3.5.2", + "eslint-plugin-import": "^2.32.0", + "eslint-plugin-jsx-a11y": "^6.10.0", + "eslint-plugin-react": "^7.37.0", + "eslint-plugin-react-hooks": "^7.1.0", + "globals": "16.4.0", + "typescript-eslint": "^8.56.0" + }, + "peerDependencies": { + "eslint": ">=9.0.0", + "typescript": ">=3.3.1" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "apps/web/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "apps/web/node_modules/globals": { + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz", + "integrity": "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "apps/web/node_modules/typescript": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz", @@ -3760,58 +3926,15 @@ } }, "node_modules/@next/env": { - "version": "16.3.8", - "resolved": "https://registry.npmjs.org/@next/env/-/env-16.3.8.tgz", - "integrity": "sha512-Al9zqHVV7TJv0eFuOU4U7Lvv74PTih4Ch63sk2xCIpSTkE3udFnaOcnzP2lQVymiL7yS9Cj2iClUXlR3EQ5sEw==", + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.4.0.tgz", + "integrity": "sha512-pyLlT5oafGlX8X7xjgGNXz5dNIDSxREm+qD6i9VLSZiZFopA/XWl8NM/KPmLx/8EG/++c49iid3gNWSQW1+2dQ==", "license": "MIT" }, - "node_modules/@next/eslint-plugin-next": { - "version": "16.3.8", - "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.3.8.tgz", - "integrity": "sha512-eCR9RcLTZrVS+K3+VOhi0xMdUUGNuriCHHhU1ZoABC/QQ/Gp0OWbLKQxV2/9DkmihYdYRcQWFmZXW8M1AMUXYw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "4.9.1", - "fast-glob": "3.3.1" - } - }, - "node_modules/@next/eslint-plugin-next/node_modules/@eslint-community/eslint-utils": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", - "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@next/eslint-plugin-next/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, "node_modules/@next/swc-darwin-arm64": { - "version": "16.3.8", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.3.8.tgz", - "integrity": "sha512-2JPRMh2nmQG5CiL7cXGL9AGwnPWJQ//cTtAUCT+w511QHk79SYz3LGv/pc5X643B/WEO0rvu3Yww0hqwt3kgeA==", + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.4.0.tgz", + "integrity": "sha512-yyZ+9MgViAKLLb5l1gPBGjRwnq3teNLb5Zx6JEw8LzNSTjlsnp+baUHFxTgl7PpjCmKf48VQfyipG1eBflb48Q==", "cpu": [ "arm64" ], @@ -3825,9 +3948,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "16.3.8", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.3.8.tgz", - "integrity": "sha512-GZtCCOBKJ4leVIT/Th0llWKhD1ca92lzbQiS5R5ON9QkoiFnilFsebDae1JU2a3HWoKMEmEZWGs1AGLavVM72Q==", + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.4.0.tgz", + "integrity": "sha512-RePEEejEj1Auf5s9qEcG0JbcDP3ceNUS/ofNX8AhxzgIjTVlZIHMatlp16YplcSMpsVo/ZMQHJTjbMYJznjGUg==", "cpu": [ "x64" ], @@ -3841,9 +3964,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "16.3.8", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.3.8.tgz", - "integrity": "sha512-O659ygeQYqneJ1fBKMpFxIFqYkYswu8IAS1OCKK/4f3ZgJJm1dRz4fVJZRi/kLLWjnBKnebOePA4WNv+sV1pVA==", + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.4.0.tgz", + "integrity": "sha512-HhsyCqd+NpmvDDVY/LqkAN24f+QVvXh3ie+PfcwKinWL2Mu826JymQxJXGjAIfmJ4VlHjp4Pe/nZtXtUoEMAqw==", "cpu": [ "arm64" ], @@ -3857,9 +3980,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "16.3.8", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.3.8.tgz", - "integrity": "sha512-dSjKSyWpzxoO1d3DIZZcP4XJcNaKeLmxQMFOiYl5vuBRMmweIqnAhty8tAmRsvTss779cK1FtYnDMj40e4TQlg==", + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.4.0.tgz", + "integrity": "sha512-sN9Pf+Zh+RpZAOmbR7d8KAA8T61CVhawtbEJ2Bk6QrRzx6Z/6haaF4UiwBzytM96GRRRWZh9/WALIEO3npMamA==", "cpu": [ "arm64" ], @@ -3873,9 +3996,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "16.3.8", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.3.8.tgz", - "integrity": "sha512-lbqOuz3RPRcv+o9msNsJw5x4+Y1ZwPTs6vmL6DCf7i0fZfvng/F59wyeDwqHIvV0mK//RBy/jJkZ+nCKsSMXjQ==", + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.4.0.tgz", + "integrity": "sha512-D5M5xLWQVd1CvR+L7BickM1weRfmM41x3L1NWcnKY/oyJJqr1qwaCgmu3dMAhLBOMdZwSuDCRV6DJRRpiUkbDQ==", "cpu": [ "x64" ], @@ -3889,9 +4012,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "16.3.8", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.3.8.tgz", - "integrity": "sha512-+316WswI8ScVgZeUd+1KGaXkHhaYQzCjvH/05TZSpJ8zBizb1a4G7DtO7F12jcBIqMOtsz9ji1t48fmKtzqsGA==", + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.4.0.tgz", + "integrity": "sha512-8d5WLS6a9YalqpOgXdlbvonFu+sIgjA+CO6tGzetrpPVcUL56foa7B7664h41rAlbLi5fJwK4vWji3zf4QDy+w==", "cpu": [ "x64" ], @@ -3905,9 +4028,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "16.3.8", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.3.8.tgz", - "integrity": "sha512-ji0gd4kMYUxO+1fJBIbiBVRCjzG/lloiyCccnlebvb1ZJ5qXCPZqYg4Jl1DrrixnWNMKylzgpmMWx0yNDYXlzw==", + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.4.0.tgz", + "integrity": "sha512-sQuOIiW3snb48UwrBcDMvFA/u03qtw2LeuSIxFIQ9AJuW9UG/4Ddsfo+eV73PnZwBhyncMtsKVh7PrzMol2/fA==", "cpu": [ "arm64" ], @@ -3921,9 +4044,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "16.3.8", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.3.8.tgz", - "integrity": "sha512-WcTlaKt/TWkh5kUjdJcUmB1XgZ+1c6fz4Y9fDHL73YNSdGaUWjceeWrrlwF0nv19iABYWC4iAq1oX1w4Bn0vfg==", + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.4.0.tgz", + "integrity": "sha512-6/33lCuxpdHUy27RegyGM/5uTpF7YZd3UR4D0gpkOA/tWY5YR7msbQjqPgCZmSmxrrTLGCXYA4A8vT2H5DPGTQ==", "cpu": [ "x64" ], @@ -12361,46 +12484,6 @@ } } }, - "node_modules/eslint-config-next": { - "version": "16.3.8", - "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.3.8.tgz", - "integrity": "sha512-81vovwMe6NGnoFsl0KUJWzlS+y239i3fdsxs49gSEX3pDQW2cuKy+fxUPXNkQqqx6OCPheYPtBlSufc9aZ0k+w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@next/eslint-plugin-next": "16.3.8", - "eslint-import-resolver-node": "^0.3.6", - "eslint-import-resolver-typescript": "^3.5.2", - "eslint-plugin-import": "^2.32.0", - "eslint-plugin-jsx-a11y": "^6.10.0", - "eslint-plugin-react": "^7.37.0", - "eslint-plugin-react-hooks": "^7.0.0", - "globals": "16.4.0", - "typescript-eslint": "^8.46.0" - }, - "peerDependencies": { - "eslint": ">=9.0.0", - "typescript": ">=3.3.1" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/eslint-config-next/node_modules/globals": { - "version": "16.4.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz", - "integrity": "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/eslint-config-prettier": { "version": "10.1.8", "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", @@ -17239,12 +17322,12 @@ } }, "node_modules/next": { - "version": "16.3.8", - "resolved": "https://registry.npmjs.org/next/-/next-16.3.8.tgz", - "integrity": "sha512-U7QEZaTini6wKrb8A8hqLLqYQyCetegKjCpJOyxk642vWoMoU1x5PyZCJFvgYgiptA8xc5j/9xYlZFO7w9Sjmw==", + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/next/-/next-16.4.0.tgz", + "integrity": "sha512-N2C+9q89Z/oLrsepSRS4jRMtwmUZAoZuAHU0e9T6+sAB5DdhADmrnOW+zjYijlrYFmVfmQhGVGy9eMFoB7UgKg==", "license": "MIT", "dependencies": { - "@next/env": "16.3.8", + "@next/env": "16.4.0", "@swc/helpers": "0.5.23", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", @@ -17258,14 +17341,14 @@ "node": ">=20.9.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "16.3.8", - "@next/swc-darwin-x64": "16.3.8", - "@next/swc-linux-arm64-gnu": "16.3.8", - "@next/swc-linux-arm64-musl": "16.3.8", - "@next/swc-linux-x64-gnu": "16.3.8", - "@next/swc-linux-x64-musl": "16.3.8", - "@next/swc-win32-arm64-msvc": "16.3.8", - "@next/swc-win32-x64-msvc": "16.3.8", + "@next/swc-darwin-arm64": "16.4.0", + "@next/swc-darwin-x64": "16.4.0", + "@next/swc-linux-arm64-gnu": "16.4.0", + "@next/swc-linux-arm64-musl": "16.4.0", + "@next/swc-linux-x64-gnu": "16.4.0", + "@next/swc-linux-x64-musl": "16.4.0", + "@next/swc-win32-arm64-msvc": "16.4.0", + "@next/swc-win32-x64-msvc": "16.4.0", "sharp": "^0.35.4" }, "peerDependencies": { @@ -18535,6 +18618,26 @@ "node": ">= 14" } }, + "node_modules/proxy-agent-negotiate": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-agent-negotiate/-/proxy-agent-negotiate-1.1.0.tgz", + "integrity": "sha512-N8IBcM3UgCVzz2L2Lqv8DVntDnnC8/hiV4nEDUPkqq72TPUgYWjQc+bdZlBPZK9LzPAvOY//gAt0S0DApoOXWQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">= 20" + }, + "peerDependencies": { + "kerberos": "^2.0.0" + }, + "peerDependenciesMeta": { + "kerberos": { + "optional": true + } + } + }, "node_modules/proxy-agent/node_modules/lru-cache": { "version": "7.18.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", @@ -18609,6 +18712,18 @@ } } }, + "node_modules/puppeteer-core/node_modules/agent-base": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-9.0.0.tgz", + "integrity": "sha512-TQf59BsZnytt8GdJKLPfUZ54g/iaUL2OWDSFCCvMOhsHduDQxO8xC4PNeyIkVcA5KwL2phPSv0douC0fgWzmnA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">= 20" + } + }, "node_modules/puppeteer-core/node_modules/ansi-regex": { "version": "6.4.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.4.0.tgz", @@ -18635,6 +18750,21 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/puppeteer-core/node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/puppeteer-core/node_modules/cliui": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", @@ -18650,6 +18780,38 @@ "node": ">=20" } }, + "node_modules/puppeteer-core/node_modules/data-uri-to-buffer": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-8.0.0.tgz", + "integrity": "sha512-6UHfyCux51b8PTGDgveqtz1tvphBku5DrMKKJbFAZAJOI2zsjDpDoYE1+QGj7FOMS4BdTFNJsJiR3zEB0xH0yQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">= 20" + } + }, + "node_modules/puppeteer-core/node_modules/degenerator": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-7.0.1.tgz", + "integrity": "sha512-ABErK0IefDSyHjlPH7WUEenIAX2rPPnrDcDM+TS3z3+zu9TfyKKi07BQM+8rmxpdE2y1v5fjjdoAS/x4D2U60w==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 20" + }, + "peerDependencies": { + "quickjs-wasi": "^2.2.0" + } + }, "node_modules/puppeteer-core/node_modules/devtools-protocol": { "version": "0.0.1687809", "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1687809.tgz", @@ -18657,6 +18819,161 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/puppeteer-core/node_modules/get-uri": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-8.0.1.tgz", + "integrity": "sha512-/5N/P4Lrh0p/mDwlDRi7Y1+P2o/OyzZI3l6Iz1Ov6XXwwm1y3RlZLuo3gVgML99djrEDtV980bBxSuOeHLk8ww==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "basic-ftp": "^5.3.1", + "data-uri-to-buffer": "8.0.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/puppeteer-core/node_modules/http-proxy-agent": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-9.1.0.tgz", + "integrity": "sha512-2NxoveTT58mjYT4n3RPTEfCZGLMbidoO8XEieXfpSYxu+PQJ1qpx4ypwH6N+uF9twBPIvRRgvkvW5HUTYWENig==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "agent-base": "9.0.0", + "debug": "^4.3.4", + "proxy-agent-negotiate": "1.1.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/puppeteer-core/node_modules/https-proxy-agent": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-9.1.0.tgz", + "integrity": "sha512-ag87y7cJJ9/3+GxFr8Oy4O5faDsGRGnBGsJj/YjOSsSx/5eadKLYTMPlzuR6obgoCDDm0abAAZitXXQkMOPSpA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "agent-base": "9.0.0", + "debug": "^4.3.4", + "proxy-agent-negotiate": "1.1.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/puppeteer-core/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/puppeteer-core/node_modules/pac-proxy-agent": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-9.1.0.tgz", + "integrity": "sha512-1aU+1mpj3DrQPfo3gh+3Gap3G5x+axnMx1P/y0ZF2ch7kb2meyOCAH8K2k9d27ROsTE7TnAerzxqF9aon2jqnA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "agent-base": "9.0.0", + "debug": "^4.3.4", + "get-uri": "8.0.1", + "http-proxy-agent": "9.1.0", + "https-proxy-agent": "9.1.0", + "pac-resolver": "9.0.1", + "quickjs-wasi": "^2.2.0", + "socks-proxy-agent": "10.1.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/puppeteer-core/node_modules/pac-resolver": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-9.0.1.tgz", + "integrity": "sha512-lJbS008tmkj08VhoM8Hzuv/VE5tK9MS0OIQ/7+s0lIF+BYhiQWFYzkSpML7lXs9iBu2jfmzBTLzhe9n6BX+dYw==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "degenerator": "7.0.1", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 20" + }, + "peerDependencies": { + "quickjs-wasi": "^2.2.0" + } + }, + "node_modules/puppeteer-core/node_modules/proxy-agent": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-8.0.2.tgz", + "integrity": "sha512-idLLRewuemWd7GH/BDJzGiB0dWGfT2SQs3jy6NtZtGWU9uPTTSdeC1/cdbqLwgzhfv027daGFuXX426e2Eg20A==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "agent-base": "9.0.0", + "debug": "^4.3.4", + "http-proxy-agent": "9.1.0", + "https-proxy-agent": "9.1.0", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "9.1.0", + "proxy-from-env": "^2.0.0", + "socks-proxy-agent": "10.1.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/puppeteer-core/node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/puppeteer-core/node_modules/socks-proxy-agent": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-10.1.0.tgz", + "integrity": "sha512-WlMj/67cEJ6MDI1OcsnjuYKDNDoyPCCYZ249kuuXPiMDw9F8PXkVaQ7YWu3siTydfQ/4BEZcvGzu+aYvz7dDCQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "agent-base": "9.0.0", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 20" + } + }, "node_modules/puppeteer-core/node_modules/strip-ansi": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", @@ -18813,6 +19130,15 @@ ], "license": "MIT" }, + "node_modules/quickjs-wasi": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/quickjs-wasi/-/quickjs-wasi-2.2.0.tgz", + "integrity": "sha512-zQxXmQMrEoD3S+jQdYsloq4qAuaxKFHZj6hHqOYGwB2iQZH+q9e/lf5zQPXCKOk0WJuAjzRFbO4KwHIp2D05Iw==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, "node_modules/radix-ui": { "version": "1.6.7", "resolved": "https://registry.npmjs.org/radix-ui/-/radix-ui-1.6.7.tgz", @@ -22725,7 +23051,7 @@ "@pyroscope/nodejs": "^0.6.3" }, "peerDependencies": { - "next": "^16.3.8", + "next": "^16.4.0", "react": "^19.3.0" } }, diff --git a/package.json b/package.json index 594df1d54..5ecaa7f50 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,9 @@ "test:dependency-security": "node --test tests/braces.test.cjs", "ci:browser-budgets": "node node_modules/c8/bin/c8.js --all --include=scripts/ci/browser-performance*.mjs --exclude=**/*.test.mjs --reporter=text --reporter=lcov --reporter=json-summary --reports-dir=reports/coverage/browser-budgets npm run ci:browser-measure", "ci:browser-measure": "node --test scripts/ci/browser-performance.test.mjs && node scripts/ci/browser-performance.mjs", - "ci:mutation": "node scripts/ci/mutation.mjs run" + "ci:mutation": "node scripts/ci/mutation.mjs run", + "web:analyze": "npm run analyze --workspace @devfeed/web --", + "admin:analyze": "npm run analyze --workspace @devfeed/admin --" }, "engines": { "node": ">=22.13" @@ -77,7 +79,8 @@ }, "typed-rest-client": { "qs": "6.16.0" - } + }, + "basic-ftp": "6.2.2" }, "devDependencies": { "@axe-core/playwright": "4.13.0", diff --git a/packages/telemetry/package.json b/packages/telemetry/package.json index 0125da8f9..5c7700db9 100644 --- a/packages/telemetry/package.json +++ b/packages/telemetry/package.json @@ -12,7 +12,7 @@ "./session": "./src/session.ts" }, "peerDependencies": { - "next": "^16.3.8", + "next": "^16.4.0", "react": "^19.3.0" }, "dependencies": { diff --git a/scripts/testing/nonce-csp.mjs b/scripts/testing/nonce-csp.mjs new file mode 100644 index 000000000..84ee5ced4 --- /dev/null +++ b/scripts/testing/nonce-csp.mjs @@ -0,0 +1,40 @@ +import assert from "node:assert/strict"; + +// Exercise real production HTML, including framework and inline bootstrap scripts. +export async function checkNonceCsp(context, url) { + const page = await context.newPage(); + await page.addInitScript(() => { + window.__nonceCspViolations = []; + document.addEventListener("securitypolicyviolation", (event) => { + window.__nonceCspViolations.push(`${event.effectiveDirective}: ${event.blockedURI}`); + }); + }); + try { + const nonces = []; + for (let visit = 0; visit < 2; visit++) { + const response = await page.goto(url); + assert.equal(response.status(), 200); + const csp = response.headers()["content-security-policy"]; + const nonce = csp?.match(/script-src [^;]*'nonce-([^']+)'/)?.[1]; + assert.ok(nonce, "HTML response must have a script nonce"); + assert.doesNotMatch(csp, /script-src [^;]*'unsafe-inline'/); + const scripts = await page + .locator("script") + .evaluateAll((elements) => + elements + .filter((element) => + ["", "module", "text/javascript", "application/javascript"].includes(element.type), + ) + .map((element) => ({ src: element.src || "inline", nonce: element.nonce })), + ); + assert.ok(scripts.length > 0, "Page must render executable scripts"); + for (const script of scripts) + assert.equal(script.nonce, nonce, `Script must use this response's nonce: ${script.src}`); + assert.deepEqual(await page.evaluate(() => window.__nonceCspViolations), []); + nonces.push(nonce); + } + assert.notEqual(nonces[0], nonces[1], "Nonce must change between page loads"); + } finally { + await page.close(); + } +} From 227246e72eb19008d877bec239123266b59a44c1 Mon Sep 17 00:00:00 2001 From: abhi1693 Date: Thu, 8 Oct 2026 13:22:47 +0530 Subject: [PATCH 2/3] Classify nonce CSP browser checks as Sonar tests --- sonar-project.properties | 1 + tests/test_sonar_coverage.py | 1 + 2 files changed, 2 insertions(+) diff --git a/sonar-project.properties b/sonar-project.properties index 3052d817d..972e6bdc6 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -29,6 +29,7 @@ sonar.test.inclusions=tests/**,apps/*/tests/**,scripts/ci/**/*.test.mjs,\ scripts/testing/managed-images.mjs,\ scripts/testing/mcp.mjs,\ scripts/testing/must-reads.mjs,\ + scripts/testing/nonce-csp.mjs,\ scripts/testing/onboarding-topics.mjs,\ scripts/testing/preview-background.mjs,\ scripts/testing/profile-editor.mjs,\ diff --git a/tests/test_sonar_coverage.py b/tests/test_sonar_coverage.py index cfa4ee0da..e845496fb 100644 --- a/tests/test_sonar_coverage.py +++ b/tests/test_sonar_coverage.py @@ -26,6 +26,7 @@ ("source", "expected_test"), [ ("scripts/testing/dev-card-promo.mjs", True), + ("scripts/testing/nonce-csp.mjs", True), ("scripts/testing/sign-in.py", True), ("apps/web/tests/date-format.test.ts", True), ("scripts/testing/dast-browser.mjs", False), From 7451938991a996185ffd9a1daba586e7faa52fa3 Mon Sep 17 00:00:00 2001 From: abhi1693 Date: Thu, 8 Oct 2026 13:28:07 +0530 Subject: [PATCH 3/3] Isolate nonce checks from overview request measurements --- apps/admin/README.md | 2 ++ apps/admin/tests/browser/overview.mjs | 8 ++++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/apps/admin/README.md b/apps/admin/README.md index d591cc235..b60a36542 100644 --- a/apps/admin/README.md +++ b/apps/admin/README.md @@ -112,3 +112,5 @@ these request-rendered routes. Adopting them requires a compatible CSP design and verification of runtime settings, authentication, and personalized feeds. Validate with `npm run admin:lint`, `npm run admin:test`, and `npm run admin:build`. +The overview browser suite checks nonce rotation after its overview scenarios so +the extra page loads do not affect panel concurrency or lazy-loading measurements. diff --git a/apps/admin/tests/browser/overview.mjs b/apps/admin/tests/browser/overview.mjs index 8d4d75c72..cb60cd232 100644 --- a/apps/admin/tests/browser/overview.mjs +++ b/apps/admin/tests/browser/overview.mjs @@ -216,7 +216,6 @@ try { }); }); await context.addCookies([{ name: "devfeed_admin_session", value: "fixture", url: origin }]); - await checkNonceCsp(context, origin); const page = await context.newPage(); page.setDefaultTimeout(15000); const errors = []; @@ -436,12 +435,17 @@ try { await page.waitForFunction(() => document.documentElement.scrollWidth <= innerWidth); await workload.screenshot({ path: `${output}/workload-live-mobile.png` }); assert.deepEqual(errors, []); + const maximumConcurrentPanelRequests = maximum; + await page.close(); + mode = "populated"; + // Separate nonce-check navigations must not affect overview request measurements. + await checkNonceCsp(context, origin); await writeFile( `${output}/results.json`, JSON.stringify( { passed: true, - maximumConcurrentPanelRequests: maximum, + maximumConcurrentPanelRequests, initialPanels, scenarios: [ "desktop",