Skip to content

Add dependency cooldown and remediate vulnerabilities - #200

Draft
miguelcalderon wants to merge 5 commits into
masterfrom
miguel/dependency-cooldown
Draft

Add dependency cooldown and remediate vulnerabilities#200
miguelcalderon wants to merge 5 commits into
masterfrom
miguel/dependency-cooldown

Conversation

@miguelcalderon

@miguelcalderon miguelcalderon commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Summary

  • add a 15-day release cooldown (min-release-age for npm, minimumReleaseAge for pnpm) to every independently installable example
  • remediate known dependency vulnerabilities across all example lockfiles
  • scope cooldown exclusions to the pinned security-remediation versions so fresh resolution and npm audit fix keep 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)
  • pin the toolchain that enforces the policy — Node 24.19.0 / npm 11.17.0 locally via .tool-versions and in CI via node-version-file, pnpm 11.20.0 via packageManager — and add a Dependency policy workflow 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 applies minimumReleaseAge at 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:

  • js-yaml CVE-2026-59870 (GHSA-5p4m-2wfm-xmqj, quadratic CPU in !!omap resolution, 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 <5 so eslint's ^4.x dependency doesn't jump to js-yaml 5.
  • eslint-config-next in signing-demo-complete bumped to ^16.2.11 (mature under the cooldown), removing the last exact pin and closing most of the skew with next 16.3.0.

If this PR sits unmerged for a few days, re-run npm audit across the examples before merging — new advisories may have appeared, and the resolved versions here go stale.

@miguelcalderon miguelcalderon self-assigned this Aug 5, 2026
@miguelcalderon
miguelcalderon requested review from a team, MahmoudElsayad and ritz078 August 5, 2026 09:21
@miguelcalderon
miguelcalderon marked this pull request as ready for review August 5, 2026 09:21

@ritz078 ritz078 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread web/signing/signing-demo-complete/pnpm-workspace.yaml
Comment thread web/ui-customization/pnpm-workspace.yaml
Comment thread web/ui-customization-doc-editor-sidebar/pnpm-workspace.yaml
Comment thread web/signing/signing-demo-complete/pnpm-workspace.yaml Outdated
Comment thread web/signing/signing-demo-complete/pnpm-workspace.yaml Outdated
Comment thread web/signing/signing-demo-complete/package.json Outdated
Comment thread web/ui-customization/package.json Outdated
Comment thread web/ui-customization/pnpm-workspace.yaml Outdated
Comment thread .npmrc Outdated
@miguelcalderon
miguelcalderon marked this pull request as draft August 6, 2026 05:34
- 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
@miguelcalderon

Copy link
Copy Markdown
Contributor Author

Replying to the review-level items from #200 (review) — all addressed in f0883d5:

  • npm audit fix broken under the cooldown: the example .npmrc files now carry min-release-age-exclude entries scoped to the pinned security-remediation packages, so the remediation versions are reachable by the repo's own tooling again during the window. The exclusions are removable after 2026-08-18 (dated TODO in the root .npmrc).
  • .tool-versions vs CI disagreement: .tool-versions is bumped to Node 24.19.0 (npm 11.17.0), and the typecheck workflow now uses node-version-file: '.tool-versions' instead of lts/*, so local and CI enforce the same cooldown.
  • Lockfiles generated with the cooldown not in effect: the three pnpm lockfiles were regenerated under the pinned toolchain, and a new Dependency policy workflow verifies each committed lockfile against the supply-chain policy (pnpm install --lockfile-only --frozen-lockfile) and keeps the duplicated npm/pnpm override maps identical — all jobs green on this PR.
  • "preserve the protected cache-related dependency versions": that phrasing was inaccurate — there is no such policy file, and no cache-related pins to check. The PR description has been rewritten without it.

@miguelcalderon
miguelcalderon marked this pull request as ready for review August 6, 2026 07:16
@miguelcalderon
miguelcalderon requested a review from ritz078 August 6, 2026 07:17
miguelcalderon and others added 2 commits August 7, 2026 12:28
- 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 ritz078 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Three things that have no single line to hang off:

  • The audit claim in the description is stale. Re-running npm audit today 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's paths: 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 has min-release-age set would close that.
  • The npm side has no lockfile verification job, and can't easily get one. pnpm-lockfile-policy covers pnpm; there is no npm equivalent, and npm ci is not a substitute — npm applies min-release-age only at resolution time, so npm ci, npm install, and npm install --package-lock-only all install a cooldown-bypassing package-lock.json without 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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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):

Suggested change
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
- 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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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/**",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
ignore: "**/node_modules/**",
exclude: (path) => path.includes("node_modules"),

Comment on lines +41 to +47
# 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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No permissions: block, so both jobs inherit the repository default token scope. Neither job writes anything.

Suggested change
on:
permissions:
contents: read
on:

Comment thread .tool-versions
@@ -1 +1 @@
nodejs 22.12.0
nodejs 24.19.0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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`.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"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.

@miguelcalderon
miguelcalderon marked this pull request as draft August 13, 2026 08:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants