From a1487458d3e9d194949ef0a02ea093b5a7278f30 Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Mon, 26 Oct 2026 03:44:36 -0700 Subject: [PATCH] v1.91.241.0 fix(deps): bump dompurify, MCP SDK, zod, body-parser, fast-uri, ip-address to OSV-fixed releases (#3462) (#3463) * fix(deps): bump dompurify, @modelcontextprotocol/sdk, zod, body-parser, fast-uri and ip-address to their OSV-fixed releases (#3462) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * build(diagram-render): empty excalidraw's public Firebase config literal in the bundle; declare BUILD_INFO.json's test lane Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test(diagram-render): name the bundle, BUILD_INFO and build script by repo path so the pre-gate sees their lane Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * chore(pregate): drop the BUILD_INFO.json lane declaration; the drift test names the file now Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix: pre-landing review fixes Testing specialist: the drift test asserts the bundled DOMPurify is the override and the bundle carries no Google API key; deps-smoke refuses ip-address below 10.7.4 and checks every bumped package against its OSV-fixed floor in both lockfiles. Adversarial: build.ts fails when any AIza key survives the strip. Outside voice and red team: the CHANGELOG hash and scope lines match the shipped tree. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs(diagram-render): document the Firebase-literal strip and the new drift checks Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --------- Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --- CHANGELOG.md | 16 + VERSION | 2 +- agents-digest/gstack-AGENTS.md | 2 +- bun.lock | 18 +- lib/diagram-render/README.md | 8 + lib/diagram-render/bun.lock | 4 +- lib/diagram-render/dist/BUILD_INFO.json | 6 +- lib/diagram-render/dist/diagram-render.html | 1170 +++++++++---------- lib/diagram-render/package.json | 2 +- lib/diagram-render/scripts/build.ts | 20 +- package.json | 8 +- test/deps-smoke.test.ts | 34 +- test/diagram-render-drift.test.ts | 26 +- 13 files changed, 701 insertions(+), 615 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f15a9fd1..2fa85cd59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ # Changelog +## [1.91.241.0] - 2026-10-26 + +**The weekly OSV scan on main is green again, so the weekly release cut can run.** The 2026-10-26 scan ([run 40288353843](https://github.com/garrytan/gstack/actions/runs/40288353843), #3462) found 14 fixable vulnerabilities in six packages across both lockfiles, and the release workflow skips any main commit with a red check run. Every one is bumped to its fixed release; no application code changed, because gstack imports none of these packages directly (the MCP SDK and zod arrive through `@anthropic-ai/claude-agent-sdk`, body-parser through its express, fast-uri through ajv, ip-address through socks and express-rate-limit, dompurify through mermaid). + +### Fixed +- **dompurify 3.4.16 → 3.4.17 in `lib/diagram-render`** (GHSA-cg2g-xh64-v4q7, CVSS 9.2; GHSA-mrv2-3pqr-j5jr; GHSA-c6gr-qvv4-5v3f). The override moves and the committed bundle is rebuilt with Bun 1.4.2 (two builds, one sha256 `c2d37f9f…`), so the shipped page runs the fixed sanitizer instead of a bundled copy of the old one. Mermaid, excalidraw and React pins are unchanged. +- **@modelcontextprotocol/sdk 1.31.0 → 1.32.0** (GHSA-22jm-h49p-29qw, GHSA-46jr-4wx8-v453, GHSA-rvq5-wwqv-78pq, GHSA-6prh-2h8m-c8cw), **fast-uri 3.1.8 → 3.1.9** (GHSA-w74x-v6f9-j3xg, GHSA-wjr9-j795-jxcr, GHSA-333x-w4r6-6crp, GHSA-xhq7-xmqj-769h) and **ip-address 10.7.3 → 10.7.4** (GHSA-rxp7-6hxx-x8q5) through the existing `overrides` pins. +- **zod 4.5.4 → 4.6.7** (GHSA-mrhx-c7xc-m3w5, fixed in 4.6.6) and **body-parser 2.3.0 → 2.4.0** (GHSA-27r3-jxmh-p828) through the lockfile alone; both sit inside their consumers' ranges, so no override was added. body-parser 2.4.0 brings a nested qs 6.16.1. + +### Changed +- **`lib/diagram-render/scripts/build.ts`** empties the one `VITE_APP_FIREBASE_CONFIG` literal that `@excalidraw/excalidraw`'s npm bundle carries (excalidraw.com's public Firebase client key, which nothing in the package reads and the offline render page never uses). The build fails if the literal is not matched exactly once or any `AIza…` key survives, so an excalidraw bump that changes its shape cannot put the key back silently. Without this, every rebuild of the committed bundle failed the pre-gate secret scan. + +### Tests +- `test/diagram-render-drift.test.ts` asserts the bundle's DOMPurify is the `package.json` override and no other version, and that it carries no Google API key; its path constants are repo-relative segment runs so the pre-gate's import graph selects it for the bundle, `BUILD_INFO.json` and the build script. +- `test/deps-smoke.test.ts` refuses `ip-address` below 10.7.4 and checks every resolved version of the six packages against its OSV-fixed floor in both lockfiles (all six fail on main's lockfiles). OSV-Scanner 2.3.8 with the workflow's own arguments (`--config=.osv-scanner.toml --include-git-root --recursive ./`) reports no issues on this tree; the katex ignore is the only filtered advisory. + ## [1.91.240.0] - 2026-10-26 **Proactive skill suggestions default to the four core skills.** `proactive` now takes `core` (the default: only /office-hours, /review, /qa and /ship auto-invoke when a request matches), `all` (any matching skill, the old behavior) or `off`. `true` and `false` keep working as `all` and `off`. The sentiment research (GSTA-93) found auto-invocation of every skill is what makes gstack collide with other skill packs; the four core skills are where the praise is, so they keep the nudge. Garry, 2026-10-25: "do it". diff --git a/VERSION b/VERSION index 0754a876c..b08862def 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.91.240.0 +1.91.241.0 diff --git a/agents-digest/gstack-AGENTS.md b/agents-digest/gstack-AGENTS.md index 4dcba9700..5388bd6cc 100644 --- a/agents-digest/gstack-AGENTS.md +++ b/agents-digest/gstack-AGENTS.md @@ -1,4 +1,4 @@ -# gstack digest v1.91.240.0 — regenerate/re-copy after upgrading gstack +# gstack digest v1.91.241.0 — regenerate/re-copy after upgrading gstack Behavioral rules from gstack (https://github.com/garrytan/gstack), compressed for agent hosts without a full skill install. The full skills add workflows, diff --git a/bun.lock b/bun.lock index dc2f348d0..30f33b2fb 100644 --- a/bun.lock +++ b/bun.lock @@ -31,12 +31,12 @@ "playwright-core@1.62.1": "patches/playwright-core@1.62.1.patch", }, "overrides": { - "@modelcontextprotocol/sdk": "1.31.0", + "@modelcontextprotocol/sdk": "1.32.0", "adm-zip": "^0.6.1", - "fast-uri": "3.1.8", + "fast-uri": "3.1.9", "hono": "4.13.13", "image-size": "2.0.4", - "ip-address": "10.7.3", + "ip-address": "10.7.4", "proxy-addr": "2.0.8", "sharp": "0.35.5", }, @@ -127,7 +127,7 @@ "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.35.5", "", { "os": "win32", "cpu": "x64" }, "sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg=="], - "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.31.0", "", { "dependencies": { "@hono/node-server": "^1.19.9 || ^2.0.5", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-UvTMgnNlnIBO/22ob2RcVGDlcvOslQs8T59+FTGdA0L27a39fdGF/EDETNtDVK4DZGpwomlsYpRdA8UXcVL/pw=="], + "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.32.0", "", { "dependencies": { "@hono/node-server": "^1.19.9 || ^2.0.5", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-8BviX/hK4Gd2eL1KdTwm0gfl4d3wdoErW0Jd00h6nomUAtk6IJk5vwQJc6kfWtJTzN5OEV/lgIiJKI8fiDGAlA=="], "@ngrok/ngrok": ["@ngrok/ngrok@1.7.0", "", { "optionalDependencies": { "@ngrok/ngrok-android-arm64": "1.7.0", "@ngrok/ngrok-darwin-arm64": "1.7.0", "@ngrok/ngrok-darwin-universal": "1.7.0", "@ngrok/ngrok-darwin-x64": "1.7.0", "@ngrok/ngrok-freebsd-x64": "1.7.0", "@ngrok/ngrok-linux-arm-gnueabihf": "1.7.0", "@ngrok/ngrok-linux-arm64-gnu": "1.7.0", "@ngrok/ngrok-linux-arm64-musl": "1.7.0", "@ngrok/ngrok-linux-x64-gnu": "1.7.0", "@ngrok/ngrok-linux-x64-musl": "1.7.0", "@ngrok/ngrok-win32-arm64-msvc": "1.7.0", "@ngrok/ngrok-win32-ia32-msvc": "1.7.0", "@ngrok/ngrok-win32-x64-msvc": "1.7.0" } }, "sha512-P06o9TpxrJbiRbHQkiwy/rUrlXRupc+Z8KT4MiJfmcdWxvIdzjCaJOdnNkcOTs6DMyzIOefG5tvk/HLdtjqr0g=="], @@ -235,7 +235,7 @@ "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], - "body-parser": ["body-parser@2.3.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^2.0.0", "debug": "^4.4.3", "http-errors": "^2.0.1", "iconv-lite": "^0.7.2", "on-finished": "^2.4.1", "qs": "^6.15.2", "raw-body": "^3.0.2", "type-is": "^2.1.0" } }, "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw=="], + "body-parser": ["body-parser@2.4.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^2.1.0", "debug": "^4.4.3", "http-errors": "^2.0.1", "iconv-lite": "^0.7.3", "on-finished": "^2.4.1", "qs": "^6.16.1", "raw-body": "^3.0.2", "type-is": "^2.1.0" } }, "sha512-Ifk1KcLrxaGt1occQagQNFNNOqRt1LFuehEnHHzB8Z6cRgIxRU1TBaiIsUiCdA+gy5PL7Y1IX31DNKkRmxtXjQ=="], "browser-split": ["browser-split@0.0.1", "", {}, "sha512-JhvgRb2ihQhsljNda3BI8/UcRHVzrVwo3Q+P8vDtSiyobXuFpuZ9mq+MbRGMnC22CjW3RrfXdg6j6ITX8M+7Ow=="], @@ -323,7 +323,7 @@ "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], - "fast-uri": ["fast-uri@3.1.8", "", {}, "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg=="], + "fast-uri": ["fast-uri@3.1.9", "", {}, "sha512-hS/0Wt3Ya043oKXs0sIxrMpKQITXhMLIrVAKKXi4gozGC9v7YRWVnXIZzjNPTO02M9BdxLCWqK1yRXgNziVyJQ=="], "finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="], @@ -383,7 +383,7 @@ "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], - "ip-address": ["ip-address@10.7.3", "", {}, "sha512-A1kdq/tSb5QjvKvAMgIoEvDBIgL7qaqVP/jkvSwYYRZ9iEzvPpopxp2wQfu3SuZRHtpHNxMn8Fs0bS+gf5Xmwg=="], + "ip-address": ["ip-address@10.7.4", "", {}, "sha512-DWBMKyeM25D70nrIiTkgQth59/G+naKOTep6huF9j+GmeK3DMD60Zvxg5d5qQnz+38AHgou6ean2LECX+lrKDg=="], "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], @@ -581,7 +581,7 @@ "xterm-addon-fit": ["xterm-addon-fit@0.8.0", "", { "peerDependencies": { "xterm": "^5.0.0" } }, "sha512-yj3Np7XlvxxhYF/EJ7p3KHaMt6OdwQ+HDu573Vx1lRXsVxOcnVJs51RgjZOouIZOczTsskaS+CpXspK81/DLqw=="], - "zod": ["zod@4.5.4", "", {}, "sha512-sC95tT5iHHH9gtpj6A81kh+NEaRAUFN+qlUPDUbRfOMvNf5QCBqsb3WgvnpVtK5Y+4UfA6KqufotuTvMGiTlsA=="], + "zod": ["zod@4.6.7", "", {}, "sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg=="], "zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="], @@ -597,6 +597,8 @@ "body-parser/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="], + "body-parser/qs": ["qs@6.16.1", "", { "dependencies": { "es-define-property": "^1.0.1", "side-channel": "^1.1.1" } }, "sha512-5Px6J7rfHBAGUhNQ3es4YzcOdBBw0SDcVtThJml4RdnKw4tKs2epA0kPZ+K8X+WiHktT93RHVcQ/3ym5aTCgtg=="], + "dom-serializer/domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="], "dom-serializer/entities": ["entities@2.2.0", "", {}, "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A=="], diff --git a/lib/diagram-render/README.md b/lib/diagram-render/README.md index ef2f3a301..c18397c94 100644 --- a/lib/diagram-render/README.md +++ b/lib/diagram-render/README.md @@ -14,6 +14,8 @@ The built page is **committed** (eng-review D2): rendering works with zero network at install time and render time, and there is no npm supply-chain surface in `./setup`. The drift test (`test/diagram-render-drift.test.ts`) fails CI if `dist/` is edited by hand or falls out of sync with `BUILD_INFO.json`. +It also checks that the bundled DOMPurify is the `package.json` override and that +the bundle carries no Google API key. ## Page API (window functions) @@ -41,6 +43,12 @@ bun run build # 3. commit package.json + bun.lock + dist/ together ``` +`scripts/build.ts` empties the one `VITE_APP_FIREBASE_CONFIG` literal in +`@excalidraw/excalidraw`'s npm bundle (a public Firebase client key the offline page +never uses), because a key-shaped literal fails the pre-gate secret scan. The build +fails if that literal is not matched exactly once or an `AIza…` key survives, so an +excalidraw bump that changes its shape needs the strip rule updated. + Render contract details (securityLevel strict, htmlLabels false, print-css font lock, `` + `
loading
diff --git a/lib/diagram-render/package.json b/lib/diagram-render/package.json index 8a41abd38..49b47f5ce 100644 --- a/lib/diagram-render/package.json +++ b/lib/diagram-render/package.json @@ -15,7 +15,7 @@ }, "overrides": { "chokidar": "4.0.3", - "dompurify": "3.4.16", + "dompurify": "3.4.17", "nanoid": "5.1.16", "source-map-js": "1.2.2", "lodash-es": "4.18.1" diff --git a/lib/diagram-render/scripts/build.ts b/lib/diagram-render/scripts/build.ts index 2bd2caf99..1ee1be1a0 100644 --- a/lib/diagram-render/scripts/build.ts +++ b/lib/diagram-render/scripts/build.ts @@ -42,8 +42,26 @@ if (!result.success) { } const js = await result.outputs[0].text(); +// @excalidraw/excalidraw's npm bundle carries excalidraw.com's public Firebase +// client config (an `AIza…` key) in an env literal nothing in the package +// reads. The offline page never opens a collab session, and a key-shaped +// literal in a committed file fails the pre-gate secret scan on every rebuild, +// so the literal is emptied. Exactly one match is expected: a bump that changes +// the shape must fail here rather than put the key back silently. +const FIREBASE_CONFIG = /VITE_APP_FIREBASE_CONFIG:'\{[^']*\}'/g; +const firebaseHits = js.match(FIREBASE_CONFIG)?.length ?? 0; +if (firebaseHits !== 1) { + console.error(`expected exactly one VITE_APP_FIREBASE_CONFIG literal in the excalidraw bundle, found ${firebaseHits}; update the strip rule in scripts/build.ts`); + process.exit(1); +} +const strippedJs = js.replace(FIREBASE_CONFIG, "VITE_APP_FIREBASE_CONFIG:'{}'"); +if (/AIza[0-9A-Za-z_-]{35}/.test(strippedJs)) { + console.error("a Google API key literal survived the strip; update the strip rule in scripts/build.ts"); + process.exit(1); +} + // Escape inline-script terminators (see header note). -const inlineJs = js.replaceAll(" diff --git a/package.json b/package.json index b9bb5b10b..06cead090 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gstack", - "version": "1.91.240", + "version": "1.91.241", "description": "Garry's Stack — Claude Code skills + fast headless browser. One repo, one install, entire AI engineering workflow.", "license": "MIT", "type": "module", @@ -103,12 +103,12 @@ "typescript": "7.0.2" }, "overrides": { - "@modelcontextprotocol/sdk": "1.31.0", + "@modelcontextprotocol/sdk": "1.32.0", "adm-zip": "^0.6.1", - "fast-uri": "3.1.8", + "fast-uri": "3.1.9", "hono": "4.13.13", "image-size": "2.0.4", - "ip-address": "10.7.3", + "ip-address": "10.7.4", "proxy-addr": "2.0.8", "sharp": "0.35.5" }, diff --git a/test/deps-smoke.test.ts b/test/deps-smoke.test.ts index 7fe7f94a1..b90d73147 100644 --- a/test/deps-smoke.test.ts +++ b/test/deps-smoke.test.ts @@ -1,8 +1,8 @@ /** * Dependency-override smoke tests (v1.78.0.0 dependency wave). * - * The wave's `overrides` entries (package.json: ip-address 10.7.3 (raised - * from 10.3.1 for GHSA-2vr4/h3mg/j6r3/rpw4, Oct 2026), + * The wave's `overrides` entries (package.json: ip-address 10.7.4 (raised + * from 10.3.1 for GHSA-2vr4/h3mg/j6r3/rpw4, then 10.7.3 for GHSA-rxp7, Oct 2026), * sharp 0.35.0; lib/diagram-render: nanoid 5.x, lodash-es 4.18.x) defeat * nested exact pins, so a green unit suite alone does not prove the forced * versions actually work for their consumers. These smokes exercise the @@ -27,13 +27,37 @@ describe("dependency-wave smoke", () => { expect(outMeta.width).toBe(4); }); - test("ip-address 10.7 override: both nested consumers resolve the fixed version", async () => { + test("ip-address 10.7.4 override: both nested consumers resolve the fixed version", async () => { const lock = await Bun.file(`${import.meta.dir}/../bun.lock`).text(); // No vulnerable ip-address node may survive anywhere in the tree // (socks pulled 10.2.0; express-rate-limit exact-pinned 10.1.0 — the - // override must defeat both). + // override must defeat both), and 10.7.3 (GHSA-rxp7-6hxx-x8q5) is vulnerable too. expect(lock).not.toMatch(/ip-address@10\.[0-6]\./); - expect(lock).toMatch(/ip-address@10\.7\./); + expect(lock).not.toMatch(/ip-address@10\.7\.[0-3]"/); + expect(lock).toMatch(/ip-address@10\.7\.(?:[4-9]|\d{2,})/); + }); + + // The 2026-10-26 OSV scan (#3462) named the fixed release of each package + // below; a lockfile that resolves any of them lower regresses the scan. + const floors: Array<[lockfile: string, pkg: string, min: string]> = [ + ["bun.lock", "@modelcontextprotocol/sdk", "1.32.0"], + ["bun.lock", "fast-uri", "3.1.9"], + ["bun.lock", "ip-address", "10.7.4"], + ["bun.lock", "zod", "4.6.6"], + ["bun.lock", "body-parser", "2.4.0"], + ["lib/diagram-render/bun.lock", "dompurify", "3.4.17"], + ]; + const cmp = (a: string, b: string) => { + const [x, y] = [a, b].map(v => v.split(".").map(Number)); + for (let i = 0; i < 3; i++) if (x[i] !== y[i]) return x[i] - y[i]; + return 0; + }; + test.each(floors)("%s resolves every %s at or above its OSV-fixed release %s", async (lockfile, pkg, min) => { + const lock = await Bun.file(`${import.meta.dir}/../${lockfile}`).text(); + const escaped = pkg.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&"); + const resolved = [...lock.matchAll(new RegExp(`"${escaped}@(\\d+\\.\\d+\\.\\d+)"`, "g"))].map(m => m[1]); + expect(resolved.length).toBeGreaterThan(0); + for (const v of resolved) expect(cmp(v, min), `${pkg}@${v} is below ${min}`).toBeGreaterThanOrEqual(0); }); test("marked stays importable and parses (direct-dep bump)", async () => { diff --git a/test/diagram-render-drift.test.ts b/test/diagram-render-drift.test.ts index 48b1574e6..e431cad50 100644 --- a/test/diagram-render-drift.test.ts +++ b/test/diagram-render-drift.test.ts @@ -17,9 +17,13 @@ import { createHash } from "node:crypto"; import { existsSync } from "node:fs"; import path from "node:path"; -const ROOT = path.resolve(import.meta.dir, "..", "lib", "diagram-render"); -const DIST_HTML = path.join(ROOT, "dist", "diagram-render.html"); -const BUILD_INFO = path.join(ROOT, "dist", "BUILD_INFO.json"); +// Repo-relative segment runs, so the pre-gate's import graph sees that this +// test owns the committed bundle, its BUILD_INFO and the build script. +const REPO = path.resolve(import.meta.dir, ".."); +const ROOT = path.join(REPO, "lib", "diagram-render"); +const DIST_HTML = path.join(REPO, "lib", "diagram-render", "dist", "diagram-render.html"); +const BUILD_INFO = path.join(REPO, "lib", "diagram-render", "dist", "BUILD_INFO.json"); +const BUILD_SCRIPT = path.join(REPO, "lib", "diagram-render", "scripts", "build.ts"); describe("diagram-render bundle drift", () => { test("dist hash matches BUILD_INFO (tamper check)", async () => { @@ -36,6 +40,20 @@ describe("diagram-render bundle drift", () => { expect(info.deps).toEqual(pkg.dependencies); }); + test("the bundled DOMPurify is the package.json override, and no other version", async () => { + const html = await Bun.file(DIST_HTML).text(); + const pkg = await Bun.file(path.join(ROOT, "package.json")).json(); + const versions = [...html.matchAll(/@license DOMPurify (\d+\.\d+\.\d+)/g)].map(m => m[1]); + expect(versions).toEqual([pkg.overrides.dompurify]); + expect(html).toContain(`version="${pkg.overrides.dompurify}"`); + }); + + test("the bundle carries no Google API key: excalidraw's Firebase config literal is emptied", async () => { + const html = await Bun.file(DIST_HTML).text(); + expect(html).not.toMatch(/AIza[0-9A-Za-z_-]{35}/); + expect(html).toContain("VITE_APP_FIREBASE_CONFIG:'{}'"); + }); + test("BUILD_INFO srcSha256 matches src on disk (edited-src-forgot-rebuild guard)", async () => { // The deep rebuild check below needs node_modules, which CI doesn't // install for this nested package — this tier-1.5 fingerprint catches a @@ -43,7 +61,7 @@ describe("diagram-render bundle drift", () => { const info = await Bun.file(BUILD_INFO).json(); const srcSha = createHash("sha256") .update(await Bun.file(path.join(ROOT, "src", "entry.ts")).text()) - .update(await Bun.file(path.join(ROOT, "scripts", "build.ts")).text()) + .update(await Bun.file(BUILD_SCRIPT).text()) .digest("hex"); expect(srcSha).toBe(info.srcSha256); });