From 41ab934917fe46ad94ed09932483300145c4f41b Mon Sep 17 00:00:00 2001 From: Markus Neusinger <2921697+MarkusNeusinger@users.noreply.github.com> Date: Wed, 2 Sep 2026 23:04:17 +0200 Subject: [PATCH 1/2] Close PYSEC-2026-2132 and pin the Node the frontend is built with MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `pip-audit` over the resolved runtime dependency set (`uv export --no-dev`) reported one advisory: PYSEC-2026-2132 against `click` 8.3.1. `click` is transitive, so the fix is a lock-file bump with no `pyproject.toml` constraint — 8.3.3, the minimal version that clears it. The audit comes back clean afterwards. `click` 8.5.0 exists and is deliberately left to Dependabot: a minor bump of the library behind every console script deserves its own PR and its own CI run, not a ride on a security patch. The lock also picks up uv 0.10.9's marker normalisation on four unrelated packages (httpcore2, plotnine x3, secretstorage) — no version moves, and reverting it by hand would leave the file different from what `uv lock` produces. The frontend half: `app/package.json` asked for `node >=20` while the image that produces the deployed bundle builds on Node 22 and CI tests on Node 24. The only version the manifest still admitted was the one nothing tests and that reached end of life in April 2026. The floor moves to `>=22`, `app/.nvmrc` names 22 for `nvm use` and `setup-node`, and `app/.npmrc` sets `engine-strict=true` so an npm install in `app/` refuses an unsupported runtime at install time instead of failing later inside the build with a message that never names the version. yarn 1, the app's own package manager, checks `engines` itself; the `.npmrc` covers everyone who reaches for npm. Same pin as the sibling repo kurrentschrift, where a Node 20 install failed deep inside the build with "node: bad option". Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01PBQdMbboxo59sSThGSbfke --- CHANGELOG.md | 19 ++++++++++++++++++ app/.npmrc | 17 ++++++++++++++++ app/.nvmrc | 1 + app/package.json | 2 +- uv.lock | 50 ++++++++++++++++++++++++------------------------ 5 files changed, 63 insertions(+), 26 deletions(-) create mode 100644 app/.npmrc create mode 100644 app/.nvmrc diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d2675e6f2..af337e0812 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -174,6 +174,25 @@ aggregate instead: an italic *Catalog* line at the end of the version section an idle window — so the instance is in practice never reclaimed and visitors keep the same time to first byte. `anyplot-api` keeps `min-instances=1`: its cold start is ~11.6 s and its traffic does leave gaps over 15 minutes. (#10812) +- **The frontend declares the Node version it is actually built with, and something + enforces it** — `app/package.json` asked for `node >=20` while the image that produces + the deployed bundle builds on Node 22 and CI tests on Node 24, so the only version the + manifest still admitted was the one nothing tests and that reached end of life in April + 2026. The floor moves to `>=22`, `app/.nvmrc` names 22 for `nvm use` and `setup-node`, + and `app/.npmrc` sets `engine-strict=true` so an npm install in `app/` refuses an + unsupported runtime at install time rather than failing later inside the build with a + message that never mentions the version (yarn 1, the app's package manager, checks + `engines` itself). Same pin as the sibling repo kurrentschrift. + +### Security + +- **`click` 8.3.1 → 8.3.3 closes PYSEC-2026-2132** — the only advisory `pip-audit` + reports against the resolved runtime dependency set (`uv export --no-dev`), which now + comes back clean. A transitive dependency, so the fix is a lock-file bump with no + `pyproject.toml` constraint, per the repository's dependency rule. The bump is the + minimal one that clears the advisory; `click` 8.5.0 exists and is left to Dependabot, + where a minor bump of the library behind every console script gets its own PR and its + own CI run. ## [3.2.0] — 2026-08-29 — Findable by assistants diff --git a/app/.npmrc b/app/.npmrc new file mode 100644 index 0000000000..37bbf077fa --- /dev/null +++ b/app/.npmrc @@ -0,0 +1,17 @@ +# package.json declares `engines.node` and, until this file existed, nothing +# enforced it: npm ignores the field unless engine-strict is set, so a machine +# on an older Node installed happily and only failed later inside the build, +# with an error that never mentions the version. Enforced here, the refusal +# happens at install time and names the version it wants. +# +# The app's package manager is yarn (CLAUDE.md "Package Management"), and +# yarn 1 checks `engines` on its own. This file covers the other half: anyone +# who reaches for npm in `app/` — a quick `npm ci`, a tool that shells out to +# npm — gets the same refusal instead of a silently unsupported toolchain. +engine-strict=true + +# The audit step of a local install reports the registry's view of dev +# dependencies, which the deployed bundle does not contain, and costs minutes +# for an answer no one acts on here. Dependabot watches the manifest instead. +audit=false +fund=false diff --git a/app/.nvmrc b/app/.nvmrc new file mode 100644 index 0000000000..2bd5a0a98a --- /dev/null +++ b/app/.nvmrc @@ -0,0 +1 @@ +22 diff --git a/app/package.json b/app/package.json index 2de95e48a8..cf35b69afc 100644 --- a/app/package.json +++ b/app/package.json @@ -18,7 +18,7 @@ "test:watch": "vitest" }, "engines": { - "node": ">=20" + "node": ">=22" }, "packageManager": "yarn@1.22.22", "dependencies": { diff --git a/uv.lock b/uv.lock index 4e3f791dc0..3e2737f835 100644 --- a/uv.lock +++ b/uv.lock @@ -854,14 +854,14 @@ wheels = [ [[package]] name = "click" -version = "8.3.1" +version = "8.3.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +sdist = { url = "https://files.pythonhosted.org/packages/bb/63/f9e1ea081ce35720d8b92acde70daaedace594dc93b693c869e0d5910718/click-8.3.3.tar.gz", hash = "sha256:398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2", size = 328061, upload-time = "2026-04-22T15:11:27.506Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, + { url = "https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl", hash = "sha256:a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613", size = 110502, upload-time = "2026-04-22T15:11:25.044Z" }, ] [[package]] @@ -1564,8 +1564,8 @@ name = "httpcore2" version = "2.12.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "h11" }, - { name = "truststore" }, + { name = "h11", marker = "sys_platform != 'emscripten'" }, + { name = "truststore", marker = "sys_platform != 'emscripten'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/be/ad/f4f0e57345f1870f3e8cb624e058d7eca6e5a27d33bcc3311d9b618734cd/httpcore2-2.12.0.tar.gz", hash = "sha256:9293522bba0aa7c4c8e9e3f040c16575bd8868e155a77fa30c7a9085a5eae648", size = 67548, upload-time = "2026-08-18T13:22:08.211Z" } wheels = [ @@ -2747,12 +2747,12 @@ resolution-markers = [ "python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] dependencies = [ - { name = "matplotlib" }, - { name = "mizani" }, - { name = "numpy" }, - { name = "pandas" }, - { name = "scipy" }, - { name = "statsmodels" }, + { name = "matplotlib", marker = "sys_platform != 'emscripten'" }, + { name = "mizani", marker = "sys_platform != 'emscripten'" }, + { name = "numpy", marker = "sys_platform != 'emscripten'" }, + { name = "pandas", marker = "sys_platform != 'emscripten'" }, + { name = "scipy", marker = "sys_platform != 'emscripten'" }, + { name = "statsmodels", marker = "sys_platform != 'emscripten'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/70/f4/c06a16b4ec540ff6af22e32144704f7e86dee18cd56eef14d66f9615cbf2/plotnine-0.15.7.tar.gz", hash = "sha256:6e578bdd93c7ba12ce495dcdeaf8651f8e4ccdceb0c85b94c8195566d709c2a0", size = 6855478, upload-time = "2026-06-13T10:47:14.277Z" } wheels = [ @@ -2768,12 +2768,12 @@ resolution-markers = [ "python_full_version == '3.14.*' and sys_platform == 'emscripten'", ] dependencies = [ - { name = "matplotlib" }, - { name = "mizani" }, - { name = "numpy" }, - { name = "pandas" }, - { name = "scipy" }, - { name = "statsmodels" }, + { name = "matplotlib", marker = "python_full_version >= '3.14' and sys_platform == 'emscripten'" }, + { name = "mizani", marker = "python_full_version >= '3.14' and sys_platform == 'emscripten'" }, + { name = "numpy", marker = "python_full_version >= '3.14' and sys_platform == 'emscripten'" }, + { name = "pandas", marker = "python_full_version >= '3.14' and sys_platform == 'emscripten'" }, + { name = "scipy", marker = "python_full_version >= '3.14' and sys_platform == 'emscripten'" }, + { name = "statsmodels", marker = "python_full_version >= '3.14' and sys_platform == 'emscripten'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/9f/5d/3471689cf15cf2b6ebb6ebdebc7e33831fb35d85e2323875b5a0e150505d/plotnine-0.15.8.tar.gz", hash = "sha256:d3859997c3abd6edff6dee376d912716e75bbe0b0099b7054176d84fe11e8e20", size = 6863148, upload-time = "2026-08-14T16:27:49.399Z" } wheels = [ @@ -2788,12 +2788,12 @@ resolution-markers = [ "python_full_version < '3.14' and sys_platform == 'emscripten'", ] dependencies = [ - { name = "matplotlib" }, - { name = "mizani" }, - { name = "numpy" }, - { name = "pandas" }, - { name = "scipy" }, - { name = "statsmodels" }, + { name = "matplotlib", marker = "python_full_version < '3.14' and sys_platform == 'emscripten'" }, + { name = "mizani", marker = "python_full_version < '3.14' and sys_platform == 'emscripten'" }, + { name = "numpy", marker = "python_full_version < '3.14' and sys_platform == 'emscripten'" }, + { name = "pandas", marker = "python_full_version < '3.14' and sys_platform == 'emscripten'" }, + { name = "scipy", marker = "python_full_version < '3.14' and sys_platform == 'emscripten'" }, + { name = "statsmodels", marker = "python_full_version < '3.14' and sys_platform == 'emscripten'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/2e/e8/d28be1fee4b1c7ef05f76bfb5a137ebc9b214f80b06b2771c73903d421e0/plotnine-0.16.0a12.tar.gz", hash = "sha256:e95565d9ae16ed60bfa16bcea2cb3c862e4b3165b8c257b8a96498f39bb8b17d", size = 7464639, upload-time = "2026-08-15T12:05:51.042Z" } wheels = [ @@ -3605,8 +3605,8 @@ name = "secretstorage" version = "3.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cryptography" }, - { name = "jeepney" }, + { name = "cryptography", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "jeepney", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/1c/03/e834bcd866f2f8a49a85eaff47340affa3bfa391ee9912a952a1faa68c7b/secretstorage-3.5.0.tar.gz", hash = "sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be", size = 19884, upload-time = "2025-11-23T19:02:53.191Z" } wheels = [ From 6e128606f20d35142676021c8b42285b415ba1ee Mon Sep 17 00:00:00 2001 From: Markus Neusinger <2921697+MarkusNeusinger@users.noreply.github.com> Date: Wed, 2 Sep 2026 23:19:19 +0200 Subject: [PATCH 2/2] Raise the Node floor to the version the build path really needs Copilot review: `>=22` advertised Node 22.0-22.11 as supported, but the locked Vite 8.0.16 (and rolldown) declare `^20.19.0 || >=22.12.0`, so those runtimes pass the project-level check and are then rejected by Vite's own dependency-level engine check - the exact late, confusing failure this pin exists to prevent. The floor becomes `>=22.12.0`. `docs/development.md` still said "Node.js 20+" in its prerequisites and now matches; it also points at `.nvmrc` and says to take the latest 22.x, because the dev-only test stack reaches higher still (jsdom wants `^22.22.2`). The declared floor deliberately tracks the build toolchain rather than a transitive dev dependency's patch version, which would need editing on every bump; `app/.npmrc` records that reasoning. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01PBQdMbboxo59sSThGSbfke --- CHANGELOG.md | 15 +++++++++------ app/.npmrc | 6 ++++++ app/package.json | 2 +- docs/development.md | 4 +++- 4 files changed, 19 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index af337e0812..be3e135ab1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -178,11 +178,14 @@ aggregate instead: an italic *Catalog* line at the end of the version section an enforces it** — `app/package.json` asked for `node >=20` while the image that produces the deployed bundle builds on Node 22 and CI tests on Node 24, so the only version the manifest still admitted was the one nothing tests and that reached end of life in April - 2026. The floor moves to `>=22`, `app/.nvmrc` names 22 for `nvm use` and `setup-node`, - and `app/.npmrc` sets `engine-strict=true` so an npm install in `app/` refuses an - unsupported runtime at install time rather than failing later inside the build with a - message that never mentions the version (yarn 1, the app's package manager, checks - `engines` itself). Same pin as the sibling repo kurrentschrift. + 2026. The floor moves to `>=22.12.0` — the version the build path actually requires + (Vite and rolldown declare `^20.19.0 || >=22.12.0`, so `>=22` would have advertised + 22.0–22.11 as supported and let Vite's own engine check reject them instead) — `app/.nvmrc` + names 22 for `nvm use` and `setup-node`, and `app/.npmrc` sets `engine-strict=true` so an + npm install in `app/` refuses an unsupported runtime at install time rather than failing + later inside the build with a message that never mentions the version (yarn 1, the app's + package manager, checks `engines` itself). `docs/development.md` said "Node.js 20+" and now + matches. Same pin as the sibling repo kurrentschrift. (#11206) ### Security @@ -192,7 +195,7 @@ aggregate instead: an italic *Catalog* line at the end of the version section an `pyproject.toml` constraint, per the repository's dependency rule. The bump is the minimal one that clears the advisory; `click` 8.5.0 exists and is left to Dependabot, where a minor bump of the library behind every console script gets its own PR and its - own CI run. + own CI run. (#11206) ## [3.2.0] — 2026-08-29 — Findable by assistants diff --git a/app/.npmrc b/app/.npmrc index 37bbf077fa..e6bcb3d263 100644 --- a/app/.npmrc +++ b/app/.npmrc @@ -8,6 +8,12 @@ # yarn 1 checks `engines` on its own. This file covers the other half: anyone # who reaches for npm in `app/` — a quick `npm ci`, a tool that shells out to # npm — gets the same refusal instead of a silently unsupported toolchain. +# +# The declared floor is 22.12 because that is what the build path needs (Vite +# and rolldown: `^20.19.0 || >=22.12.0`). `.nvmrc` says plain `22` — the latest +# 22.x — on purpose, because the dev-only test stack reaches higher still +# (jsdom wants `^22.22.2`); a floor tracking a transitive dev dependency's +# patch version would need editing on every bump. engine-strict=true # The audit step of a local install reports the registry's view of dev diff --git a/app/package.json b/app/package.json index cf35b69afc..af915c8c98 100644 --- a/app/package.json +++ b/app/package.json @@ -18,7 +18,7 @@ "test:watch": "vitest" }, "engines": { - "node": ">=22" + "node": ">=22.12.0" }, "packageManager": "yarn@1.22.22", "dependencies": { diff --git a/docs/development.md b/docs/development.md index 2927b64c93..70825e3254 100644 --- a/docs/development.md +++ b/docs/development.md @@ -7,7 +7,9 @@ Guide for setting up a local development environment. ## Prerequisites - **Python 3.13+** -- **Node.js 20+** and yarn +- **Node.js 22.12+** and yarn — `app/.nvmrc` names the version to use (`nvm use` in `app/`), + and `app/package.json` refuses anything older. Take the latest 22.x: the build toolchain + needs 22.12, and the dev-only test stack (jsdom) currently wants 22.22. - **PostgreSQL** (or access to Cloud SQL) - **uv** - Fast Python package manager