Skip to content

fix(security): resolve 82 dependency advisories and harden CI - #115

Merged
yeboster merged 5 commits into
mainfrom
chore/security-hardening
Aug 28, 2026
Merged

fix(security): resolve 82 dependency advisories and harden CI#115
yeboster merged 5 commits into
mainfrom
chore/security-hardening

Conversation

@yeboster

Copy link
Copy Markdown
Contributor

Summary

yarn audit on the production dependency tree reported 82 advisories (19 critical, 25 high) before this change. Nearly all of them were phantom: the committed yarn.lock had drifted well behind the semver ranges already declared in package.json, so patched versions were available but never installed.

Result: 82 advisories → 4 low.

The 4 remaining are all the same elliptic advisory reached by different paths. It has no patched release upstream and enters only via partisia-blockchain-applications-crypto and @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

  • Refresh yarn.lock within the existing semver ranges — no declared range was widened.
  • Add four resolutions for 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.js and **/number-to-bn/bn.js^4.12.3 — infinite loop (GHSA-378v-28hj-76wf)

CI hardening

  • Audit gate added to main, pull_request, merge_group, and to release before publish.
    yarn audit ignores --level when setting its exit code and instead returns a severity bitmask (1=info, 2=low, 4=moderate, 8=high, 16=critical). scripts/audit-ci.sh masks for high+critical rather than treating any non-zero exit as failure — otherwise the job would fail permanently on the unpatchable elliptic low.
  • Pinned third-party actions to commit SHAs. romeovs/lcov-reporter-action@v0.2.11 and JamesIves/github-pages-deploy-action@v4 were both referenced by mutable tags; the latter is a floating major.
  • Least-privilege tokens. The pull_request test job ran with permissions: write-all while passing secrets.GITHUB_TOKEN to a third-party action — now contents: read + pull-requests: write. main.yml dropped from a top-level contents: write to contents: read, with contents: write granted only to the docs job that pushes to gh-pages.
  • Provenance is now actually emitted. release.yml already requested id-token: write for OIDC but then ran a bare npm publish, so no attestation was ever produced. Now npm publish --provenance --access public.

Also

  • Declare engines.node: ">=20".
  • Exclude coverage/, dist/, docs/ in tsconfig.json. A local coverage run left stray JS outside rootDir and broke yarn tsc with TS6059.

Verification

  • yarn audit-ci → passes, bitmask 2 (low only).
  • npx tsc --noEmit → clean (was failing locally before the tsconfig fix).
  • npx jest -i253/255 passing, up from 234/255 on main.
    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

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
@github-actions

Copy link
Copy Markdown

Total Coverage: 87.34%

Coverage Report
File Branch Funcs Lines Uncovered Lines
src
   actions.ts 100% 100% 100%
   index.ts 100% 100% 100%
   interface.ts 100% 100% 100%
   meta-names-sdk.ts 75% 75% 95.65% 22, 44, 45
   partisia-name-system.ts 58% 87.50% 84.07% 106, 114, 12, 139, 152, 160, 164, 169, 170, 171, 178, 180, 188, 189, 19, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 22, 220, 221, 222, 29, 33, 42, 49, 54, 55, 7, 78, 82
   version.ts 100% 100% 100%
src/actions
   domain.ts 64.29% 100% 100% 35, 44, 56, 71, 74
   record.ts 77.78% 100% 100% 14, 48
src/models
   domain.ts 100% 83.33% 95.95% 45, 46, 47
   index.ts 100% 100% 100%
src/models/helpers
   domain.ts 100% 100% 100%
src/providers
   config.ts 66.67% 100% 98.11% 40, 49, 50
   index.ts 100% 100% 100%
   secrets.ts 45.45% 62.50% 78.57% 19, 23, 24, 25, 26, 27, 29, 30, 44, 50, 51, 52, 53, 56, 57, 58, 59, 62, 63, 64, 65
src/providers/config
   mainnet.ts 100% 100% 100%
   testnet.ts 100% 100% 100%
src/repositories
   contract-repository.ts 75.81% 100% 98.19% 103, 114, 123, 124, 126, 127, 129, 130, 132, 133, 142, 158, 180, 196, 34, 46, 47, 57, 63
   domain-repository.ts 60% 100% 100% 102, 113, 117, 118, 133, 142, 150, 176, 220, 225, 240, 256, 262, 269, 34, 53, 54, 96, 98, 99
   index.ts 100% 75% 100%
   record-repository.ts 71.43% 100% 100% 24, 34, 50, 60
src/repositories/contracts
   meta-names-contract-repository.ts 84.62% 88.89% 88.66% 58, 59, 60, 61, 62, 63, 64, 65, 77, 78, 79, 80, 90
src/repositories/helpers
   avl-client.ts 80% 77.78% 61.62% 16, 22, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 95
   client.ts 88.89% 100% 100% 31
   contract.ts 75% 100% 92.45% 41, 42, 43, 44, 45, 50
src/transactions
   helper.ts 76.92% 85.71% 93.18% 51, 52, 53, 54, 55, 56, 57, 79, 79
   index.ts 87.50% 25% 40.36% 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 13, 14, 147, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 93, 94, 95, 96, 97, 98, 99
   ledger.ts 100% 0% 43.62% 100, 101, 102, 103, 104, 105, 106, 11, 116, 117, 118, 119, 12, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 13, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 14, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 15, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 16, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 17, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 18, 180, 181, 182, 183, 184, 185, 186, 187, 188, 19, 20, 21, 26, 27, 28, 29, 30, 83, 84, 85, 86, 87, 88, 89, 97, 98, 99
src/validators
   base-validator.ts 100% 100% 100%
   domain-validator.ts 87.50% 100% 100% 29, 30, 31
   index.ts 100% 100% 100%
   record-validator.ts 100% 100% 100%
src/validators/records
   default-validator.ts 100% 100% 100%
   discord-validator.ts 100% 100% 100%
   email-validator.ts 100% 100% 100%
   main-validator.ts 100% 100% 100%
   price-validator.ts 100% 100% 100%
   regex-validator.ts 100% 100% 100%
   twitter-validator.ts 100% 100% 100%
   uri-validator.ts 100% 100% 100%
   wallet-validator.ts 100% 100% 100%
test
test/helpers
   config.ts 0% 100% 100% 7
   helper.ts 100% 60% 68% 25, 26, 27, 28, 29, 30, 31, 32, 35, 36, 37, 38, 39, 40, 41, 42
   index.ts 100% 100% 100%

@yeboster
yeboster merged commit 5be5e3f into main Aug 28, 2026
8 checks passed
@yeboster
yeboster deleted the chore/security-hardening branch August 28, 2026 18:51
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
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.

1 participant