From 651345aae986d490726aa6802c7bd80632641a56 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 3 Jul 2026 18:18:26 +0000 Subject: [PATCH 1/4] Make pnpm blockExoticSubdeps explicit pnpm 11 already defaults this to true: only direct dependencies may resolve from git or tarball URLs, so transitive deps can't bypass minimumReleaseAge and trustPolicy. Setting it explicitly documents the intent and gives the config guard something to assert against a future default change. Co-Authored-By: Claude Opus 4.8 Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01KvPbYiZxbqWVnJyNkoVzky --- pnpm-workspace.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index b70488b..cf94370 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -8,3 +8,5 @@ minimumReleaseAge: 10080 trustPolicy: no-downgrade trustPolicyExclude: - semver@6.3.1 # legacy transitive dep predating npm provenance; false positive +# Forbid transitive deps from resolving via git/tarball URLs (registry only). +blockExoticSubdeps: true From 96a4b94e1aa1c96b55bc12ba751e76f23d62d676 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 3 Jul 2026 18:26:18 +0000 Subject: [PATCH 2/4] Harden CI workflows: least-privilege token, SHA-pinned actions, GHA cooldown - ci.yml: add top-level `permissions: contents: read` (least privilege) - Pin every action to a tagged release's commit SHA with an exact `# vX.Y.Z` comment, so a moved/compromised tag cannot alter what runs - `persist-credentials: false` on checkouts; neither workflow pushes with GITHUB_TOKEN - Pin diff-cover, which `pipx run` otherwise fetches unpinned from PyPI - dependabot.yml: add github-actions ecosystem with a 7-day cooldown, the min-release-age equivalent for Actions; Dependabot bumps the pinned SHAs and keeps the version comment current Co-Authored-By: Claude Opus 4.8 Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01KvPbYiZxbqWVnJyNkoVzky --- .github/dependabot.yml | 8 ++++++++ .github/workflows/ci.yml | 19 ++++++++++++------- .github/workflows/update-pnpm.yml | 11 +++++++---- 3 files changed, 27 insertions(+), 11 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 659b1b2..2f93c8d 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -43,3 +43,11 @@ updates: # bump it by hand when moving to the next Node LTS. - dependency-name: "@types/node" update-types: ["version-update:semver-major"] + + - package-ecosystem: "github-actions" # bumps SHA-pinned actions, keeps # vX comment + directory: "/" + schedule: + interval: "weekly" + cooldown: + default-days: 7 # min-release-age equivalent for GHA: wait out same-day yanks + open-pull-requests-limit: 10 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1f1c9d4..ba2f584 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,21 +5,26 @@ on: branches: [main] pull_request: +# Least privilege: this workflow only reads the repo. +permissions: + contents: read + jobs: validate: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 with: # Full history so diff-cover can find the merge-base with the PR base branch. fetch-depth: 0 + persist-credentials: false - - uses: actions/setup-node@v6 + - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0 with: node-version-file: .nvmrc # Reads the pnpm version from the "packageManager" field in package.json. - - uses: pnpm/action-setup@v6 + - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 # Manual cache (instead of setup-node's built-in cache: pnpm) so we can # use restore-keys for warm/partial restores when the lockfile changes. @@ -27,7 +32,7 @@ jobs: shell: bash run: echo "STORE_PATH=$(pnpm store path --silent)" >> "$GITHUB_ENV" - - uses: actions/cache@v5 + - uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # v5.1.0 with: path: ${{ env.STORE_PATH }} key: ${{ runner.os }}-pnpm-store-${{ hashFiles('pnpm-lock.yaml') }} @@ -64,7 +69,7 @@ jobs: # the git diff against the base branch (no baseline or second run needed). - name: Diff coverage run: | - pipx run diff-cover coverage/lcov.info \ + pipx run diff-cover==10.5.1 coverage/lcov.info \ --compare-branch origin/${{ github.base_ref || 'main' }} \ --fail-under 80 @@ -72,7 +77,7 @@ jobs: run: pnpm build - name: Cache Playwright browsers - uses: actions/cache@v5 + uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # v5.1.0 with: path: ~/.cache/ms-playwright key: ${{ runner.os }}-playwright-${{ hashFiles('pnpm-lock.yaml') }} @@ -87,7 +92,7 @@ jobs: - name: E2E run: pnpm test:e2e - - uses: actions/upload-artifact@v7 + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 if: ${{ failure() }} with: name: playwright-report diff --git a/.github/workflows/update-pnpm.yml b/.github/workflows/update-pnpm.yml index 1ec68cb..56c148d 100644 --- a/.github/workflows/update-pnpm.yml +++ b/.github/workflows/update-pnpm.yml @@ -18,10 +18,13 @@ jobs: update: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 + with: + # create-pull-request authenticates with the App token, not persisted creds. + persist-credentials: false # Pin Node to the repo's version so the registry query runs deterministically. - - uses: actions/setup-node@v6 + - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0 with: node-version-file: .nvmrc @@ -54,7 +57,7 @@ jobs: - name: Generate App token if: steps.versions.outputs.changed == 'true' id: app-token - uses: actions/create-github-app-token@v3 + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 with: client-id: ${{ vars.APP_CLIENT_ID }} private-key: ${{ secrets.APP_PRIVATE_KEY }} @@ -63,7 +66,7 @@ jobs: - name: Create pull request if: steps.versions.outputs.changed == 'true' - uses: peter-evans/create-pull-request@v8 + uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 with: token: ${{ steps.app-token.outputs.token }} commit-message: "chore: update pnpm to ${{ steps.versions.outputs.latest }}" From 846415d800011d14b564230e63aa85d7714f8272 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 3 Jul 2026 18:44:33 +0000 Subject: [PATCH 3/4] Add config-guard test for pnpm supply-chain settings Parses pnpm-workspace.yaml (via the yaml devDep) and asserts trustPolicy, blockExoticSubdeps, minimumReleaseAge, minimumReleaseAgeStrict, and the absence of a global build allowlist. pnpm silently ignores unknown keys, so this fails CI if a protection is removed or misspelled instead of no-opping unnoticed. Co-Authored-By: Claude Opus 4.8 Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01KvPbYiZxbqWVnJyNkoVzky --- package.json | 3 +- pnpm-lock.yaml | 90 +++++++++++++++-------------- src/lib/supply-chain-config.test.ts | 42 ++++++++++++++ 3 files changed, 90 insertions(+), 45 deletions(-) create mode 100644 src/lib/supply-chain-config.test.ts diff --git a/package.json b/package.json index 6ee88b9..ec990c7 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,8 @@ "typescript": "npm:@typescript/typescript6@^6.0.2", "vite": "^8.3.0", "vitest": "^4.1.11", - "vitest-axe": "^0.1.0" + "vitest-axe": "^0.1.0", + "yaml": "^2.9.0" }, "packageManager": "pnpm@11.9.0" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0d225a8..c79b994 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -19,10 +19,10 @@ importers: version: 1.167.2(@tanstack/react-router@1.170.38(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(@tanstack/router-core@1.171.32)(csstype@3.2.3)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) '@tanstack/react-start': specifier: ^1.168.56 - version: 1.168.56(crossws@0.4.12(srvx@1.0.5))(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(rolldown@1.2.9)(vite@8.3.0(@types/node@24.13.6)(jiti@2.7.0)(tsx@4.20.6)(yaml@2.8.2)) + version: 1.168.56(crossws@0.4.12(srvx@1.0.5))(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(rolldown@1.2.9)(vite@8.3.0(@types/node@24.13.6)(jiti@2.7.0)(tsx@4.20.6)(yaml@2.9.1)) '@tanstack/router-plugin': specifier: ^1.168.40 - version: 1.168.40(@tanstack/react-router@1.170.38(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(rolldown@1.2.9)(vite@8.3.0(@types/node@24.13.6)(jiti@2.7.0)(tsx@4.20.6)(yaml@2.8.2)) + version: 1.168.40(@tanstack/react-router@1.170.38(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(rolldown@1.2.9)(vite@8.3.0(@types/node@24.13.6)(jiti@2.7.0)(tsx@4.20.6)(yaml@2.9.1)) react: specifier: ^19.3.0 version: 19.3.0 @@ -38,10 +38,10 @@ importers: version: 1.63.0 '@tailwindcss/vite': specifier: ^4.3.3 - version: 4.3.3(vite@8.3.0(@types/node@24.13.6)(jiti@2.7.0)(tsx@4.20.6)(yaml@2.8.2)) + version: 4.3.3(vite@8.3.0(@types/node@24.13.6)(jiti@2.7.0)(tsx@4.20.6)(yaml@2.9.1)) '@tanstack/devtools-vite': specifier: ^0.8.5 - version: 0.8.5(@emnapi/core@1.11.0)(@emnapi/runtime@1.11.0)(vite@8.3.0(@types/node@24.13.6)(jiti@2.7.0)(tsx@4.20.6)(yaml@2.8.2)) + version: 0.8.5(@emnapi/core@1.11.0)(@emnapi/runtime@1.11.0)(vite@8.3.0(@types/node@24.13.6)(jiti@2.7.0)(tsx@4.20.6)(yaml@2.9.1)) '@tanstack/eslint-plugin-router': specifier: ^1.162.0 version: 1.162.0(@typescript/typescript6@6.0.2)(eslint@10.4.1(jiti@2.7.0)) @@ -71,7 +71,7 @@ importers: version: typescript@7.0.2 '@vitejs/plugin-react': specifier: ^6.1.1 - version: 6.1.1(oxc-transform-react@0.145.0)(vite@8.3.0(@types/node@24.13.6)(jiti@2.7.0)(tsx@4.20.6)(yaml@2.8.2)) + version: 6.1.1(oxc-transform-react@0.145.0)(vite@8.3.0(@types/node@24.13.6)(jiti@2.7.0)(tsx@4.20.6)(yaml@2.9.1)) '@vitest/coverage-v8': specifier: ^4.1.11 version: 4.1.11(vitest@4.1.11) @@ -128,13 +128,16 @@ importers: version: '@typescript/typescript6@6.0.2' vite: specifier: ^8.3.0 - version: 8.3.0(@types/node@24.13.6)(jiti@2.7.0)(tsx@4.20.6)(yaml@2.8.2) + version: 8.3.0(@types/node@24.13.6)(jiti@2.7.0)(tsx@4.20.6)(yaml@2.9.1) vitest: specifier: ^4.1.11 - version: 4.1.11(@types/node@24.13.6)(@vitest/coverage-v8@4.1.11)(jsdom@30.1.0)(vite@8.3.0(@types/node@24.13.6)(jiti@2.7.0)(tsx@4.20.6)(yaml@2.8.2)) + version: 4.1.11(@types/node@24.13.6)(@vitest/coverage-v8@4.1.11)(jsdom@30.1.0)(vite@8.3.0(@types/node@24.13.6)(jiti@2.7.0)(tsx@4.20.6)(yaml@2.9.1)) vitest-axe: specifier: ^0.1.0 version: 0.1.0(vitest@4.1.11) + yaml: + specifier: ^2.9.0 + version: 2.9.1 packages: @@ -3616,8 +3619,8 @@ packages: yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} - yaml@2.8.2: - resolution: {integrity: sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==} + yaml@2.9.1: + resolution: {integrity: sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==} engines: {node: '>= 14.6'} hasBin: true @@ -4500,12 +4503,12 @@ snapshots: '@tailwindcss/oxide-win32-arm64-msvc': 4.3.3 '@tailwindcss/oxide-win32-x64-msvc': 4.3.3 - '@tailwindcss/vite@4.3.3(vite@8.3.0(@types/node@24.13.6)(jiti@2.7.0)(tsx@4.20.6)(yaml@2.8.2))': + '@tailwindcss/vite@4.3.3(vite@8.3.0(@types/node@24.13.6)(jiti@2.7.0)(tsx@4.20.6)(yaml@2.9.1))': dependencies: '@tailwindcss/node': 4.3.3 '@tailwindcss/oxide': 4.3.3 tailwindcss: 4.3.3 - vite: 8.3.0(@types/node@24.13.6)(jiti@2.7.0)(tsx@4.20.6)(yaml@2.8.2) + vite: 8.3.0(@types/node@24.13.6)(jiti@2.7.0)(tsx@4.20.6)(yaml@2.9.1) '@tanstack/devtools-bundler-core@0.1.3(@emnapi/core@1.11.0)(@emnapi/runtime@1.11.0)': dependencies: @@ -4544,13 +4547,13 @@ snapshots: transitivePeerDependencies: - csstype - '@tanstack/devtools-vite@0.8.5(@emnapi/core@1.11.0)(@emnapi/runtime@1.11.0)(vite@8.3.0(@types/node@24.13.6)(jiti@2.7.0)(tsx@4.20.6)(yaml@2.8.2))': + '@tanstack/devtools-vite@0.8.5(@emnapi/core@1.11.0)(@emnapi/runtime@1.11.0)(vite@8.3.0(@types/node@24.13.6)(jiti@2.7.0)(tsx@4.20.6)(yaml@2.9.1))': dependencies: '@tanstack/devtools-bundler-core': 0.1.3(@emnapi/core@1.11.0)(@emnapi/runtime@1.11.0) '@tanstack/devtools-client': 0.0.8 '@tanstack/devtools-event-bus': 0.4.3 chalk: 5.6.2 - vite: 8.3.0(@types/node@24.13.6)(jiti@2.7.0)(tsx@4.20.6)(yaml@2.8.2) + vite: 8.3.0(@types/node@24.13.6)(jiti@2.7.0)(tsx@4.20.6)(yaml@2.9.1) transitivePeerDependencies: - '@emnapi/core' - '@emnapi/runtime' @@ -4626,14 +4629,14 @@ snapshots: react: 19.3.0 react-dom: 19.3.0(react@19.3.0) - '@tanstack/react-start-rsc@0.1.55(crossws@0.4.12(srvx@1.0.5))(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(rolldown@1.2.9)(vite@8.3.0(@types/node@24.13.6)(jiti@2.7.0)(tsx@4.20.6)(yaml@2.8.2))': + '@tanstack/react-start-rsc@0.1.55(crossws@0.4.12(srvx@1.0.5))(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(rolldown@1.2.9)(vite@8.3.0(@types/node@24.13.6)(jiti@2.7.0)(tsx@4.20.6)(yaml@2.9.1))': dependencies: '@tanstack/react-router': 1.170.38(react-dom@19.3.0(react@19.3.0))(react@19.3.0) '@tanstack/router-core': 1.171.32 '@tanstack/router-utils': 1.162.3 '@tanstack/start-client-core': 1.170.32 '@tanstack/start-fn-stubs': 1.162.0 - '@tanstack/start-plugin-core': 1.171.46(@tanstack/react-router@1.170.38(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(crossws@0.4.12(srvx@1.0.5))(rolldown@1.2.9)(vite@8.3.0(@types/node@24.13.6)(jiti@2.7.0)(tsx@4.20.6)(yaml@2.8.2)) + '@tanstack/start-plugin-core': 1.171.46(@tanstack/react-router@1.170.38(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(crossws@0.4.12(srvx@1.0.5))(rolldown@1.2.9)(vite@8.3.0(@types/node@24.13.6)(jiti@2.7.0)(tsx@4.20.6)(yaml@2.9.1)) '@tanstack/start-storage-context': 1.167.34 pathe: 2.0.3 react: 19.3.0 @@ -4662,21 +4665,21 @@ snapshots: transitivePeerDependencies: - crossws - '@tanstack/react-start@1.168.56(crossws@0.4.12(srvx@1.0.5))(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(rolldown@1.2.9)(vite@8.3.0(@types/node@24.13.6)(jiti@2.7.0)(tsx@4.20.6)(yaml@2.8.2))': + '@tanstack/react-start@1.168.56(crossws@0.4.12(srvx@1.0.5))(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(rolldown@1.2.9)(vite@8.3.0(@types/node@24.13.6)(jiti@2.7.0)(tsx@4.20.6)(yaml@2.9.1))': dependencies: '@tanstack/react-router': 1.170.38(react-dom@19.3.0(react@19.3.0))(react@19.3.0) '@tanstack/react-start-client': 1.168.36(react-dom@19.3.0(react@19.3.0))(react@19.3.0) - '@tanstack/react-start-rsc': 0.1.55(crossws@0.4.12(srvx@1.0.5))(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(rolldown@1.2.9)(vite@8.3.0(@types/node@24.13.6)(jiti@2.7.0)(tsx@4.20.6)(yaml@2.8.2)) + '@tanstack/react-start-rsc': 0.1.55(crossws@0.4.12(srvx@1.0.5))(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(rolldown@1.2.9)(vite@8.3.0(@types/node@24.13.6)(jiti@2.7.0)(tsx@4.20.6)(yaml@2.9.1)) '@tanstack/react-start-server': 1.167.43(crossws@0.4.12(srvx@1.0.5))(react-dom@19.3.0(react@19.3.0))(react@19.3.0) '@tanstack/router-utils': 1.162.3 '@tanstack/start-client-core': 1.170.32 - '@tanstack/start-plugin-core': 1.171.46(@tanstack/react-router@1.170.38(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(crossws@0.4.12(srvx@1.0.5))(rolldown@1.2.9)(vite@8.3.0(@types/node@24.13.6)(jiti@2.7.0)(tsx@4.20.6)(yaml@2.8.2)) + '@tanstack/start-plugin-core': 1.171.46(@tanstack/react-router@1.170.38(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(crossws@0.4.12(srvx@1.0.5))(rolldown@1.2.9)(vite@8.3.0(@types/node@24.13.6)(jiti@2.7.0)(tsx@4.20.6)(yaml@2.9.1)) '@tanstack/start-server-core': 1.169.37(crossws@0.4.12(srvx@1.0.5)) pathe: 2.0.3 react: 19.3.0 react-dom: 19.3.0(react@19.3.0) optionalDependencies: - vite: 8.3.0(@types/node@24.13.6)(jiti@2.7.0)(tsx@4.20.6)(yaml@2.8.2) + vite: 8.3.0(@types/node@24.13.6)(jiti@2.7.0)(tsx@4.20.6)(yaml@2.9.1) transitivePeerDependencies: - '@farmfe/core' - '@rspack/core' @@ -4726,7 +4729,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@tanstack/router-plugin@1.168.40(@tanstack/react-router@1.170.38(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(rolldown@1.2.9)(vite@8.3.0(@types/node@24.13.6)(jiti@2.7.0)(tsx@4.20.6)(yaml@2.8.2))': + '@tanstack/router-plugin@1.168.40(@tanstack/react-router@1.170.38(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(rolldown@1.2.9)(vite@8.3.0(@types/node@24.13.6)(jiti@2.7.0)(tsx@4.20.6)(yaml@2.9.1))': dependencies: '@babel/core': 7.29.7 '@babel/template': 7.29.7 @@ -4735,11 +4738,11 @@ snapshots: '@tanstack/router-generator': 1.167.38 '@tanstack/router-utils': 1.162.3 chokidar: 5.0.0 - unplugin: 3.4.0(rolldown@1.2.9)(vite@8.3.0(@types/node@24.13.6)(jiti@2.7.0)(tsx@4.20.6)(yaml@2.8.2)) + unplugin: 3.4.0(rolldown@1.2.9)(vite@8.3.0(@types/node@24.13.6)(jiti@2.7.0)(tsx@4.20.6)(yaml@2.9.1)) zod: 4.6.5 optionalDependencies: '@tanstack/react-router': 1.170.38(react-dom@19.3.0(react@19.3.0))(react@19.3.0) - vite: 8.3.0(@types/node@24.13.6)(jiti@2.7.0)(tsx@4.20.6)(yaml@2.8.2) + vite: 8.3.0(@types/node@24.13.6)(jiti@2.7.0)(tsx@4.20.6)(yaml@2.9.1) transitivePeerDependencies: - '@farmfe/core' - '@rspack/core' @@ -4772,7 +4775,7 @@ snapshots: '@tanstack/start-fn-stubs@1.162.0': {} - '@tanstack/start-plugin-core@1.171.46(@tanstack/react-router@1.170.38(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(crossws@0.4.12(srvx@1.0.5))(rolldown@1.2.9)(vite@8.3.0(@types/node@24.13.6)(jiti@2.7.0)(tsx@4.20.6)(yaml@2.8.2))': + '@tanstack/start-plugin-core@1.171.46(@tanstack/react-router@1.170.38(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(crossws@0.4.12(srvx@1.0.5))(rolldown@1.2.9)(vite@8.3.0(@types/node@24.13.6)(jiti@2.7.0)(tsx@4.20.6)(yaml@2.9.1))': dependencies: '@babel/code-frame': 7.29.7 '@babel/core': 7.29.7 @@ -4780,7 +4783,7 @@ snapshots: '@jridgewell/remapping': 2.3.5 '@tanstack/router-core': 1.171.32 '@tanstack/router-generator': 1.167.38 - '@tanstack/router-plugin': 1.168.40(@tanstack/react-router@1.170.38(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(rolldown@1.2.9)(vite@8.3.0(@types/node@24.13.6)(jiti@2.7.0)(tsx@4.20.6)(yaml@2.8.2)) + '@tanstack/router-plugin': 1.168.40(@tanstack/react-router@1.170.38(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(rolldown@1.2.9)(vite@8.3.0(@types/node@24.13.6)(jiti@2.7.0)(tsx@4.20.6)(yaml@2.9.1)) '@tanstack/router-utils': 1.162.3 '@tanstack/start-server-core': 1.169.37(crossws@0.4.12(srvx@1.0.5)) exsolve: 1.1.1 @@ -4792,11 +4795,11 @@ snapshots: srvx: 0.11.22 tinyglobby: 0.2.17 ufo: 1.6.4 - vitefu: 1.1.3(vite@8.3.0(@types/node@24.13.6)(jiti@2.7.0)(tsx@4.20.6)(yaml@2.8.2)) + vitefu: 1.1.3(vite@8.3.0(@types/node@24.13.6)(jiti@2.7.0)(tsx@4.20.6)(yaml@2.9.1)) xmlbuilder2: 4.0.3 zod: 4.6.5 optionalDependencies: - vite: 8.3.0(@types/node@24.13.6)(jiti@2.7.0)(tsx@4.20.6)(yaml@2.8.2) + vite: 8.3.0(@types/node@24.13.6)(jiti@2.7.0)(tsx@4.20.6)(yaml@2.9.1) transitivePeerDependencies: - '@farmfe/core' - '@rspack/core' @@ -4852,7 +4855,7 @@ snapshots: picocolors: 1.1.1 redent: 3.0.0 optionalDependencies: - vitest: 4.1.11(@types/node@24.13.6)(@vitest/coverage-v8@4.1.11)(jsdom@30.1.0)(vite@8.3.0(@types/node@24.13.6)(jiti@2.7.0)(tsx@4.20.6)(yaml@2.8.2)) + vitest: 4.1.11(@types/node@24.13.6)(@vitest/coverage-v8@4.1.11)(jsdom@30.1.0)(vite@8.3.0(@types/node@24.13.6)(jiti@2.7.0)(tsx@4.20.6)(yaml@2.9.1)) '@testing-library/react@16.3.3(@testing-library/dom@10.4.2)(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)': dependencies: @@ -5015,10 +5018,10 @@ snapshots: dependencies: '@typescript/old': typescript@6.0.3 - '@vitejs/plugin-react@6.1.1(oxc-transform-react@0.145.0)(vite@8.3.0(@types/node@24.13.6)(jiti@2.7.0)(tsx@4.20.6)(yaml@2.8.2))': + '@vitejs/plugin-react@6.1.1(oxc-transform-react@0.145.0)(vite@8.3.0(@types/node@24.13.6)(jiti@2.7.0)(tsx@4.20.6)(yaml@2.9.1))': dependencies: '@rolldown/pluginutils': 1.0.1 - vite: 8.3.0(@types/node@24.13.6)(jiti@2.7.0)(tsx@4.20.6)(yaml@2.8.2) + vite: 8.3.0(@types/node@24.13.6)(jiti@2.7.0)(tsx@4.20.6)(yaml@2.9.1) optionalDependencies: oxc-transform-react: 0.145.0 @@ -5034,7 +5037,7 @@ snapshots: obug: 2.1.1 std-env: 4.1.0 tinyrainbow: 3.1.0 - vitest: 4.1.11(@types/node@24.13.6)(@vitest/coverage-v8@4.1.11)(jsdom@30.1.0)(vite@8.3.0(@types/node@24.13.6)(jiti@2.7.0)(tsx@4.20.6)(yaml@2.8.2)) + vitest: 4.1.11(@types/node@24.13.6)(@vitest/coverage-v8@4.1.11)(jsdom@30.1.0)(vite@8.3.0(@types/node@24.13.6)(jiti@2.7.0)(tsx@4.20.6)(yaml@2.9.1)) '@vitest/expect@4.1.11': dependencies: @@ -5045,13 +5048,13 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.11(vite@8.3.0(@types/node@24.13.6)(jiti@2.7.0)(tsx@4.20.6)(yaml@2.8.2))': + '@vitest/mocker@4.1.11(vite@8.3.0(@types/node@24.13.6)(jiti@2.7.0)(tsx@4.20.6)(yaml@2.9.1))': dependencies: '@vitest/spy': 4.1.11 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 8.3.0(@types/node@24.13.6)(jiti@2.7.0)(tsx@4.20.6)(yaml@2.8.2) + vite: 8.3.0(@types/node@24.13.6)(jiti@2.7.0)(tsx@4.20.6)(yaml@2.9.1) '@vitest/pretty-format@4.1.11': dependencies: @@ -6519,14 +6522,14 @@ snapshots: unicorn-magic@0.4.0: {} - unplugin@3.4.0(rolldown@1.2.9)(vite@8.3.0(@types/node@24.13.6)(jiti@2.7.0)(tsx@4.20.6)(yaml@2.8.2)): + unplugin@3.4.0(rolldown@1.2.9)(vite@8.3.0(@types/node@24.13.6)(jiti@2.7.0)(tsx@4.20.6)(yaml@2.9.1)): dependencies: '@jridgewell/remapping': 2.3.5 picomatch: 4.0.7 webpack-virtual-modules: 0.6.2 optionalDependencies: rolldown: 1.2.9 - vite: 8.3.0(@types/node@24.13.6)(jiti@2.7.0)(tsx@4.20.6)(yaml@2.8.2) + vite: 8.3.0(@types/node@24.13.6)(jiti@2.7.0)(tsx@4.20.6)(yaml@2.9.1) unstorage@2.0.0-alpha.10: {} @@ -6546,7 +6549,7 @@ snapshots: util-deprecate@1.0.2: {} - vite@8.3.0(@types/node@24.13.6)(jiti@2.7.0)(tsx@4.20.6)(yaml@2.8.2): + vite@8.3.0(@types/node@24.13.6)(jiti@2.7.0)(tsx@4.20.6)(yaml@2.9.1): dependencies: lightningcss: 1.33.0 picomatch: 4.0.7 @@ -6558,11 +6561,11 @@ snapshots: fsevents: 2.3.3 jiti: 2.7.0 tsx: 4.20.6 - yaml: 2.8.2 + yaml: 2.9.1 - vitefu@1.1.3(vite@8.3.0(@types/node@24.13.6)(jiti@2.7.0)(tsx@4.20.6)(yaml@2.8.2)): + vitefu@1.1.3(vite@8.3.0(@types/node@24.13.6)(jiti@2.7.0)(tsx@4.20.6)(yaml@2.9.1)): optionalDependencies: - vite: 8.3.0(@types/node@24.13.6)(jiti@2.7.0)(tsx@4.20.6)(yaml@2.8.2) + vite: 8.3.0(@types/node@24.13.6)(jiti@2.7.0)(tsx@4.20.6)(yaml@2.9.1) vitest-axe@0.1.0(vitest@4.1.11): dependencies: @@ -6572,12 +6575,12 @@ snapshots: dom-accessibility-api: 0.5.16 lodash-es: 4.18.1 redent: 3.0.0 - vitest: 4.1.11(@types/node@24.13.6)(@vitest/coverage-v8@4.1.11)(jsdom@30.1.0)(vite@8.3.0(@types/node@24.13.6)(jiti@2.7.0)(tsx@4.20.6)(yaml@2.8.2)) + vitest: 4.1.11(@types/node@24.13.6)(@vitest/coverage-v8@4.1.11)(jsdom@30.1.0)(vite@8.3.0(@types/node@24.13.6)(jiti@2.7.0)(tsx@4.20.6)(yaml@2.9.1)) - vitest@4.1.11(@types/node@24.13.6)(@vitest/coverage-v8@4.1.11)(jsdom@30.1.0)(vite@8.3.0(@types/node@24.13.6)(jiti@2.7.0)(tsx@4.20.6)(yaml@2.8.2)): + vitest@4.1.11(@types/node@24.13.6)(@vitest/coverage-v8@4.1.11)(jsdom@30.1.0)(vite@8.3.0(@types/node@24.13.6)(jiti@2.7.0)(tsx@4.20.6)(yaml@2.9.1)): dependencies: '@vitest/expect': 4.1.11 - '@vitest/mocker': 4.1.11(vite@8.3.0(@types/node@24.13.6)(jiti@2.7.0)(tsx@4.20.6)(yaml@2.8.2)) + '@vitest/mocker': 4.1.11(vite@8.3.0(@types/node@24.13.6)(jiti@2.7.0)(tsx@4.20.6)(yaml@2.9.1)) '@vitest/pretty-format': 4.1.11 '@vitest/runner': 4.1.11 '@vitest/snapshot': 4.1.11 @@ -6594,7 +6597,7 @@ snapshots: tinyexec: 1.2.3 tinyglobby: 0.2.17 tinyrainbow: 3.1.0 - vite: 8.3.0(@types/node@24.13.6)(jiti@2.7.0)(tsx@4.20.6)(yaml@2.8.2) + vite: 8.3.0(@types/node@24.13.6)(jiti@2.7.0)(tsx@4.20.6)(yaml@2.9.1) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 24.13.6 @@ -6676,8 +6679,7 @@ snapshots: yallist@3.1.1: {} - yaml@2.8.2: - optional: true + yaml@2.9.1: {} yargs-parser@22.0.0: {} diff --git a/src/lib/supply-chain-config.test.ts b/src/lib/supply-chain-config.test.ts new file mode 100644 index 0000000..37083d2 --- /dev/null +++ b/src/lib/supply-chain-config.test.ts @@ -0,0 +1,42 @@ +import { readFileSync } from "node:fs"; +import path from "node:path"; +import process from "node:process"; + +import { describe, expect, it } from "vitest"; +import { parse } from "yaml"; + +// Guard the supply-chain settings in pnpm-workspace.yaml against silent +// regression. pnpm ignores unknown keys, so a removed or misspelled setting +// disables its protection with no error -- these assertions fail CI instead. + +const config = parse(readFileSync(path.join(process.cwd(), "pnpm-workspace.yaml"), "utf8")) as { + trustPolicy?: string; + blockExoticSubdeps?: boolean; + minimumReleaseAge?: number; + minimumReleaseAgeStrict?: boolean; + dangerouslyAllowAllBuilds?: boolean; +}; + +describe("pnpm supply-chain config", () => { + it("blocks trust-level downgrades", () => { + expect(config.trustPolicy).toBe("no-downgrade"); + }); + + it("blocks transitive git/tarball dependencies", () => { + expect(config.blockExoticSubdeps).toBe(true); + }); + + it("enforces at least a 7-day release-age cooldown", () => { + expect(config.minimumReleaseAge).toBeGreaterThanOrEqual(10080); + }); + + it("fails resolution instead of falling back to immature versions", () => { + // Defaults to true only while minimumReleaseAge is set explicitly. + expect(config.minimumReleaseAgeStrict).not.toBe(false); + }); + + it("does not globally allow dependency build scripts", () => { + // allowBuilds is an explicit allowlist; this catches the escape hatch. + expect(config.dangerouslyAllowAllBuilds).not.toBe(true); + }); +}); From 2645a1fe3f40048f5d43edc32eae1b08eeee70e4 Mon Sep 17 00:00:00 2001 From: Callum Howard Date: Mon, 28 Sep 2026 09:01:47 +1000 Subject: [PATCH 4/4] Set minimumReleaseAgeStrict explicitly and guard it The implicit default only holds while minimumReleaseAge is set, and the previous `not.toBe(false)` guard also passed when the key was absent. Co-Authored-By: Claude Opus 5.5 (1M context) --- pnpm-workspace.yaml | 2 ++ src/lib/supply-chain-config.test.ts | 3 +-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index cf94370..61ff136 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -4,6 +4,8 @@ allowBuilds: unrs-resolver: true # Ignore versions published < 7 days ago, giving time to vet/yank releases. minimumReleaseAge: 10080 +# Fail resolution rather than fall back to a version inside the cooldown. +minimumReleaseAgeStrict: true # Block versions with weaker trust attestation than prior releases. trustPolicy: no-downgrade trustPolicyExclude: diff --git a/src/lib/supply-chain-config.test.ts b/src/lib/supply-chain-config.test.ts index 37083d2..ac5b301 100644 --- a/src/lib/supply-chain-config.test.ts +++ b/src/lib/supply-chain-config.test.ts @@ -31,8 +31,7 @@ describe("pnpm supply-chain config", () => { }); it("fails resolution instead of falling back to immature versions", () => { - // Defaults to true only while minimumReleaseAge is set explicitly. - expect(config.minimumReleaseAgeStrict).not.toBe(false); + expect(config.minimumReleaseAgeStrict).toBe(true); }); it("does not globally allow dependency build scripts", () => {