From 5d541a06ad766f44fe3dc02cd6a9d87fc7d311c2 Mon Sep 17 00:00:00 2001 From: Steven Date: Thu, 29 Oct 2026 14:16:16 +1100 Subject: [PATCH 1/3] Upgrade Next.js from 16.4.0 to 16.4.3 to clear three security advisories MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Next.js 16.4.0 is affected by three published advisories, all fixed in 16.4.3: - GHSA-2464-fhr4-w9xw (critical): unauthenticated RCE in the Image Optimization API when SVG files are used - GHSA-673q-mjm6-r5xr (critical): unauthenticated RCE in `use cache` functions returning typed arrays - GHSA-p3m5-59mp-jv5r (high): denial of service in the App Router via Server Actions This is a same-major patch bump, so no codemod or migration guide applies. The webapp pins next exactly at 16.4.3, and the lockfile only moves next, @next/env and the @next/swc-* binaries. 16.4.3 was published on 2026-10-23, less than 7 days ago. CLAUDE.md ยง9 allows that exception for a security fix, which this is. Also set `experimental.agentUpgrade: "security"`, which 16.4.3 supports, so coding agents only nudge about Next.js security upgrades. Verified with pnpm lint, pnpm typecheck, the node test suites (webapp, db, ingest) and a full `next build`. --- packages/webapp/next.config.mjs | 4 +- packages/webapp/package.json | 2 +- pnpm-lock.yaml | 88 ++++++++++++++++----------------- 3 files changed, 48 insertions(+), 46 deletions(-) diff --git a/packages/webapp/next.config.mjs b/packages/webapp/next.config.mjs index b9254f8..e8e76a9 100644 --- a/packages/webapp/next.config.mjs +++ b/packages/webapp/next.config.mjs @@ -83,7 +83,9 @@ const nextConfig = { // container and never deletes them: 12 GB in 3 days in production, // on the disk that Postgres shares. The cache now starts empty after // each deploy. - experimental: { isrFlushToDisk: false }, + // `agentUpgrade: "security"` nudges coding agents about Next.js + // security upgrades only, not every stable release. + experimental: { isrFlushToDisk: false, agentUpgrade: "security" }, // /search was called /tree until 2026-10. A bare /tree redirects to // /search. Old /tree?unit=, ?course= and ?aos= links are rewritten diff --git a/packages/webapp/package.json b/packages/webapp/package.json index 8d3544b..0047d4b 100644 --- a/packages/webapp/package.json +++ b/packages/webapp/package.json @@ -33,7 +33,7 @@ "drizzle-orm": "0.45.4", "jose": "^6.2.12", "lucide-react": "^1.53.0", - "next": "16.4.0", + "next": "16.4.3", "next-themes": "^0.4.6", "postgres": "^3.4.9", "posthog-js": "^1.438.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f3bdfbb..911bd8e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -87,7 +87,7 @@ importers: version: 12.12.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(immer@11.1.4)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) better-auth: specifier: ^1.7.7 - version: 1.7.7(@opentelemetry/api@1.9.1)(drizzle-kit@0.31.11)(drizzle-orm@0.45.4(@opentelemetry/api@1.9.1)(gel@2.2.0)(kysely@0.29.6)(postgres@3.4.9))(next@16.4.0(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@types/node@24.19.1)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react-dom@19.3.0(react@19.3.0))(react@19.3.0) + version: 1.7.7(@opentelemetry/api@1.9.1)(drizzle-kit@0.31.11)(drizzle-orm@0.45.4(@opentelemetry/api@1.9.1)(gel@2.2.0)(kysely@0.29.6)(postgres@3.4.9))(next@16.4.3(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@types/node@24.19.1)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react-dom@19.3.0(react@19.3.0))(react@19.3.0) boring-avatars: specifier: 2.0.4 version: 2.0.4(react-dom@19.3.0(react@19.3.0))(react@19.3.0) @@ -113,8 +113,8 @@ importers: specifier: ^1.53.0 version: 1.53.0(@types/react@19.3.0)(react@19.3.0) next: - specifier: 16.4.0 - version: 16.4.0(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@types/node@24.19.1)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) + specifier: 16.4.3 + version: 16.4.3(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@types/node@24.19.1)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) next-themes: specifier: ^0.4.6 version: 0.4.6(react-dom@19.3.0(react@19.3.0))(react@19.3.0) @@ -1129,60 +1129,60 @@ packages: '@next/bundle-analyzer@16.4.0': resolution: {integrity: sha512-z4iuh6QGVhPKa+Pq/e6WO1d5jFeVGDtjaVY2PVnguTyGTNY/Tw/ozRAHJoaQUyiKEcREot8iQnvmuIOkD+1wyw==} - '@next/env@16.4.0': - resolution: {integrity: sha512-pyLlT5oafGlX8X7xjgGNXz5dNIDSxREm+qD6i9VLSZiZFopA/XWl8NM/KPmLx/8EG/++c49iid3gNWSQW1+2dQ==} + '@next/env@16.4.3': + resolution: {integrity: sha512-a7tz1QCcWWkwC/rPJcPMJk/UNRu9CFhDd3OHYE5hddbJrs8BqRTalxI6GlKCWEGzRLHN+S/B7a4PAHCtm25aBQ==} '@next/eslint-plugin-next@16.3.8': resolution: {integrity: sha512-eCR9RcLTZrVS+K3+VOhi0xMdUUGNuriCHHhU1ZoABC/QQ/Gp0OWbLKQxV2/9DkmihYdYRcQWFmZXW8M1AMUXYw==} - '@next/swc-darwin-arm64@16.4.0': - resolution: {integrity: sha512-yyZ+9MgViAKLLb5l1gPBGjRwnq3teNLb5Zx6JEw8LzNSTjlsnp+baUHFxTgl7PpjCmKf48VQfyipG1eBflb48Q==} + '@next/swc-darwin-arm64@16.4.3': + resolution: {integrity: sha512-4yq5EXBt+VDtAZwTyfijaFp5D2C5oz3Ikw5a2kiX2K9nw72vrZX2q5UuAlkPtUMO1tQz6JTKpaKWjS7OOefTDQ==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@next/swc-darwin-x64@16.4.0': - resolution: {integrity: sha512-RePEEejEj1Auf5s9qEcG0JbcDP3ceNUS/ofNX8AhxzgIjTVlZIHMatlp16YplcSMpsVo/ZMQHJTjbMYJznjGUg==} + '@next/swc-darwin-x64@16.4.3': + resolution: {integrity: sha512-1Rvkn/LUhnEF8WxnRkOrqGhbwXui1bkWFxONKIbN+r7C+9KHDSZ+iUoj4CzymVMWUyZIyx9szcb9+kbKhD500A==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@next/swc-linux-arm64-gnu@16.4.0': - resolution: {integrity: sha512-HhsyCqd+NpmvDDVY/LqkAN24f+QVvXh3ie+PfcwKinWL2Mu826JymQxJXGjAIfmJ4VlHjp4Pe/nZtXtUoEMAqw==} + '@next/swc-linux-arm64-gnu@16.4.3': + resolution: {integrity: sha512-dCKpXErGg3abUM6t5JTJV/ottVBTStJB5lRJTbgdUdQaQ+jNHiK56Hsc0YAEg/rinSh42hYG3HWiYmEMdilnxg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] libc: [glibc] - '@next/swc-linux-arm64-musl@16.4.0': - resolution: {integrity: sha512-sN9Pf+Zh+RpZAOmbR7d8KAA8T61CVhawtbEJ2Bk6QrRzx6Z/6haaF4UiwBzytM96GRRRWZh9/WALIEO3npMamA==} + '@next/swc-linux-arm64-musl@16.4.3': + resolution: {integrity: sha512-WwYbhWkhYBvBtib1K4W0kL8MW2snBD2A5b3SN4vPpqalZbZs5+bQ1yzAdepVoOqrFfEW3dmREy4Zs1MrWcrFbw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] libc: [musl] - '@next/swc-linux-x64-gnu@16.4.0': - resolution: {integrity: sha512-D5M5xLWQVd1CvR+L7BickM1weRfmM41x3L1NWcnKY/oyJJqr1qwaCgmu3dMAhLBOMdZwSuDCRV6DJRRpiUkbDQ==} + '@next/swc-linux-x64-gnu@16.4.3': + resolution: {integrity: sha512-QymWvfUwDEC5/lpJOsxX5VAbyoR2D13+2Jsjz4be7gT4LPqEgdpkeLIR/HFfCLgKzoMhpJbp6I/JnogyNZHcaA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] libc: [glibc] - '@next/swc-linux-x64-musl@16.4.0': - resolution: {integrity: sha512-8d5WLS6a9YalqpOgXdlbvonFu+sIgjA+CO6tGzetrpPVcUL56foa7B7664h41rAlbLi5fJwK4vWji3zf4QDy+w==} + '@next/swc-linux-x64-musl@16.4.3': + resolution: {integrity: sha512-Vmjd9D7DpA4Vv3cUlr82QWgJD/BQZmns801vtC5VWHg6jCIA4a5vMKpQnfrM2pyCrp2eV0ENdUg4PlN3bAc2Yw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] libc: [musl] - '@next/swc-win32-arm64-msvc@16.4.0': - resolution: {integrity: sha512-sQuOIiW3snb48UwrBcDMvFA/u03qtw2LeuSIxFIQ9AJuW9UG/4Ddsfo+eV73PnZwBhyncMtsKVh7PrzMol2/fA==} + '@next/swc-win32-arm64-msvc@16.4.3': + resolution: {integrity: sha512-lG1pPdPLk7gTyGb00n5JIMLdBJgjb+UEhSdZvFbhUMg0DyC6z4B7RnpSKF9VCd4imkLObVbXOFA0kFCpw7yFow==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@next/swc-win32-x64-msvc@16.4.0': - resolution: {integrity: sha512-6/33lCuxpdHUy27RegyGM/5uTpF7YZd3UR4D0gpkOA/tWY5YR7msbQjqPgCZmSmxrrTLGCXYA4A8vT2H5DPGTQ==} + '@next/swc-win32-x64-msvc@16.4.3': + resolution: {integrity: sha512-NmQC9RkJdnPXRaMxtkCzobB3teVjqF33oSOnwDfi+XYGWWYpt+xlGK3mtktSKLbdjwaWbJu2OkkMiRFGUUncKg==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -2948,8 +2948,8 @@ packages: react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc - next@16.4.0: - resolution: {integrity: sha512-N2C+9q89Z/oLrsepSRS4jRMtwmUZAoZuAHU0e9T6+sAB5DdhADmrnOW+zjYijlrYFmVfmQhGVGy9eMFoB7UgKg==} + next@16.4.3: + resolution: {integrity: sha512-OqxKTGi9u80FydydsMNYNjnuje9Au+kvHlt2GZ//ftdpDHm1dxKqAaENoOOTX5wKzn+9vaKUNIfZeyZUuLhCnA==} engines: {node: '>=20.9.0'} hasBin: true peerDependencies: @@ -4326,7 +4326,7 @@ snapshots: - bufferutil - utf-8-validate - '@next/env@16.4.0': {} + '@next/env@16.4.3': {} '@next/eslint-plugin-next@16.3.8(eslint@9.39.5(jiti@2.7.0))': dependencies: @@ -4335,28 +4335,28 @@ snapshots: transitivePeerDependencies: - eslint - '@next/swc-darwin-arm64@16.4.0': + '@next/swc-darwin-arm64@16.4.3': optional: true - '@next/swc-darwin-x64@16.4.0': + '@next/swc-darwin-x64@16.4.3': optional: true - '@next/swc-linux-arm64-gnu@16.4.0': + '@next/swc-linux-arm64-gnu@16.4.3': optional: true - '@next/swc-linux-arm64-musl@16.4.0': + '@next/swc-linux-arm64-musl@16.4.3': optional: true - '@next/swc-linux-x64-gnu@16.4.0': + '@next/swc-linux-x64-gnu@16.4.3': optional: true - '@next/swc-linux-x64-musl@16.4.0': + '@next/swc-linux-x64-musl@16.4.3': optional: true - '@next/swc-win32-arm64-msvc@16.4.0': + '@next/swc-win32-arm64-msvc@16.4.3': optional: true - '@next/swc-win32-x64-msvc@16.4.0': + '@next/swc-win32-x64-msvc@16.4.3': optional: true '@noble/ciphers@2.4.0': {} @@ -4963,7 +4963,7 @@ snapshots: baseline-browser-mapping@2.11.28: {} - better-auth@1.7.7(@opentelemetry/api@1.9.1)(drizzle-kit@0.31.11)(drizzle-orm@0.45.4(@opentelemetry/api@1.9.1)(gel@2.2.0)(kysely@0.29.6)(postgres@3.4.9))(next@16.4.0(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@types/node@24.19.1)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react-dom@19.3.0(react@19.3.0))(react@19.3.0): + better-auth@1.7.7(@opentelemetry/api@1.9.1)(drizzle-kit@0.31.11)(drizzle-orm@0.45.4(@opentelemetry/api@1.9.1)(gel@2.2.0)(kysely@0.29.6)(postgres@3.4.9))(next@16.4.3(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@types/node@24.19.1)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react-dom@19.3.0(react@19.3.0))(react@19.3.0): dependencies: '@better-auth/core': 1.7.7(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.6.5))(jose@6.2.12)(kysely@0.29.6)(nanostores@1.5.5) '@better-auth/drizzle-adapter': 1.7.7(@better-auth/core@1.7.7(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.6.5))(jose@6.2.12)(kysely@0.29.6)(nanostores@1.5.5))(@better-auth/utils@0.4.2)(drizzle-orm@0.45.4(@opentelemetry/api@1.9.1)(gel@2.2.0)(kysely@0.29.6)(postgres@3.4.9)) @@ -4985,7 +4985,7 @@ snapshots: optionalDependencies: drizzle-kit: 0.31.11 drizzle-orm: 0.45.4(@opentelemetry/api@1.9.1)(gel@2.2.0)(kysely@0.29.6)(postgres@3.4.9) - next: 16.4.0(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@types/node@24.19.1)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) + next: 16.4.3(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@types/node@24.19.1)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) react: 19.3.0 react-dom: 19.3.0(react@19.3.0) transitivePeerDependencies: @@ -6125,9 +6125,9 @@ snapshots: react: 19.3.0 react-dom: 19.3.0(react@19.3.0) - next@16.4.0(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@types/node@24.19.1)(react-dom@19.3.0(react@19.3.0))(react@19.3.0): + next@16.4.3(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@types/node@24.19.1)(react-dom@19.3.0(react@19.3.0))(react@19.3.0): dependencies: - '@next/env': 16.4.0 + '@next/env': 16.4.3 '@swc/helpers': 0.5.23 baseline-browser-mapping: 2.11.28 caniuse-lite: 1.0.30001815 @@ -6136,14 +6136,14 @@ snapshots: react-dom: 19.3.0(react@19.3.0) styled-jsx: 5.1.6(@babel/core@7.29.7)(react@19.3.0) optionalDependencies: - '@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 + '@next/swc-darwin-arm64': 16.4.3 + '@next/swc-darwin-x64': 16.4.3 + '@next/swc-linux-arm64-gnu': 16.4.3 + '@next/swc-linux-arm64-musl': 16.4.3 + '@next/swc-linux-x64-gnu': 16.4.3 + '@next/swc-linux-x64-musl': 16.4.3 + '@next/swc-win32-arm64-msvc': 16.4.3 + '@next/swc-win32-x64-msvc': 16.4.3 '@opentelemetry/api': 1.9.1 sharp: 0.35.5(@types/node@24.19.1) transitivePeerDependencies: From 7935b8853efbf384b06dc87ffa7029c14ec3ae05 Mon Sep 17 00:00:00 2001 From: Steven Date: Thu, 29 Oct 2026 14:56:48 +1100 Subject: [PATCH 2/3] Track page views with Google Analytics Load gtag.js through @next/third-parties' GoogleAnalytics component in the root layout, so every page reports a page view. The measurement ID is public (it ships in every page), so it lives in code, not .env. It loads in dev as well as production; filter localhost out by hostname in the GA reports. The report-only CSP now allows gtag.js from www.googletagmanager.com and hits to *.google-analytics.com and *.analytics.google.com. --- packages/webapp/app/layout.tsx | 7 +++++++ packages/webapp/next.config.mjs | 8 +++++--- packages/webapp/package.json | 1 + pnpm-lock.yaml | 20 ++++++++++++++++++++ 4 files changed, 33 insertions(+), 3 deletions(-) diff --git a/packages/webapp/app/layout.tsx b/packages/webapp/app/layout.tsx index cdf2192..c640761 100644 --- a/packages/webapp/app/layout.tsx +++ b/packages/webapp/app/layout.tsx @@ -1,5 +1,6 @@ import type { Metadata, Viewport } from "next" import { Poppins } from "next/font/google" +import { GoogleAnalytics } from "@next/third-parties/google" import "./globals.css" import { PostHogIdentify } from "@/components/posthog-identify" @@ -16,6 +17,9 @@ const poppins = Poppins({ variable: "--font-sans", }) +// Public by design (it ships in every page), so it lives in code, not .env. +const GA_MEASUREMENT_ID = "G-6QWVWW0NLG" + const SITE_DESCRIPTION = "Plan your Monash degree: drag units into semesters, check prerequisites and track your WAM. Read student reviews and see requisite maps for every Monash unit and course." @@ -128,6 +132,9 @@ export default function RootLayout({ mobileOffset={{ bottom: 80 }} /> + {/* Page views only, Loads in dev too; filter + localhost out by hostname in the GA reports. */} +