Add dependency cooldown and remediate vulnerabilities - #200
Add dependency cooldown and remediate vulnerabilities#200miguelcalderon wants to merge 5 commits into
Conversation
ritz078
left a comment
There was a problem hiding this comment.
Three cross-cutting items with no single line to anchor to.
npm audit fix no longer works in these examples. A/B on web/ui-customization, npm 12.0.2, no lockfile, identical inputs except the cooldown:
| exit | result | |
|---|---|---|
with min-release-age=15 |
1 | 2 vulnerabilities (1 moderate, 1 high), prints fix available via 'npm audit fix' |
| control | 0 | found 0 vulnerabilities |
The control resolves to exactly the versions this PR hand-pins, so the cooldown blocks the remediation from being reachable by the repo's own tooling, and the retry hint loops with no exit. The surviving vulns are this PR's own targets (brace-expansion high, postcss moderate). Scoping note: npm ci from the committed lockfiles is unaffected across all 25 npm examples — npm never validates a lockfile against the age window, only pnpm 11 does, which is why the breakage is confined to the three pnpm examples.
.tool-versions and CI disagree about whether the cooldown exists. nodejs 22.12.0 → npm 10.9.0, which predates the feature; CI's lts/* → Node 24.19.0 → npm 11.17.0, which enforces it. A contributor following the repo's own pin regenerates lockfiles with no cooldown applied and can't reproduce any of this. Bumping .tool-versions and pinning the workflow to the same version would make the policy enforce identically in both places.
The committed lockfiles were generated with the cooldown not in effect — they introduce entries 2 days old (nanoid@3.3.17, next@16.3.0 + 9 @next/*, fast-uri@3.1.5, @emnapi/runtime@1.11.3). Harmless for npm ci, but it means the artifacts can't be reproduced by anyone honoring the policy this same commit adds.
Also — could you spell out what "preserve the protected cache-related dependency versions" refers to? I couldn't find a policy file or anything in AGENTS.md, and no cache-adjacent package (flatted, flat-cache, lru-cache, caniuse-lite) changed version in any of the 26 lockfiles, so there's nothing for a reviewer to check against.
- Scope min-release-age-exclude / minimumReleaseAgeExclude to the pinned security-remediation versions so fresh resolution, npm audit fix, and pnpm's lockfile supply-chain policy all pass under the 15-day cooldown; document the npm version requirements (11.10.0+ for min-release-age, 11.17.0+ for the exclusions) and the intentional npm/pnpm exclusion asymmetry, with a dated TODO to drop the exclusions after 2026-08-18. - Migrate the three dual npm/pnpm examples to pnpm 11 config: move package.json#pnpm.overrides to pnpm-workspace.yaml, mirror the full map into package.json#overrides for npm, bound the cross-major sharp/postcss ranges, replace the exact next override with a direct ^16.3.0 dependency, and regenerate the lockfiles under the pinned managers. - Pin the enforcing toolchain: Node 24.19.0 in .tool-versions, the typecheck workflow reading it via node-version-file, and packageManager pnpm@11.20.0 in the dual-manager examples. - Add a Dependency policy workflow that keeps the duplicated npm/pnpm override maps identical and verifies the committed pnpm lockfiles against the supply-chain policy. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> 🔮 View transcript: https://nutrient-agentlogs.dev/s/dkd0hknzkpeagh2nhsbz1p1m
|
Replying to the review-level items from #200 (review) — all addressed in f0883d5:
|
- check-override-sync.mjs discovers dual-manager examples by globbing for directories committing both lockfiles instead of a hardcoded list, and fails when a discovered example lacks a pnpm-workspace.yaml to mirror overrides into. - Rename the pnpm CI step to say what it verifies (lockfile vs manifest and overrides) and document that a frozen install does not re-check minimumReleaseAge. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> 🔮 View transcript: https://nutrient-agentlogs.dev/s/crp7fpnzsx3aup8to3tobdfs
Today's npm audit flags js-yaml <=4.3.0 (GHSA-5p4m-2wfm-xmqj, quadratic CPU in !!omap resolution) in 10 examples. The fix, 4.3.1, was published 2026-07-31 and is still inside the 15-day cooldown, so it gets the same exclusion treatment as the existing remediations; the exclusions become removable on 2026-08-15. - Add js-yaml to min-release-age-exclude / minimumReleaseAgeExclude in the affected examples. - Widen the dual-manager override rule to js-yaml@>=4.0.0 <4.3.1 => ">=4.3.1 <5" (bounded to 4.x so eslint's ^4.x dependency does not jump to js-yaml 5), subsuming the earlier <=4.1.1 rule. - Regenerate the affected npm and pnpm lockfiles; all 25 examples audit clean again. - Bump eslint-config-next to ^16.2.11 (mature, published 2026-07-21) in signing-demo-complete, removing the last exact pin and closing most of the skew with next 16.3.0. - Extend the root .npmrc TODO with the js-yaml exit date and the override consolidation cleanup. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ritz078
left a comment
There was a problem hiding this comment.
Three things that have no single line to hang off:
- The audit claim in the description is stale. Re-running
npm audittoday against this branch reports advisories in 17 of the 25 examples, not a clean sweep as of 2026-08-07. Worth re-running and refreshing the remediation set before merge, since the overrides were generated from the older audit output. - Nothing enforces that an example has an
.npmrc. The policy is only as good as its coverage, but the new workflow'spaths:filter fires on**/.npmrc— i.e. only when one is touched. A new example added without one silently opts out of the cooldown forever. A CI assertion that every directory with a lockfile also hasmin-release-ageset would close that. - The npm side has no lockfile verification job, and can't easily get one.
pnpm-lockfile-policycovers pnpm; there is no npm equivalent, andnpm ciis not a substitute — npm appliesmin-release-ageonly at resolution time, sonpm ci,npm install, andnpm install --package-lock-onlyall install a cooldown-bypassingpackage-lock.jsonwithout complaint. That asymmetry is worth stating somewhere, because today the pnpm job reads as if both managers are covered.
| # Allow the security-remediation versions already pinned in this example. | ||
| min-release-age-exclude[]=brace-expansion | ||
| min-release-age-exclude[]=js-yaml | ||
| min-release-age-exclude[]=postcss |
There was a problem hiding this comment.
Excluding postcss but not nanoid makes a fresh install fail outright. min-release-age-exclude is scoped to a package name, not to the pinned version — so excluding postcss lets it float to the newest release (8.5.26), which requires nanoid@^3.3.17; nanoid is not excluded and 3.3.17/3.3.18 are inside the 15-day window.
Reproduced in a clean copy of this directory:
[ERR_PNPM_NO_MATURE_MATCHING_VERSION] nanoid@3.3.17 was published at
2026-08-03T10:39:22.487Z, within the minimumReleaseAge cutoff
(2026-07-26T08:26:54.121Z)
The committed lockfiles mask this — anyone who deletes a lockfile, adds a dependency, or runs a lockfile-less install hits it. It self-heals on 2026-08-18, but until then resolution is hard-broken.
Adding nanoid fixes it (verified: resolves to postcss@8.5.26 + nanoid@3.3.18, the patched version for GHSA-2v37-7h3g-55p8):
| min-release-age-exclude[]=postcss | |
| min-release-age-exclude[]=nanoid | |
| min-release-age-exclude[]=postcss |
Same edit is needed in the other 15 .npmrc files that exclude postcss without nanoid.
| minimumReleaseAgeExclude: | ||
| - brace-expansion | ||
| - js-yaml | ||
| - postcss |
There was a problem hiding this comment.
Same missing nanoid exclusion on the pnpm side — minimumReleaseAgeExclude is name-scoped too, so postcss floats to 8.5.26 and drags in an immature nanoid.
| - postcss | |
| - nanoid | |
| - postcss |
Also applies to web/ui-customization-doc-editor-sidebar/pnpm-workspace.yaml.
| # Allow the security-remediation versions already pinned in this example. | ||
| # This list is derived from package-lock.json and intentionally differs from | ||
| # minimumReleaseAgeExclude in pnpm-workspace.yaml: pnpm-lock.yaml pins mature | ||
| # versions of @emnapi/runtime and nanoid, so pnpm does not need those two. |
There was a problem hiding this comment.
This justification doesn't hold: pnpm-lock.yaml in this directory pins nanoid@3.3.16, which is the version affected by GHSA-2v37-7h3g-55p8 (fixed in 3.3.17). package-lock.json pins 3.3.17.
So the two lockfiles disagree on a security fix, and the new pnpm-lockfile-policy job certifies the vulnerable side as passing. Regenerating pnpm-lock.yaml (with nanoid in minimumReleaseAgeExclude) should bring it to 3.3.17+ and make this comment unnecessary.
| }, | ||
| "overrides": { | ||
| "dompurify": "3.4.11" | ||
| "dompurify": "3.4.12", |
There was a problem hiding this comment.
dompurify@3.4.12 is still within the affected range of GHSA-55q2-fjhq-7xh7 (<=3.4.12); the fix is 3.4.13. Bumping from 3.4.11 to 3.4.12 doesn't clear the advisory.
3.4.13 will need a cooldown exclusion if it's inside the window. Four places carry this pin: web/ui-customization/{package.json,pnpm-workspace.yaml} and web/ui-customization-doc-editor-sidebar/{package.json,pnpm-workspace.yaml}.
| // A dual-manager example is any directory (outside node_modules) that commits | ||
| // both an npm and a pnpm lockfile. | ||
| const EXAMPLES = globSync("**/pnpm-lock.yaml", { | ||
| ignore: "**/node_modules/**", |
There was a problem hiding this comment.
fs.globSync has no ignore option — it's exclude. Unknown keys are silently dropped, so this glob currently walks node_modules too. It happens not to bite in CI (fresh checkout, no installs), but it will pick up vendored lockfiles the moment anyone runs the script locally after an install, and then report drift for third-party packages.
| ignore: "**/node_modules/**", | |
| exclude: (path) => path.includes("node_modules"), |
| # Keep in sync with the dual-manager examples (directories committing | ||
| # both package-lock.json and pnpm-lock.yaml). check-override-sync.mjs | ||
| # discovers those automatically; this matrix is maintained by hand. | ||
| example: | ||
| - web/signing/signing-demo-complete | ||
| - web/ui-customization | ||
| - web/ui-customization-doc-editor-sidebar |
There was a problem hiding this comment.
The comment names the problem and then ships it: check-override-sync.mjs already discovers dual-manager examples, so a fourth one added later gets override-sync checking but silently no lockfile-policy checking.
Since the discovery logic exists, emitting the list from the script (--json) into a fromJSON matrix via a small setup job would keep the two in step automatically.
| @@ -0,0 +1,66 @@ | |||
| name: Dependency policy | |||
|
|
|||
| on: | |||
There was a problem hiding this comment.
No permissions: block, so both jobs inherit the repository default token scope. Neither job writes anything.
| on: | |
| permissions: | |
| contents: read | |
| on: |
| @@ -1 +1 @@ | |||
| nodejs 22.12.0 | |||
| nodejs 24.19.0 | |||
There was a problem hiding this comment.
Node 24.19.0 ships npm 11.17.0 — exactly the floor the new .npmrc comments require for min-release-age-exclude. Zero margin: any pin to an older Node here silently downgrades npm below 11.17.0, at which point npm enforces the cooldown while ignoring the exclusions, and every dual-manager example fails to resolve.
An engines.npm: ">=11.17.0" in the root package.json would make that constraint fail loudly instead of turning into a confusing resolution error.
| 3. Run `npm i` to install the project dependencies. This example also ships a pnpm setup (`packageManager: pnpm@11.20.0` plus `pnpm-lock.yaml`); if you have enabled corepack's npm shim (`corepack enable npm`), corepack will refuse `npm` commands here — use `pnpm install` instead, or run npm with `COREPACK_ENABLE_STRICT=0`. | ||
| 4. Copy the Nutrient Web SDK library assets to the public directory by running: | ||
| `cp -R ./node_modules/pspdfkit/dist/pspdfkit-lib public/pspdfkit-lib` | ||
| 5. You should now be able to run the project locally by executing `npm run dev`. |
There was a problem hiding this comment.
Step 3 now warns that corepack may refuse npm in this directory, but steps 4 and 5 still tell the reader to use ./node_modules/... and npm run dev unconditionally. A reader who followed the pnpm branch of step 3 gets contradictory instructions two lines later.
Either carry the caveat through (pnpm dev) or pick one manager as the documented path for this example.
| # security-remediation versions. | ||
| min-release-age=15 | ||
|
|
||
| # Allow the security-remediation versions already pinned in this example. |
There was a problem hiding this comment.
"already pinned in this example" isn't accurate here — this example has no overrides block and no pin for brace-expansion, js-yaml, or postcss; they're transitive and unpinned, so the exclusions let them float to whatever the registry has. Same wording is copied into the other .npmrc files that have no override map.
Worth rewording to what the exclusion actually does ("allow the remediated transitive versions through the cooldown"), since the current text implies a version bound that isn't there.
Summary
min-release-agefor npm,minimumReleaseAgefor pnpm) to every independently installable examplenpm audit fixkeep working during the window; the exclusions are removable after 2026-08-18 (tracked in a dated TODO in the root.npmrc, together with a follow-up to collapse the stacked audit-generated override rules).tool-versionsand in CI vianode-version-file, pnpm 11.20.0 viapackageManager— and add aDependency policyworkflow that keeps the duplicated npm/pnpm override maps in sync (dual-manager examples are auto-discovered by globbing for both lockfiles) and verifies the committed pnpm lockfiles still match their manifests and overrides (note: a frozen install cannot re-check the cooldown itself; pnpm only appliesminimumReleaseAgeat resolution time)Audit state as of 2026-08-07
The audit assessment changes day to day as new advisories land. The latest sweep (all 25 examples with an npm lockfile) is clean:
!!omapresolution, affects ≤4.3.0) surfaced after the branch was first authored and hit 10 examples. Fixed by moving to 4.3.1, which was published 2026-07-31 and is itself inside the cooldown window, so it follows the same exclusion pattern as the other remediations (exclusion removable 2026-08-15). The dual-manager override rule is bounded to>=4.3.1 <5so eslint's^4.xdependency doesn't jump to js-yaml 5.eslint-config-nextin signing-demo-complete bumped to^16.2.11(mature under the cooldown), removing the last exact pin and closing most of the skew withnext16.3.0.If this PR sits unmerged for a few days, re-run
npm auditacross the examples before merging — new advisories may have appeared, and the resolved versions here go stale.