fix(security): resolve 82 dependency advisories and harden CI - #115
Merged
Conversation
The committed yarn.lock had drifted far behind the semver ranges declared in package.json, so `yarn audit` on the production tree reported 82 advisories (19 critical, 25 high) even though every one of them already had a compatible patched release available. Refreshing the lockfile within the existing ranges, plus four targeted `resolutions` for transitive packages whose parents pin vulnerable versions, brings the production tree to 4 low advisories -- all four are the same `elliptic` issue, which has no patched release upstream and is reachable only through partisia-blockchain-applications-crypto and @partisiablockchain/abi-client. Dependencies: - Refresh yarn.lock within existing semver ranges. - Pin minimatch ^9.0.7 (ReDoS, GHSA-3ppc-4f35-3m26 and two others). - Pin elliptic ^6.6.1 and the nested bn.js copies under ethjs-unit and number-to-bn to ^4.12.3 (infinite loop, GHSA-378v-28hj-76wf). CI: - Add a production-dependency audit gate to the main, pull_request and merge_group workflows, and to release before publish. `yarn audit` ignores --level when setting its exit code and instead returns a severity bitmask, so scripts/audit-ci.sh masks for high and critical rather than treating any non-zero exit as a failure. - Pin romeovs/lcov-reporter-action and JamesIves/github-pages-deploy-action to commit SHAs. Both were referenced by mutable tags. - Replace `permissions: write-all` on the pull_request test job with contents:read + pull-requests:write, and narrow main.yml from a top-level contents:write to contents:read, granting contents:write only to the docs job that pushes to gh-pages. - Actually emit provenance on publish. The workflow already requested id-token:write for OIDC but ran a plain `npm publish`, so no provenance attestation was ever produced. Also declare engines.node >=20 and exclude coverage/, dist/ and docs/ in tsconfig.json -- a local coverage run previously left stray JS outside rootDir and broke `yarn tsc`. Claude-Session: https://claude.ai/code/session_01GceWCwGXu66D1xEBDxZWBb
allowJs picks up scripts/*.js, and later branches add files there. Keeping the exclude list identical across branches means the three PRs that touch this line merge without a conflict. Claude-Session: https://claude.ai/code/session_01GceWCwGXu66D1xEBDxZWBb
Domain transfer was budgeted at 100_000 gas, which the contract exhausted: the chain reported "Out of instruction cycles! 98750191/98710000 instructions run" from avl_get_next inside action_03. The margin was under 0.05%, so the transfer test passed or failed depending on the state of the AVL tree, which is what made CI intermittently red. Unused gas is refunded on PBC, so raising the extra-high tier to 200_000 costs nothing in practice. Every test in the suite drives the live testnet and waits for a transaction to finalize. The per-test timeouts (10-20s) were tuned on a local machine and left no room on a CI runner, timing out "mint subdomain without parent". Replaced them with a single global 30s testTimeout. Claude-Session: https://claude.ai/code/session_01GceWCwGXu66D1xEBDxZWBb
Under jsdom, axios selects its XMLHttpRequest adapter. Requests through it intermittently failed with "read ETIMEDOUT", which timed a record test out while a transaction was being broadcast; the next transaction from the same wallet then reused a spent nonce and the reader node rejected it with 400 Bad Request, taking the domain action tests down with it. Nothing in the suite touches the DOM: it drives the SDK the way a Node consumer does. On the node environment axios uses the http adapter and the full suite passes. Claude-Session: https://claude.ai/code/session_01GceWCwGXu66D1xEBDxZWBb
…ected The nonce is read from a reader node, which trails the chain: a transaction signed right after another one, or from a wallet that is in use elsewhere at the same time, can carry a nonce the chain has already spent. The node rejects the broadcast with 400 Bad Request, which surfaced in CI as an unexplained "400 Bad Request" from whichever test happened to sign at that moment. A rejected transaction never reaches the chain, so reading the nonce again and signing again is safe and costs nothing. Retry up to three times with a short back-off, and rethrow the last error if the node keeps refusing. Claude-Session: https://claude.ai/code/session_01GceWCwGXu66D1xEBDxZWBb
|
Total Coverage: 87.34% Coverage Report
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
yeboster
added a commit
that referenced
this pull request
Aug 28, 2026
node-fetch was the single largest entry in the bundle at 302 KB minified, and pulling it in also dragged along whatwg-url, form-data and mime-db. Every runtime this package supports already ships a global fetch: it is stable in Node from v18 and available in every browser target, so the dependency bought nothing. Bundling `dist/index.js` with esbuild (minified, esm): before 1,685,983 bytes (438,782 gzipped) after 1,333,533 bytes (345,961 gzipped) delta -352,450 bytes (-20.9%), -92,821 gzipped (-21.2%) The saving exceeds node-fetch's own 302 KB because form-data and mime-db leave with it. Alongside the swap: - Add a 30s AbortSignal timeout to requests. `promiseRetry` only advances when the underlying promise settles, so a request that never settled previously pinned the whole retry chain open indefinitely. - Remove the dead FetchError branch in MetaNamesContractRepository. It could never run for two independent reasons: the guarded call was returned without `await`, so rejections escaped the try/catch entirely, and node-fetch sets FetchError#code for system errors such as ENOTFOUND rather than HTTP statuses, so `code === '404'` never held. A missing AVL value is already reported as `undefined` by `getRequest`, which resolves a body only on HTTP 200. The call is now awaited and transport failures are logged via console.error. - Drop jest-environment-jsdom. main already runs the suite on the node environment; jsdom does not implement fetch either, so the dependency is now dead weight and its removal is what the environment switch was for. Rebased onto main after #115. Full suite is 255/255 passing; test wall time for the lookup suite dropped from 40s (timing out) to 1.2s. Requires Node >= 20, already declared in engines. Claude-Session: https://claude.ai/code/session_01GceWCwGXu66D1xEBDxZWBb
yeboster
added a commit
that referenced
this pull request
Aug 28, 2026
node-fetch was the single largest entry in the bundle at 302 KB minified, and pulling it in also dragged along whatwg-url, form-data and mime-db. Every runtime this package supports already ships a global fetch: it is stable in Node from v18 and available in every browser target, so the dependency bought nothing. Bundling `dist/index.js` with esbuild (minified, esm): before 1,685,983 bytes (438,782 gzipped) after 1,333,533 bytes (345,961 gzipped) delta -352,450 bytes (-20.9%), -92,821 gzipped (-21.2%) The saving exceeds node-fetch's own 302 KB because form-data and mime-db leave with it. Alongside the swap: - Add a 30s AbortSignal timeout to requests. `promiseRetry` only advances when the underlying promise settles, so a request that never settled previously pinned the whole retry chain open indefinitely. - Remove the dead FetchError branch in MetaNamesContractRepository. It could never run for two independent reasons: the guarded call was returned without `await`, so rejections escaped the try/catch entirely, and node-fetch sets FetchError#code for system errors such as ENOTFOUND rather than HTTP statuses, so `code === '404'` never held. A missing AVL value is already reported as `undefined` by `getRequest`, which resolves a body only on HTTP 200. The call is now awaited and transport failures are logged via console.error. - Drop jest-environment-jsdom. main already runs the suite on the node environment; jsdom does not implement fetch either, so the dependency is now dead weight and its removal is what the environment switch was for. Rebased onto main after #115. Full suite is 255/255 passing; test wall time for the lookup suite dropped from 40s (timing out) to 1.2s. Requires Node >= 20, already declared in engines. Claude-Session: https://claude.ai/code/session_01GceWCwGXu66D1xEBDxZWBb
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
yarn auditon the production dependency tree reported 82 advisories (19 critical, 25 high) before this change. Nearly all of them were phantom: the committedyarn.lockhad drifted well behind the semver ranges already declared inpackage.json, so patched versions were available but never installed.Result: 82 advisories → 4 low.
The 4 remaining are all the same
ellipticadvisory reached by different paths. It has no patched release upstream and enters only viapartisia-blockchain-applications-cryptoand@partisiablockchain/abi-client. The follow-up PR that moves the signing backends to optional peer dependencies removes three of those four paths from this package's own graph.Dependency changes
yarn.lockwithin the existing semver ranges — no declared range was widened.resolutionsfor transitive packages whose parents pin vulnerable versions:minimatch@^9.0.7— ReDoS (GHSA-3ppc-4f35-3m26,GHSA-7r86-cg39-jmmj,GHSA-23c5-xmqv-rm74)elliptic@^6.6.1**/ethjs-unit/bn.jsand**/number-to-bn/bn.js→^4.12.3— infinite loop (GHSA-378v-28hj-76wf)CI hardening
main,pull_request,merge_group, and toreleasebefore publish.yarn auditignores--levelwhen setting its exit code and instead returns a severity bitmask (1=info, 2=low, 4=moderate, 8=high, 16=critical).scripts/audit-ci.shmasks for high+critical rather than treating any non-zero exit as failure — otherwise the job would fail permanently on the unpatchableellipticlow.romeovs/lcov-reporter-action@v0.2.11andJamesIves/github-pages-deploy-action@v4were both referenced by mutable tags; the latter is a floating major.pull_requesttest job ran withpermissions: write-allwhile passingsecrets.GITHUB_TOKENto a third-party action — nowcontents: read+pull-requests: write.main.ymldropped from a top-levelcontents: writetocontents: read, withcontents: writegranted only to the docs job that pushes togh-pages.release.ymlalready requestedid-token: writefor OIDC but then ran a barenpm publish, so no attestation was ever produced. Nownpm publish --provenance --access public.Also
engines.node: ">=20".coverage/,dist/,docs/intsconfig.json. A local coverage run left stray JS outsiderootDirand brokeyarn tscwithTS6059.Verification
yarn audit-ci→ passes, bitmask 2 (low only).npx tsc --noEmit→ clean (was failing locally before the tsconfig fix).npx jest -i→ 253/255 passing, up from 234/255 onmain.The 2 failures are live-testnet integration suites that broadcast real transactions; they are flaky independent of this change — different suites failed on consecutive baseline runs of unmodified
main.No source files under
src/were touched. No public API change.https://claude.ai/code/session_01GceWCwGXu66D1xEBDxZWBb