diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3423fa752..0993c6ce9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,10 +5,10 @@ settings: excludeLinksFromLockfile: false overrides: - dompurify@<=3.4.10: ^3.4.11 - postcss@<8.5.10: ^8.5.10 + dompurify@<3.4.13: ^3.4.13 + postcss@<8.5.23: ^8.5.23 nanoid@<3.3.18: ^3.3.18 - '@hono/node-server@<1.19.13': ^1.19.13 + '@hono/node-server@<1.19.15': ^1.19.15 hono@<4.12.25: ^4.12.25 picomatch@<4.0.4: ^4.0.4 body-parser@<2.3.0: ^2.3.0 @@ -21,6 +21,7 @@ overrides: sharp@<0.35.0: ^0.35.3 pdfjs-dist@>=5.6.83 <6.2.108: 6.2.108 deepmerge-ts@<8.0.0: ^8.0.0 + valibot@<1.4.2: ^1.4.2 importers: @@ -40,7 +41,7 @@ importers: version: 3.2.2(react@19.2.7) '@hookform/resolvers': specifier: ^5.9.1 - version: 5.9.1(@standard-schema/spec@1.1.0)(ajv-formats@2.1.1(ajv@8.20.0))(ajv@8.20.0)(effect@3.20.0)(react-hook-form@7.83.0(react@19.2.7))(valibot@1.2.0(typescript@6.0.3))(zod@4.4.3) + version: 5.9.1(@standard-schema/spec@1.1.0)(ajv-formats@2.1.1(ajv@8.20.0))(ajv@8.20.0)(effect@3.20.0)(react-hook-form@7.83.0(react@19.2.7))(valibot@1.4.2(typescript@6.0.3))(zod@4.4.3) '@modelcontextprotocol/sdk': specifier: ^1.30.0 version: 1.30.0(@cfworker/json-schema@4.1.1)(supports-color@7.2.0)(zod@4.4.3) @@ -864,7 +865,7 @@ packages: react-hook-form: ^7.55.0 superstruct: '>=0.12.0' typanion: ^3.3.2 - valibot: '>=0.31.0 || ^1.0.0-beta.4 || ^1.0.0-rc' + valibot: ^1.4.2 vest: '>=6.0.0' yup: ^1.0.0 zod: ^3.25.0 || ^4.0.0 @@ -4204,8 +4205,8 @@ packages: resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} engines: {node: '>=0.10.0'} - dompurify@3.4.12: - resolution: {integrity: sha512-zQvGet8Z2sWbQhCmfFz/T5QWH2oBmjnqK3qvOjaqaNLrLEF912WamU+ohnTp0TCep/MFVHpdJuCZEdFOdTnEFg==} + dompurify@3.4.14: + resolution: {integrity: sha512-dVoH9z+MY+C9IilgGCk3YfFqjLi3fChm2OiKJMzh6axrJ5qwxqWaZamgmHrpv22CN/KdbZJuGEGgfQoL00LTdg==} dot-prop@6.0.1: resolution: {integrity: sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==} @@ -5961,10 +5962,6 @@ packages: resolution: {integrity: sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg==} engines: {node: '>=4'} - postcss@8.5.20: - resolution: {integrity: sha512-lW616l85ucIQL+FocMmL7pQFPqBmwejrCMg+iPxyImlrANNJG9NHq/RkyCZopDhd8C3LA03PHRJDjkbGu8vvug==} - engines: {node: ^10 || ^12 || >=14} - postcss@8.5.25: resolution: {integrity: sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==} engines: {node: ^10 || ^12 || >=14} @@ -6923,8 +6920,8 @@ packages: utrie@1.0.2: resolution: {integrity: sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==} - valibot@1.2.0: - resolution: {integrity: sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg==} + valibot@1.4.2: + resolution: {integrity: sha512-gjdCvJ6d3RyHAneqxMYMW9QMCwYMb3jpOO0IyHZV1bnRHFBHrX3VkIILt5XYR0WhwHiH7Mty8ovuPZ/O3gamrg==} peerDependencies: typescript: '>=5' peerDependenciesMeta: @@ -7923,7 +7920,7 @@ snapshots: dependencies: hono: 4.13.3 - '@hookform/resolvers@5.9.1(@standard-schema/spec@1.1.0)(ajv-formats@2.1.1(ajv@8.20.0))(ajv@8.20.0)(effect@3.20.0)(react-hook-form@7.83.0(react@19.2.7))(valibot@1.2.0(typescript@6.0.3))(zod@4.4.3)': + '@hookform/resolvers@5.9.1(@standard-schema/spec@1.1.0)(ajv-formats@2.1.1(ajv@8.20.0))(ajv@8.20.0)(effect@3.20.0)(react-hook-form@7.83.0(react@19.2.7))(valibot@1.4.2(typescript@6.0.3))(zod@4.4.3)': dependencies: '@standard-schema/utils': 0.3.0 react-hook-form: 7.83.0(react@19.2.7) @@ -7932,7 +7929,7 @@ snapshots: ajv: 8.20.0 ajv-formats: 2.1.1(ajv@8.20.0) effect: 3.20.0 - valibot: 1.2.0(typescript@6.0.3) + valibot: 1.4.2(typescript@6.0.3) zod: 4.4.3 '@humanfs/core@0.19.1': {} @@ -8894,7 +8891,7 @@ snapshots: proper-lockfile: 4.1.2 remeda: 2.33.4 std-env: 3.10.0 - valibot: 1.2.0(typescript@6.0.3) + valibot: 1.4.2(typescript@6.0.3) zeptomatch: 2.1.0 transitivePeerDependencies: - typescript @@ -11068,7 +11065,7 @@ snapshots: dependencies: esutils: 2.0.3 - dompurify@3.4.12: + dompurify@3.4.14: optionalDependencies: '@types/trusted-types': 2.0.7 optional: true @@ -12253,7 +12250,7 @@ snapshots: optionalDependencies: canvg: 3.0.11 core-js: 3.49.0 - dompurify: 3.4.12 + dompurify: 3.4.14 html2canvas: 1.4.1 jsx-ast-utils@3.3.5: @@ -12603,7 +12600,7 @@ snapshots: '@swc/helpers': 0.5.15 baseline-browser-mapping: 2.10.43 caniuse-lite: 1.0.30001805 - postcss: 8.5.20 + postcss: 8.5.26 react: 19.2.7 react-dom: 19.2.7(react@19.2.7) styled-jsx: 5.1.6(@babel/core@7.29.7(supports-color@7.2.0))(react@19.2.7) @@ -12990,12 +12987,6 @@ snapshots: cssesc: 3.0.0 util-deprecate: 1.0.2 - postcss@8.5.20: - dependencies: - nanoid: 3.3.18 - picocolors: 1.1.1 - source-map-js: 1.2.1 - postcss@8.5.25: dependencies: nanoid: 3.3.18 @@ -14169,7 +14160,7 @@ snapshots: base64-arraybuffer: 1.0.2 optional: true - valibot@1.2.0(typescript@6.0.3): + valibot@1.4.2(typescript@6.0.3): optionalDependencies: typescript: 6.0.3 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index c90da0443..5970d1ce6 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,8 +1,20 @@ overrides: - "dompurify@<=3.4.10": "^3.4.11" - "postcss@<8.5.10": "^8.5.10" + # GHSA-55q2-fjhq-7xh7: a crafted document gets past the sanitiser and its + # markup survives into the output. The copy here is jspdf's optional HTML + # renderer, which the doctor-report PDF path reaches. + "dompurify@<3.4.13": "^3.4.13" + # CVE-2026-69153. Next declares an exact `postcss@8.4.31`, so nothing except + # this bound moves it. The build has run on the 8.5 line since the first + # bump; the fix landed in 8.5.23. + "postcss@<8.5.23": "^8.5.23" "nanoid@<3.3.18": "^3.3.18" - "@hono/node-server@<1.19.13": "^1.19.13" + # GHSA-frvp-7c67-39w9 raised the floor from 1.19.13 to 1.19.15. Mirrored for + # /opt/prisma-cli in the Dockerfile, which is where the flagged 1.19.11 + # actually landed: prisma pulls it through @prisma/dev, and that install + # reads npm `overrides`, not this file. The bound stays on the 1.x line + # because the advisory is fixed there too and 2.x is a major nothing asked + # for. + "@hono/node-server@<1.19.15": "^1.19.15" "hono@<4.12.25": "^4.12.25" "picomatch@<4.0.4": "^4.0.4" "body-parser@<2.3.0": "^2.3.0" @@ -39,6 +51,12 @@ overrides: # override is the only way through. It runs on the config-loading and CLI # path, not under a request, but a red audit blocks the whole release train. "deepmerge-ts@<8.0.0": "^8.0.0" + # CVE-2026-59952. Two copies arrive, neither of them ours to call: an + # optional peer of `@hookform/resolvers` (this app validates with zod) and + # prisma's local dev server. Mirrored for /opt/prisma-cli in the Dockerfile, + # where the flagged 1.2.0 landed. 1.4.2 is the same major, so no consumer is + # dragged onto a new one. + "valibot@<1.4.2": "^1.4.2" allowBuilds: "@prisma/engines": true diff --git a/scripts/__tests__/ci-determinism.test.ts b/scripts/__tests__/ci-determinism.test.ts index 34bac3b29..5d6714b18 100644 --- a/scripts/__tests__/ci-determinism.test.ts +++ b/scripts/__tests__/ci-determinism.test.ts @@ -150,17 +150,86 @@ describe("container dependency installation", () => { }); }); +/** + * `>= 0` comparison over a dotted version, enough for the floor checks below. + * Pre-release suffixes are not expected in an override replacement and are + * ignored rather than guessed at. + */ +function atLeast(version: string, floor: string): boolean { + const parts = (value: string) => + value + .replace(/^[\^~>=<\s]+/, "") + .split("-")[0] + .split(".") + .map((piece) => Number.parseInt(piece, 10) || 0); + const [a, b] = [parts(version), parts(floor)]; + for (let i = 0; i < Math.max(a.length, b.length); i += 1) { + const left = a[i] ?? 0; + const right = b[i] ?? 0; + if (left !== right) return left > right; + } + return true; +} + describe("production dependency advisory floors", () => { - it("pins vulnerable transitive ranges to compatible patched versions", () => { + /** + * The floor an advisory demands, by package. Raising an override past one of + * these must not break this test: an earlier version of it asserted the exact + * key and replacement string, so every legitimate floor raise failed it while + * a floor that had gone STALE against a newer advisory passed. It now asserts + * the property that matters, which is that the pin is at or above the fixed + * version. Update a floor here when a new advisory raises it. + */ + const floors: Record = { + // GHSA-55q2-fjhq-7xh7, reached through jspdf's optional HTML renderer. + dompurify: "3.4.13", + // CVE-2026-69153. + postcss: "8.5.23", + // GHSA-frvp-7c67-39w9 and CVE-2026-39406. Mirrored in the Dockerfile. + "@hono/node-server": "1.19.15", + hono: "4.12.25", + // CVE-2026-59952. Mirrored in the Dockerfile. + valibot: "1.4.2", + }; + + it("pins vulnerable transitive ranges at or above the fixed version", () => { const workspace = parse(readRepoFile("pnpm-workspace.yaml")) as { overrides?: Record; }; + const overrides = workspace.overrides ?? {}; - expect(workspace.overrides).toMatchObject({ - "dompurify@<=3.4.10": "^3.4.11", - "postcss@<8.5.10": "^8.5.10", - "@hono/node-server@<1.19.13": "^1.19.13", - "hono@<4.12.25": "^4.12.25", - }); + const byName = new Map(); + for (const [key, replacement] of Object.entries(overrides)) { + const selectorAt = key.startsWith("@") + ? key.indexOf("@", 1) + : key.indexOf("@"); + byName.set( + selectorAt === -1 ? key : key.slice(0, selectorAt), + replacement, + ); + } + expect(byName.size).toBeGreaterThan(0); + + for (const [name, floor] of Object.entries(floors)) { + const replacement = byName.get(name); + expect( + replacement, + `${name} has no override in pnpm-workspace.yaml`, + ).toBeDefined(); + expect( + atLeast(replacement as string, floor), + `${name} is pinned to ${replacement}, below the advisory floor ${floor}.`, + ).toBe(true); + } + }); + + it("compares versions rather than always agreeing", () => { + // The comparison is the whole test above; a helper that never returns + // false would make it vacuous. + expect(atLeast("^3.4.13", "3.4.13")).toBe(true); + expect(atLeast("^3.4.14", "3.4.13")).toBe(true); + expect(atLeast("^3.4.12", "3.4.13")).toBe(false); + expect(atLeast("^1.19.9", "1.19.15")).toBe(false); + expect(atLeast("^2.0.5", "1.19.15")).toBe(true); }); });