fix(security): verify Node.js runtime SHASUMS OpenPGP signature#12295
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds detached OpenPGP signature verification of Node.js SHASUMS256.txt using pinned Node release-team keys, a key-sync CLI and release-time check, JS and Rust verified fetch APIs with tests, and resolver wiring that enables verification for official releases only. ChangesNode.js SHASUMS OpenPGP Verification
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Integrated-Benchmark Report (Linux)Each scenario reports direct installs and pnpr installs. Bencher consumes pacquet@HEAD and pnpr@HEAD. Scenario: Isolated linker: fresh restore, cold cache + cold store
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 10.43676948356,
"stddev": 0.5202391299244099,
"median": 10.324647991359999,
"user": 2.41320822,
"system": 2.5207580199999997,
"min": 10.04190892886,
"max": 11.859537288859999,
"times": [
10.34311575886,
11.859537288859999,
10.51761381986,
10.22204345086,
10.04190892886,
10.30618022386,
10.362417812859999,
10.096084506859999,
10.416974093859999,
10.20181895086
]
},
{
"command": "pacquet@main",
"mean": 10.113197012959999,
"stddev": 0.07158809538866599,
"median": 10.11973201136,
"user": 2.4190981199999997,
"system": 2.52458132,
"min": 10.021184761859999,
"max": 10.21733413386,
"times": [
10.21733413386,
10.059467870859999,
10.021340325859999,
10.12808050386,
10.158597452859999,
10.15278313686,
10.05670814586,
10.20509027886,
10.021184761859999,
10.111383518859999
]
},
{
"command": "pnpr@HEAD",
"mean": 5.97848290816,
"stddev": 0.5573971719797916,
"median": 5.66614571836,
"user": 2.0164709199999997,
"system": 2.2590464199999998,
"min": 5.57829235586,
"max": 7.00093413886,
"times": [
5.6122034568600005,
5.57931123886,
5.579665808860001,
5.57829235586,
5.69354520986,
5.63874622686,
6.85530244386,
7.00093413886,
6.3790296598600005,
5.86779854186
]
},
{
"command": "pnpr@main",
"mean": 5.73025449086,
"stddev": 0.2295467073721952,
"median": 5.61898430986,
"user": 1.9797764199999999,
"system": 2.25834952,
"min": 5.53643263686,
"max": 6.2223935388600005,
"times": [
6.2223935388600005,
5.59554164686,
5.57739890886,
5.56120961586,
5.543073558860001,
5.64242697286,
5.82561998086,
5.81141013686,
5.53643263686,
5.98703791186
]
}
]
}Scenario: Isolated linker: fresh restore, hot cache + hot store
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 0.7332562675600001,
"stddev": 0.07365974581955163,
"median": 0.73088119926,
"user": 0.30286878,
"system": 1.04679822,
"min": 0.65311287726,
"max": 0.8538321802600001,
"times": [
0.78658897526,
0.66279736926,
0.65767234826,
0.8538321802600001,
0.68898976926,
0.77277262926,
0.67234340926,
0.8012297312600001,
0.78322338626,
0.65311287726
]
},
{
"command": "pacquet@main",
"mean": 0.77280382826,
"stddev": 0.18520812768821024,
"median": 0.67410889976,
"user": 0.30664378000000003,
"system": 1.0407157200000001,
"min": 0.63787712626,
"max": 1.21744541226,
"times": [
0.77005863726,
0.90900158726,
1.21744541226,
0.8835987012600001,
0.67878568126,
0.6492691632600001,
0.63787712626,
0.65210233226,
0.66943211826,
0.66046752326
]
},
{
"command": "pnpr@HEAD",
"mean": 0.8988642635599999,
"stddev": 0.07338307079547653,
"median": 0.8834099312600001,
"user": 0.32450838000000004,
"system": 1.0409758199999999,
"min": 0.7827774362600001,
"max": 1.05910579426,
"times": [
0.9739014582600001,
0.87520332326,
0.8590526942600001,
0.86723794026,
0.90447652326,
1.05910579426,
0.7827774362600001,
0.90006760326,
0.8870121362600001,
0.87980772626
]
},
{
"command": "pnpr@main",
"mean": 0.83689378926,
"stddev": 0.09553138802750825,
"median": 0.81428109026,
"user": 0.31204498,
"system": 1.0480999199999999,
"min": 0.73627383826,
"max": 0.95753570026,
"times": [
0.76411558026,
0.73627383826,
0.95753570026,
0.94694687526,
0.76225410726,
0.94939847726,
0.74055488226,
0.89931163926,
0.86444660026,
0.74810019226
]
}
]
}Scenario: Isolated linker: fresh install, cold cache + cold store
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 9.555473642239999,
"stddev": 0.3238170416602305,
"median": 9.427804818039998,
"user": 2.8526164199999995,
"system": 2.57155534,
"min": 9.27974340154,
"max": 10.264767064539999,
"times": [
9.41498570154,
9.46113716254,
9.27974340154,
9.31129472254,
9.53416141354,
9.40719416054,
9.42299701054,
9.43261262554,
10.025843159539999,
10.264767064539999
]
},
{
"command": "pacquet@main",
"mean": 9.34205022224,
"stddev": 0.07557131025355318,
"median": 9.31935796454,
"user": 2.85465472,
"system": 2.57618204,
"min": 9.26754756854,
"max": 9.48377470454,
"times": [
9.375277969539999,
9.34494167254,
9.26754756854,
9.48377470454,
9.315262537539999,
9.32345339154,
9.27291886254,
9.307148180539999,
9.455995292539999,
9.27418204254
]
},
{
"command": "pnpr@HEAD",
"mean": 5.431035551640001,
"stddev": 0.16069814796728785,
"median": 5.36104464454,
"user": 1.8582565199999999,
"system": 2.1970913399999996,
"min": 5.28237750454,
"max": 5.75264986054,
"times": [
5.311874025540001,
5.34882606854,
5.28237750454,
5.47913052254,
5.66446007354,
5.316086798540001,
5.459182756540001,
5.32250468554,
5.75264986054,
5.37326322054
]
},
{
"command": "pnpr@main",
"mean": 5.4644236699399995,
"stddev": 0.15946235304412754,
"median": 5.417995874040001,
"user": 1.8621950199999997,
"system": 2.19074094,
"min": 5.30558970454,
"max": 5.758867271540001,
"times": [
5.32089376454,
5.457936359540001,
5.30558970454,
5.32453622354,
5.35121689354,
5.627292096540001,
5.483519971540001,
5.63632902554,
5.758867271540001,
5.37805538854
]
}
]
}Scenario: Isolated linker: fresh install, hot cache + hot store
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 1.4612504321200002,
"stddev": 0.3067133042021927,
"median": 1.32465142312,
"user": 1.23569606,
"system": 1.4514640599999997,
"min": 1.29597593162,
"max": 2.2567697216200004,
"times": [
1.7128889496200002,
1.35808529862,
1.30374868762,
2.2567697216200004,
1.31789400362,
1.29654664962,
1.29597593162,
1.31311434262,
1.42607189362,
1.33140884262
]
},
{
"command": "pacquet@main",
"mean": 1.3917729469200002,
"stddev": 0.15104652396537913,
"median": 1.30043985912,
"user": 1.2587557600000001,
"system": 1.4184824599999997,
"min": 1.26967017162,
"max": 1.62709946362,
"times": [
1.36966458562,
1.26967017162,
1.62709946362,
1.5958643856199999,
1.59736432862,
1.30704227862,
1.29383743962,
1.29210627062,
1.27661351862,
1.28846702662
]
},
{
"command": "pnpr@HEAD",
"mean": 0.70729447032,
"stddev": 0.09225228821408565,
"median": 0.67949588762,
"user": 0.26980735999999994,
"system": 1.0048969599999997,
"min": 0.66778266962,
"max": 0.9693882086200001,
"times": [
0.9693882086200001,
0.68867512162,
0.67781064962,
0.67951945962,
0.67947231562,
0.68034325962,
0.67555849262,
0.66778266962,
0.67311504962,
0.68127947662
]
},
{
"command": "pnpr@main",
"mean": 0.7074020961199999,
"stddev": 0.09856031558983139,
"median": 0.67318472512,
"user": 0.26955535999999997,
"system": 1.0027182599999998,
"min": 0.6606533946200001,
"max": 0.98495697762,
"times": [
0.66891284362,
0.67445907862,
0.66957648162,
0.67026575862,
0.6606533946200001,
0.67191037162,
0.98495697762,
0.71327862762,
0.68406452962,
0.67594289762
]
}
]
}Scenario: Isolated linker: fresh install, cold cache + hot store
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 4.9198433403,
"stddev": 0.0988643991716069,
"median": 4.9004285382,
"user": 1.3849202600000001,
"system": 1.54288412,
"min": 4.8368935587,
"max": 5.1668876857,
"times": [
4.8368935587,
4.8981761267,
4.8389979687,
4.8633124757,
5.1668876857,
4.9899978507,
4.9286494537,
4.9026809497,
4.9205258567,
4.8523114767
]
},
{
"command": "pacquet@main",
"mean": 4.927556420899999,
"stddev": 0.053885751866278545,
"median": 4.9279674772,
"user": 1.39122616,
"system": 1.5397104199999998,
"min": 4.8451862247,
"max": 5.0291285767,
"times": [
4.9761666627,
4.8550545567,
4.9450131487,
4.9283010777000005,
4.9276338767,
5.0291285767,
4.9268040287,
4.9437608577,
4.8451862247,
4.8985151987
]
},
{
"command": "pnpr@HEAD",
"mean": 0.6732972241,
"stddev": 0.010750405051886227,
"median": 0.6720544977,
"user": 0.26290956000000004,
"system": 1.01383192,
"min": 0.6595788457,
"max": 0.6974073577000001,
"times": [
0.6709648307000001,
0.6623596807000001,
0.6740547877,
0.6731441647,
0.6974073577000001,
0.6790019037000001,
0.6702493757000001,
0.6802846107000001,
0.6659266837000001,
0.6595788457
]
},
{
"command": "pnpr@main",
"mean": 0.7244257625,
"stddev": 0.10030965678369738,
"median": 0.6811041922000001,
"user": 0.26755586,
"system": 1.01959122,
"min": 0.6673374847000001,
"max": 0.9877694127000001,
"times": [
0.7126153867000001,
0.6673374847000001,
0.6827851457,
0.6783511467000001,
0.6794232387000001,
0.9877694127000001,
0.7981824147000001,
0.6949498567000001,
0.6712590697,
0.6715844687000001
]
}
]
} |
|
| Branch | pr/12295 |
| Testbed | pacquet |
🚨 1 Alert
| Benchmark | Measure Units | View | Benchmark Result (Result Δ%) | Upper Boundary (Limit %) |
|---|---|---|---|---|
| isolated-linker.fresh-install.cold-cache.cold-store | Latency seconds (s) | 📈 plot 🚷 threshold 🚨 alert (🔔) | 9.56 s(+33.47%)Baseline: 7.16 s | 8.59 s (111.22%) |
Click to view all benchmark results
| Benchmark | Latency | Benchmark Result milliseconds (ms) (Result Δ%) | Upper Boundary milliseconds (ms) (Limit %) |
|---|---|---|---|
| isolated-linker.fresh-install.cold-cache.cold-store | 📈 view plot 🚷 view threshold 🚨 view alert (🔔) | 9,555.47 ms(+33.47%)Baseline: 7,159.34 ms | 8,591.21 ms (111.22%) |
| isolated-linker.fresh-install.cold-cache.hot-store | 📈 view plot 🚷 view threshold | 4,919.84 ms(-1.96%)Baseline: 5,018.19 ms | 6,021.83 ms (81.70%) |
| isolated-linker.fresh-install.hot-cache.hot-store | 📈 view plot 🚷 view threshold | 1,461.25 ms(+3.66%)Baseline: 1,409.71 ms | 1,691.65 ms (86.38%) |
| isolated-linker.fresh-restore.cold-cache.cold-store | 📈 view plot 🚷 view threshold | 10,436.77 ms(+14.12%)Baseline: 9,145.30 ms | 10,974.36 ms (95.10%) |
| isolated-linker.fresh-restore.hot-cache.hot-store | 📈 view plot 🚷 view threshold | 733.26 ms(+12.63%)Baseline: 651.03 ms | 781.23 ms (93.86%) |
Code Review by Qodo
1. Unused KeyDetails import
|
PR Summary by QodoVerify Node.js runtime SHASUMS via OpenPGP signature (trusted release keys) WalkthroughsDescription• Verify downloaded Node.js runtime SHASUMS256.txt via detached OpenPGP signature. • Embed/pin Node release public keys and gate releases on keylist freshness. • Verify only release channel mirrors; keep hardcoded musl mirror trusted over TLS. Diagramgraph TD
A["Node runtime resolver"] --> B["SHASUMS fetch"] --> C{Signature verify?}
C -->|"release channel"| D["OpenPGP verify"] --> E["Embedded Node keys"]
C -->|"non-release / musl"| F["TLS trust only"]
D --> G["Node mirror (configurable)"]
F --> H["Unofficial musl mirror"]
I["Release workflow"] --> J["Keys sync check"] --> E
subgraph Legend
direction LR
_comp["Component"] ~~~ _dec{"Decision"} ~~~ _ext[["External source"]]
end
High-Level AssessmentThe following are alternative approaches to this PR: 1. Always fetch SHASUMS(+sig) from nodejs.org, regardless of mirror
2. Pin trusted key fingerprints only (download keys on demand)
3. Use a modern transparency/supply-chain scheme (Sigstore/TUF) for runtime artifacts
Recommendation: The PR’s approach (verify SHASUMS256.txt.sig against pinned Node release keys) is the best pragmatic fix for the mirror-configurable integrity loop. It preserves mirror compatibility for faithful mirrors, blocks malicious mirrors without requiring new upstream infrastructure, and adds a release-time guard to keep embedded trust roots current. The main alternative worth considering is fetching SHASUMS(+sig) from nodejs.org regardless of mirror; however, that would be a behavioral change that undermines mirror-only environments. File ChangesEnhancement (1)
Bug fix (2)
Tests (1)
Documentation (1)
Other (8)
|
|
Code review by qodo was updated up to the latest commit 7a27f09 |
|
Thanks @qodo — good catch on the revoked-key gap. Fixed in 7a27f09: the keys freshness check ( The other items in the summary (alternative approaches, walkthrough) were informational; the chosen pinned-keys + packet-level verification approach is intentional. Written by an agent (Claude Code, claude-opus-4-8). |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
engine/runtime/node-resolver/src/index.ts (1)
99-99: ⚡ Quick winConsider refactoring to use an options object.
As per coding guidelines, functions should be limited to two or three arguments, with more parameters grouped into a single options object.
readNodeAssetsnow has four parameters.♻️ Proposed refactor
-async function readNodeAssets (fetch: FetchFromRegistry, nodeMirrorBaseUrl: string, version: string, releaseChannel: string): Promise<PlatformAssetResolution[]> { +async function readNodeAssets ( + fetch: FetchFromRegistry, + opts: { nodeMirrorBaseUrl: string, version: string, releaseChannel: string } +): Promise<PlatformAssetResolution[]> { + const { nodeMirrorBaseUrl, version, releaseChannel } = optsUpdate the call site at line 66:
- const variants = await readNodeAssets(ctx.fetchFromRegistry, nodeMirrorBaseUrl, version, releaseChannel) + const variants = await readNodeAssets(ctx.fetchFromRegistry, { nodeMirrorBaseUrl, version, releaseChannel })And the musl-variant call at line 113:
- const muslAssets = await readNodeAssetsFromMirror(fetch, { nodeMirrorBaseUrl: UNOFFICIAL_NODE_MIRROR_BASE_URL, version, muslOnly: true, verifySignature: false }) + const muslAssets = await readNodeAssetsFromMirror(fetch, { nodeMirrorBaseUrl: UNOFFICIAL_NODE_MIRROR_BASE_URL, version, releaseChannel, muslOnly: true, verifySignature: false })Based on coding guidelines: "limit functions to no more than two or three arguments, and use a single options object for functions needing more parameters"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@engine/runtime/node-resolver/src/index.ts` at line 99, Refactor readNodeAssets to accept a single options object instead of four positional parameters: replace the signature async function readNodeAssets(fetch: FetchFromRegistry, nodeMirrorBaseUrl: string, version: string, releaseChannel: string) with async function readNodeAssets(opts: { fetch: FetchFromRegistry; nodeMirrorBaseUrl: string; version: string; releaseChannel: string }): Promise<PlatformAssetResolution[]> and update the implementation to use opts.fetch, opts.nodeMirrorBaseUrl, opts.version and opts.releaseChannel; then update every call site of readNodeAssets (including the standard and musl-variant calls) to pass an object with those named properties rather than four separate arguments, and adjust any type imports or usage accordingly.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@engine/runtime/node-resolver/src/index.ts`:
- Line 99: Refactor readNodeAssets to accept a single options object instead of
four positional parameters: replace the signature async function
readNodeAssets(fetch: FetchFromRegistry, nodeMirrorBaseUrl: string, version:
string, releaseChannel: string) with async function readNodeAssets(opts: {
fetch: FetchFromRegistry; nodeMirrorBaseUrl: string; version: string;
releaseChannel: string }): Promise<PlatformAssetResolution[]> and update the
implementation to use opts.fetch, opts.nodeMirrorBaseUrl, opts.version and
opts.releaseChannel; then update every call site of readNodeAssets (including
the standard and musl-variant calls) to pass an object with those named
properties rather than four separate arguments, and adjust any type imports or
usage accordingly.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 5b59cb51-b1af-4313-9f4a-590bce2dc28a
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (12)
.changeset/verify-node-runtime-shasums.md.github/workflows/create-release-pr.ymlcrypto/shasums-file/package.jsoncrypto/shasums-file/scripts/update-node-release-keys.mjscrypto/shasums-file/src/index.tscrypto/shasums-file/src/nodeReleaseKeys.tscrypto/shasums-file/src/verifyNodeShasums.tscrypto/shasums-file/test/verifyNodeShasums.tscspell.jsonengine/runtime/node-resolver/src/index.tspackage.jsonpnpm-workspace.yaml
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
- GitHub Check: Analyze (javascript)
- GitHub Check: Run benchmark on ubuntu-latest
- GitHub Check: Compile & Lint
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (AGENTS.md)
**/*.{ts,tsx,js,jsx}: Use Standard Style with trailing commas, prefer functions over classes, declare functions after they are used (relying on hoisting), limit functions to no more than two or three arguments, and use a single options object for functions needing more parameters
Follow import order: standard libraries first, then external dependencies (sorted alphabetically), then relative imports
Do not write comments that restate what the code already says; rename variables, split helpers, or move checks to more obvious places instead
Do not repeat documentation at call sites that already lives on the callee; update the JSDoc once and let every call site benefit
Use JSDoc for the function's contract (preconditions, postconditions, edge cases, why the function exists), not for re-narrating the function body
Do not record past implementation shape, refactor history, or removed code in comments; use git log and git blame for that information instead
Write comments only when the reason for code is non-obvious, a hidden invariant exists, a workaround for a known bug is needed, or an exception to surrounding pattern is deliberate
Files:
crypto/shasums-file/test/verifyNodeShasums.tscrypto/shasums-file/src/index.tscrypto/shasums-file/src/verifyNodeShasums.tsengine/runtime/node-resolver/src/index.ts
🧠 Learnings (5)
📚 Learning: 2026-05-05T23:03:04.286Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11479
File: __utils__/scripts/package.json:6-9
Timestamp: 2026-05-05T23:03:04.286Z
Learning: The pattern cross-env NODE_OPTIONS="$NODE_OPTIONS ..." in package.json scripts is an established convention in the pnpm/pnpm repository and is used across many packages (e.g., fs/hard-link-dir, worker, __utils__/scripts). Do not flag this as a cross-platform issue in individual files; if a change is needed, apply it as a repo-wide change in a separate PR. Scope this guidance to all package.json files in the repo; use the minimatch pattern '**/package.json' to identify relevant files and review changes at the repository level rather than per-file.
Applied to files:
package.jsoncrypto/shasums-file/package.json
📚 Learning: 2026-05-14T09:04:00.133Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11622
File: resolving/npm-resolver/test/publishedBy.test.ts:350-354
Timestamp: 2026-05-14T09:04:00.133Z
Learning: In the pnpm/pnpm repository, ESLint is the authoritative style linter. Do not raise review findings for missing trailing commas in multiline function calls (e.g., `fs.writeFileSync(...)`) when this repo’s ESLint configuration does not report them and lint passes. Prefer deferring to the ESLint results for this specific trailing-comma rule rather than enforcing it manually in code review.
Applied to files:
crypto/shasums-file/test/verifyNodeShasums.tscrypto/shasums-file/src/index.tscrypto/shasums-file/src/verifyNodeShasums.tsengine/runtime/node-resolver/src/index.ts
📚 Learning: 2026-06-05T13:47:05.929Z
Learnt from: vsumner
Repo: pnpm/pnpm PR: 12190
File: installing/deps-installer/test/install/frozenStore.ts:2-17
Timestamp: 2026-06-05T13:47:05.929Z
Learning: In the pnpm/pnpm repository, the shared Jest preset keeps `injectGlobals` at its default (`true`), so `test` and `expect` are available as Jest globals. Therefore, reviewers should not flag (or treat as TypeScript/compilation errors) missing `import { test, expect } from 'jest/globals'` when a test file uses `test`/`expect` without importing them. Importing from `jest/globals` may still be used for consistency with sibling files, but it is not required for execution in this repo unless a Jest preset is explicitly configured with `injectGlobals: false`.
Applied to files:
crypto/shasums-file/test/verifyNodeShasums.ts
📚 Learning: 2026-06-05T13:47:26.046Z
Learnt from: vsumner
Repo: pnpm/pnpm PR: 12190
File: installing/deps-installer/src/install/index.ts:2337-2343
Timestamp: 2026-06-05T13:47:26.046Z
Learning: In the pnpm/pnpm codebase, `PnpmError` automatically prefixes `err.code` with `ERR_PNPM_` when you pass a code that does not already start with `ERR_PNPM_` (it normalizes `this.code` via `code.startsWith('ERR_PNPM_') ? code : `ERR_PNPM_${code}``). Therefore, during code review you should NOT flag `new PnpmError(...)` call sites for passing a bare error code (e.g., `new PnpmError('FROZEN_STORE_INCOMPATIBLE_WITH_PNPR', ...)`); the resulting `err.code` will still be `ERR_PNPM_FROZEN_STORE_INCOMPATIBLE_WITH_PNPR`.
Applied to files:
crypto/shasums-file/test/verifyNodeShasums.tscrypto/shasums-file/src/index.tscrypto/shasums-file/src/verifyNodeShasums.tsengine/runtime/node-resolver/src/index.ts
📚 Learning: 2026-05-26T21:01:06.666Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11966
File: .changeset/require-tarball-integrity.md:6-6
Timestamp: 2026-05-26T21:01:06.666Z
Learning: In pnpm lockfile-related release notes/docs (especially changeset markdown), preserve URL hostnames exactly as they appear in pnpm-lock.yaml tarball resolution entries—keep hosts like `codeload.github.com`, `bitbucket.org`, and `gitlab.com` in lowercase. Do not “correct” them to title-case/preserve brand capitalization (e.g., LanguageTool rules like `GITHUB` capitalization) because these are literal URL fragments, not platform brand names.
Applied to files:
.changeset/verify-node-runtime-shasums.md
🔇 Additional comments (23)
pnpm-workspace.yaml (1)
244-245: LGTM!crypto/shasums-file/package.json (1)
38-39: LGTM!Also applies to: 43-43
package.json (1)
29-30: LGTM!cspell.json (1)
2-5: LGTM!Also applies to: 224-224, 353-354
crypto/shasums-file/scripts/update-node-release-keys.mjs (4)
1-18: LGTM!
19-44: LGTM!
46-55: LGTM!
57-75: LGTM!crypto/shasums-file/src/verifyNodeShasums.ts (5)
1-23: LGTM!
25-63: LGTM!
65-88: LGTM!
90-109: LGTM!
111-120: LGTM!crypto/shasums-file/src/index.ts (3)
6-8: LGTM!
15-20: LGTM!
22-34: LGTM!crypto/shasums-file/test/verifyNodeShasums.ts (3)
1-27: LGTM!
29-48: LGTM!
50-67: LGTM!engine/runtime/node-resolver/src/index.ts (2)
1-1: LGTM!
99-121: LGTM!Also applies to: 129-136
.changeset/verify-node-runtime-shasums.md (1)
1-14: LGTM!.github/workflows/create-release-pr.yml (1)
53-58: LGTM!
|
Code review by qodo was updated up to the latest commit e371499 |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
pacquet/crates/crypto-shasums-file/src/lib.rs (1)
171-171: ⚡ Quick winConsider using
from_utf8with explicit error handling instead offrom_utf8_lossy.
from_utf8_lossysilently replaces invalid UTF-8 bytes with U+FFFD replacement characters. If the verified SHASUMS file somehow contains non-UTF-8 bytes (e.g., due to a compromised mirror injecting binary data that still passes signature verification via a different attack vector), the replacement would corrupt hash values without raising an error. UsingString::from_utf8(...).map_err(...)would surface such anomalies as explicit failures.Suggested change
- Ok(String::from_utf8_lossy(&shasums_bytes).into_owned()) + String::from_utf8(shasums_bytes).map_err(|_| FetchVerifiedNodeShasumsError::SignatureInvalid { + url: shasums_url.to_string(), + })Alternatively, add a dedicated error variant for invalid UTF-8 if you want a distinct diagnostic code.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@pacquet/crates/crypto-shasums-file/src/lib.rs` at line 171, Replace the silent UTF-8 lossiness at the end of the function that currently uses String::from_utf8_lossy(&shasums_bytes).into_owned() with explicit UTF-8 validation: call String::from_utf8(shasums_bytes) and map any error into the crate's error type (or add a new InvalidUtf8 error variant) so the function (the code producing the Ok(...) in lib.rs) returns an explicit error on invalid UTF-8 instead of producing replacement characters.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@pacquet/crates/crypto-shasums-file/src/lib.rs`:
- Around line 270-273: The debug-only assertion on the key fingerprint
(fingerprint_matches computed from key.fingerprint().to_string() and
trusted_key.fingerprint) must become a runtime check: replace
debug_assert!(fingerprint_matches) with an explicit conditional that returns an
error (or otherwise fails the function) when fingerprint_matches is false so the
function does not return Ok(key) for a mismatched fingerprint; keep the same
symbols (key, trusted_key, fingerprint_matches) and propagate an appropriate Err
from the enclosing function instead of allowing the key to be used.
---
Nitpick comments:
In `@pacquet/crates/crypto-shasums-file/src/lib.rs`:
- Line 171: Replace the silent UTF-8 lossiness at the end of the function that
currently uses String::from_utf8_lossy(&shasums_bytes).into_owned() with
explicit UTF-8 validation: call String::from_utf8(shasums_bytes) and map any
error into the crate's error type (or add a new InvalidUtf8 error variant) so
the function (the code producing the Ok(...) in lib.rs) returns an explicit
error on invalid UTF-8 instead of producing replacement characters.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 823375b9-c651-45c5-9722-be9aad3a9631
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (11)
Cargo.tomlcrypto/shasums-file/scripts/update-node-release-keys.mjscrypto/shasums-file/src/nodeReleaseKeys.tscspell.jsonpacquet/crates/crypto-shasums-file/Cargo.tomlpacquet/crates/crypto-shasums-file/src/lib.rspacquet/crates/crypto-shasums-file/src/node_release_keys.rspacquet/crates/crypto-shasums-file/src/tests.rspacquet/crates/engine-runtime-node-resolver/Cargo.tomlpacquet/crates/engine-runtime-node-resolver/src/node_resolver.rspacquet/crates/engine-runtime-node-resolver/src/node_resolver/tests.rs
✅ Files skipped from review due to trivial changes (1)
- cspell.json
🚧 Files skipped from review as they are similar to previous changes (1)
- crypto/shasums-file/scripts/update-node-release-keys.mjs
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (10)
- GitHub Check: Run benchmark on ubuntu-latest
- GitHub Check: Lint and Test (windows-latest)
- GitHub Check: Doc
- GitHub Check: Format
- GitHub Check: Lint and Test (ubuntu-latest)
- GitHub Check: Lint and Test (macos-latest)
- GitHub Check: Dylint
- GitHub Check: Code Coverage
- GitHub Check: Run benchmark on ubuntu-latest
- GitHub Check: Compile & Lint
🧰 Additional context used
📓 Path-based instructions (2)
pacquet/**/Cargo.toml
📄 CodeRabbit inference engine (pacquet/AGENTS.md)
pacquet/**/Cargo.toml: Check whether the workspace already depends on something suitable in[workspace.dependencies]in the rootCargo.tomlbefore adding a new dependency
Keep dependencies at the right level — add a new dependency to the specific crate that needs it, not to the workspace root or shared crate unless multiple crates depend on it
Files:
pacquet/crates/engine-runtime-node-resolver/Cargo.tomlpacquet/crates/crypto-shasums-file/Cargo.toml
pacquet/**/*.rs
📄 CodeRabbit inference engine (pacquet/AGENTS.md)
pacquet/**/*.rs: Log emissions are part of matching pnpm — when porting a function that firespnpm:<channel>events throughglobalLogger,logger.debug(...), orstreamParser.write(...), mirror the call site, payload, and ordering so@pnpm/cli.default-reporterparses pacquet's NDJSON the same way
Declare a newtype wrapper for branded string types instead of collapsing the brand into a plainStringor&strin Rust
If upstream TypeScript always validates before construction of a branded string, validate in the Rust wrapper too viaTryFrom<String>and/orFromStrand do not provide an infallible public constructor
If upstream TypeScript never validates a branded string, just brand for type-safety in Rust by exposing an infallibleFrom<String>constructor
If upstream TypeScript occasionally constructs a branded string without validation, exposefrom_str_uncheckedin Rust as an escape hatch alongside the validating constructor
Match upstream serde behavior for branded strings crossing JSON, YAML, or INI boundaries by using#[serde(try_from = "String")]for deserialization and#[serde(into = "String")]for serialization
Derive simple conversions for branded strings using#[derive(derive_more::From)]and#[derive(derive_more::Into)]instead of handwritingimplblocks; use manualimplonly when conversion needs custom logic
Model TypeScript string literal unions (like'auto' | 'always' | 'never') as Rustenums instead of newtype wrappers, since the set of valid values is closed
Treat TypeScript string template literal types (like`${string}@${string}`) the same as branded string types in Rust, using a newtype wrapper with validation
Follow the code style guide inCODE_STYLE_GUIDE.md— imports, modules, naming, ownership and borrowing, parameter type selection, trait bounds, pattern matching,pipe-trait, error handling, test layout, and cloning ofArcandRc
Choose owned vs. borrowed parameters to minimize copies; widen to t...
Files:
pacquet/crates/crypto-shasums-file/src/tests.rspacquet/crates/engine-runtime-node-resolver/src/node_resolver.rspacquet/crates/engine-runtime-node-resolver/src/node_resolver/tests.rspacquet/crates/crypto-shasums-file/src/lib.rs
🧠 Learnings (4)
📚 Learning: 2026-05-20T19:40:55.051Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11774
File: pacquet/crates/resolving-deps-resolver/src/resolve_peers.rs:0-0
Timestamp: 2026-05-20T19:40:55.051Z
Learning: In the pacquet Rust code, ensure the semver implementation uses the `node-semver` crate (not `nodejs-semver`). `node-semver`’s public API does not include a `satisfies_with_prerelease`-style method; prerelease-tolerant matching should be implemented inline by first calling `Range::satisfies`, and when it rejects a prerelease version, retry matching against a stripped `MAJOR.MINOR.PATCH` base of the prerelease version.
Applied to files:
pacquet/crates/crypto-shasums-file/src/tests.rspacquet/crates/engine-runtime-node-resolver/src/node_resolver.rspacquet/crates/engine-runtime-node-resolver/src/node_resolver/tests.rspacquet/crates/crypto-shasums-file/src/lib.rs
📚 Learning: 2026-05-22T00:08:44.646Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11837
File: pacquet/crates/resolving-npm-resolver/src/pick_package.rs:33-51
Timestamp: 2026-05-22T00:08:44.646Z
Learning: In the pnpm/pnpm repo’s pacquet Rust crates, do not flag Unicode ellipsis characters (U+2026, `…`) in Rust doc comments (`///` / `/** */`) as a lint violation. The pacquet crate’s `dylint.toml` only enables `perfectionist::derive_ordering`, and the Dylint `unicode-ellipsis` rule is not enabled for this project—so `…` in doc comments is an intentional, repo-consistent style.
Applied to files:
pacquet/crates/crypto-shasums-file/src/tests.rspacquet/crates/engine-runtime-node-resolver/src/node_resolver.rspacquet/crates/engine-runtime-node-resolver/src/node_resolver/tests.rspacquet/crates/crypto-shasums-file/src/lib.rs
📚 Learning: 2026-05-20T23:07:58.444Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11784
File: pacquet/crates/resolving-deps-resolver/src/hoist_peers.rs:120-133
Timestamp: 2026-05-20T23:07:58.444Z
Learning: When reviewing code in this pacquet Rust port, follow the upstream pnpm compatibility rule: only match pnpm’s behavior exactly. Do not propose review changes that intentionally deviate from pnpm’s documented/observed behavior, even if pnpm appears buggy. If you identify a real bug in pnpm behavior, the review should prioritize fixing it upstream in pnpm first, and avoid implementing a pnpm-behavior workaround here unless the same fix has already landed upstream.
Applied to files:
pacquet/crates/crypto-shasums-file/src/tests.rspacquet/crates/engine-runtime-node-resolver/src/node_resolver.rspacquet/crates/engine-runtime-node-resolver/src/node_resolver/tests.rspacquet/crates/crypto-shasums-file/src/lib.rs
📚 Learning: 2026-06-06T18:58:37.156Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 12243
File: pacquet/crates/package-manager/src/install_package_by_snapshot.rs:319-322
Timestamp: 2026-06-06T18:58:37.156Z
Learning: When reviewing Rust code, do not assume `matches!(expr, Pattern(_))` will move out of `expr` if `Pattern(_)` contains no by-value bindings. `matches!` desugars to a `match` that auto-borrows the scrutinee for discrimination, so even if `expr` is a non-`Copy` value behind a shared reference (e.g., `&T`), the macro should not move-out of the borrowed data purely due to `matches!`. Treat `matches!(&expr, Pattern(_))` as a readability/clarity improvement, not a correctness requirement. Only flag potential move-out-of-borrow risks when the pattern includes by-value bindings that would require moving the matched value.
Applied to files:
pacquet/crates/crypto-shasums-file/src/tests.rspacquet/crates/engine-runtime-node-resolver/src/node_resolver.rspacquet/crates/engine-runtime-node-resolver/src/node_resolver/tests.rspacquet/crates/crypto-shasums-file/src/lib.rs
🔇 Additional comments (14)
Cargo.toml (1)
133-133: LGTM!pacquet/crates/crypto-shasums-file/Cargo.toml (1)
19-19: LGTM!Also applies to: 23-23
pacquet/crates/crypto-shasums-file/src/lib.rs (4)
152-182: LGTM!
213-240: LGTM!
242-259: LGTM!
69-115: LGTM!pacquet/crates/crypto-shasums-file/src/tests.rs (2)
101-165: LGTM!
167-231: LGTM!pacquet/crates/engine-runtime-node-resolver/Cargo.toml (1)
29-29: LGTM!pacquet/crates/engine-runtime-node-resolver/src/node_resolver.rs (3)
71-72: LGTM!
222-249: LGTM!
275-286: LGTM!pacquet/crates/engine-runtime-node-resolver/src/node_resolver/tests.rs (2)
108-156: LGTM!
158-160: LGTM!
Micro-Benchmark ResultsLinux |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #12295 +/- ##
==========================================
+ Coverage 87.54% 87.68% +0.13%
==========================================
Files 288 288
Lines 34952 35050 +98
==========================================
+ Hits 30599 30733 +134
+ Misses 4353 4317 -36 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
Code review by qodo was updated up to the latest commit 5425770 |
|
Code review by qodo was updated up to the latest commit 2959415 |
When a repository requests a Node.js runtime (devEngines.runtime / useNodeVersion), the download mirror is repository-configurable via `node-mirror:<channel>`. The downloaded binary was only checked against `SHASUMS256.txt` fetched from that same mirror — a circular check a malicious mirror satisfies by serving a tampered binary plus a matching SHASUMS. pnpm then executes the binary (e.g. for lifecycle scripts). pnpm now fetches `SHASUMS256.txt.sig` and verifies its detached OpenPGP signature against the Node.js release team's public keys, embedded in the CLI. A mirror that serves a tampered binary cannot also produce a valid signature. - `@pnpm/crypto.shasums-file`: `fetchVerifiedNodeShasums` / `fetchVerifiedNodeShasumsFile` verify the signature (via openpgp) against the embedded keys. - The keys live in a generated file kept in sync with the canonical nodejs/release-keys list; the create-release-pr workflow runs the check as a gate. - The node-resolver verifies the configurable-mirror SHASUMS. The hardcoded unofficial-builds.nodejs.org musl mirror (not repo-configurable, signed by a different key) stays trusted over TLS. Bun and Deno are unaffected: their download URLs are hardcoded to canonical GitHub. Pacquet parity (engine-runtime-node-resolver has the same mirror logic) still needs the equivalent Rust port; tracked as a follow-up.
…gned channels Two CI failures in the Node.js runtime integration tests: - Older releases (e.g. v22.0.0) failed to verify because `nodejs/release-keys` ships the *re-certified* key, whose self-signature postdates the release, so `openpgp.verify` (which validates the key as of the signature's creation time) rejects it. Since the trusted keys are pinned, verify the detached signature at the packet level against the matching key packet — the raw cryptographic check — bypassing OpenPGP's key-validity-window evaluation. Still rejects tampered content and signatures from untrusted keys. - Pre-release channels (rc, nightly, …) have no SHASUMS256.txt.sig at all — Node only signs the `release` channel. Gate signature verification on the channel so those installs are not forced to fail. (They remain unverifiable, which is inherent: Node does not sign them.) Verified against live nodejs.org across v18/v20/v22 release lines.
… canonical The freshness check only failed when a canonical fingerprint was missing from the embedded list. It now also fails when an embedded key is no longer in nodejs/release-keys (a removed/revoked key), so a revoked Node release key cannot stay in pnpm's trust set. --update already rewrites to exactly the canonical set.
2959415 to
fbeb182
Compare
|
Code review by qodo was updated up to the latest commit fbeb182 |
|
Code review by qodo was updated up to the latest commit 0dd0db4 |
|
Code review by qodo was updated up to the latest commit db86ff4 |
When a repository requests a Node.js runtime (useNodeVersion or an execution env), pnpm downloads and then executes a Node binary. The download mirror is repository-configurable via node-mirror:<channel> in project .npmrc, and the integrity came from SHASUMS256.txt fetched from that same mirror — a circular check a malicious mirror can satisfy with a tampered binary and matching hashes. pnpm now fetches SHASUMS256.txt.sig and verifies its detached OpenPGP signature against the Node.js release team's public keys, embedded in the pnpm CLI, before trusting the hashes: - @pnpm/crypto.shasums-file: new fetchVerifiedNodeShasums / fetchVerifiedNodeShasumsFile verify the signature via openpgp against the embedded keys (generated src/nodeReleaseKeys.ts, mirrored from the canonical nodejs/release-keys list). - @pnpm/node.fetcher verifies the configurable-mirror SHASUMS for the release channel; pre-release channels (rc, nightly, ...) are unsigned by Node and remain unverified. - scripts/update-node-release-keys.mjs keeps the keys current (pnpm run check:node-release-keys / update:node-release-keys), and the release workflow runs the check as a gate. Backport of #12295 to v10 (without the pacquet Rust port, which does not exist on this branch).
* fix(package-bins): reject reserved manifest bin names Manifest bin keys "", ".", "..", and scoped forms such as "@scope/.." passed the bin-name guard because encodeURIComponent leaves them unchanged. When joined to the global bin directory during global remove/update/add operations, "." resolves to the bin directory itself and ".." to its parent, which removeBin then recursively deletes. Reject empty, ".", and ".." bin names after scope stripping. Backport of #12289 to v10. * fix: block untrusted request destination env expansion Makes environment expansion trust-aware for registry/auth config and request destinations: - Stops project and workspace .npmrc files from expanding ${...} placeholders in registry/proxy request destinations, URL-scoped keys, and registry credential values. - Stops repository-controlled pnpm-workspace.yaml from expanding ${...} placeholders in the registry setting. - Preserves env expansion for trusted user/global/CLI/env config so existing token and registry setup flows continue to work. Backport of #12291 (CAND-PNPM-122 / GHSA-3qhv-2rgh-x77r) to v10. * fix(security): verify npm registry signature before spawning a package-manager binary The packageManager field (and pnpm self-update) makes pnpm download and run a specific pnpm version. The staged install's bytes were trusted based on lockfile integrity alone, which proves nothing when the inputs are repository-controlled. pnpm now verifies the npm registry signature of the engine it is about to spawn, over the installed integrity, against npm's public signing keys embedded in the pnpm CLI (exactly as corepack does): - verifyPnpmEngineIdentity() checks pnpm/@pnpm/exe and the materialized platform binaries of the staged install before it is linked into the tools directory. - Fails closed: any verification failure, including an unreachable registry, refuses the version switch rather than running an unverified binary. Runs only on a tools-directory cache miss (an actual download). - The embedded keys live in a generated file kept in sync with npm's keys endpoint by scripts/update-npm-signing-keys.mjs; the release workflow runs the check as a gate so a key rotation cannot silently break verification. Backport of #12292 (CAND-PNPM-097) to v10. * fix: harden package-manager bootstrap metadata Resolve package-manager bootstrap traffic through trusted user/CLI registries and trusted network config, defaulting to the public npm registry instead of project/workspace registry settings: - getConfig() now computes packageManagerRegistries and packageManagerNetworkConfig from trusted config sources only (CLI options, env config, user and global .npmrc) — never the repository's project/workspace .npmrc or pnpm-workspace.yaml. - switchCliVersion() applies that bootstrap config when installing and verifying the wanted pnpm version, so repository .npmrc proxy/TLS/registry values cannot steer package-manager bootstrap traffic. Backport of #12296 to v10. The v11 env-lockfile validation parts do not apply: v10 bootstraps the wanted version through a staged child install instead of an env lockfile. * fix(security): verify Node.js runtime SHASUMS OpenPGP signature When a repository requests a Node.js runtime (useNodeVersion or an execution env), pnpm downloads and then executes a Node binary. The download mirror is repository-configurable via node-mirror:<channel> in project .npmrc, and the integrity came from SHASUMS256.txt fetched from that same mirror — a circular check a malicious mirror can satisfy with a tampered binary and matching hashes. pnpm now fetches SHASUMS256.txt.sig and verifies its detached OpenPGP signature against the Node.js release team's public keys, embedded in the pnpm CLI, before trusting the hashes: - @pnpm/crypto.shasums-file: new fetchVerifiedNodeShasums / fetchVerifiedNodeShasumsFile verify the signature via openpgp against the embedded keys (generated src/nodeReleaseKeys.ts, mirrored from the canonical nodejs/release-keys list). - @pnpm/node.fetcher verifies the configurable-mirror SHASUMS for the release channel; pre-release channels (rc, nightly, ...) are unsigned by Node and remain unverified. - scripts/update-node-release-keys.mjs keeps the keys current (pnpm run check:node-release-keys / update:node-release-keys), and the release workflow runs the check as a gate. Backport of #12295 to v10 (without the pacquet Rust port, which does not exist on this branch). * test(env): sign the SHASUMS fixture for Node.js download tests The Node.js download tests exercise the release channel, whose SHASUMS256.txt is now signature-verified. Sign the fixture with a generated OpenPGP key and trust it through the new trustedNodeReleaseKeys test seam (threaded from plugin-commands-env via @pnpm/node.fetcher to fetchVerifiedNodeShasums), so the tests keep exercising the verification path instead of bypassing it. * fix(self-updater): redact registry credentials from engine identity errors Registry URLs may legally embed basic-auth credentials (https://user:pass@host/). verifyPnpmEngineIdentity() interpolated the packument URL and registry URL into PnpmError messages, and the unreachable-registry path surfaced fetch-layer error messages that embed the request URL — all of which land in terminal output and CI logs. Strip URL credentials from every error message and truncate the non-200 response body. * fix: update vulnerable transitive dependencies Override shell-quote to >=1.8.4 (GHSA-w7jw-789q-3m8p, critical, pulled in via concurrently) so the audit workflow passes again. The advisory was published after the last release/10 audit run; it is unrelated to the security backports on this branch.
Summary
Follow-up to #12292 (which verifies the package-manager binary). This closes the same class of gap for the Node.js runtime.
When a repository requests a Node.js runtime —
devEngines.runtime: node@X(withonFail: download, the default) oruseNodeVersion— pnpm downloads and then executes a Node binary (it's used to run lifecycle /run/execscripts). The download mirror is repository-configurable vianode-mirror:<channel>(nodeDownloadMirrors) in project.npmrc, and the integrity comes fromSHASUMS256.txtfetched from that same mirror.That's a circular check: a malicious mirror serves a tampered
nodetarball and a matchingSHASUMS256.txt, the sha256 check passes, and pnpm runs the binary. Drive-by on a normal command in a cloned repo.Fix
pnpm now fetches
SHASUMS256.txt.sigand verifies its detached OpenPGP signature against the Node.js release team's public keys, embedded in the pnpm CLI, before trusting the hashes. A mirror that serves a tampered binary cannot also produce a valid signature, so verification fails. Any faithful mirror (one that proxies the real signed SHASUMS) keeps working.@pnpm/crypto.shasums-file: newfetchVerifiedNodeShasums/fetchVerifiedNodeShasumsFileverify the signature viaopenpgpagainst the embedded keys.src/nodeReleaseKeys.ts, 28 keys) mirrored from the canonicalnodejs/release-keyslist.crypto/shasums-file/scripts/update-node-release-keys.mjskeeps them current (pnpm check:node-release-keys/--update), and the create-release-pr workflow runs the check as a gate so a new release signer can't silently break verification.@pnpm/engine.runtime.node-resolververifies the configurable-mirror SHASUMS. The hardcodedunofficial-builds.nodejs.orgmusl mirror is not repo-configurable and is signed by a different key, so it stays trusted over TLS.Scope
releasechannel (noSHASUMS256.txt.sigexists for them, even on nodejs.org), so they remain unverifiable. Verification is gated on thereleasechannel.github.com/oven-sh/bun,api.github.com/repos/denoland/deno), not mirror-configurable, so a repo can't redirect them.pacquet/crates/engine-runtime-node-resolverhas the same mirror-configurable SHASUMS logic and needs the equivalent Rust port — tracked as a follow-up (per the repo's parity rule, opening the TS side first).Testing
@pnpm/crypto.shasums-file: 7 (incl. 4 new — valid signature, untrusted key, tampered content, missing.sig), using openpgp-generated test keys.@pnpm/engine.runtime.node-resolver: 54 passing.v22.11.0SHASUMS256.txtsignature end-to-end.Written by an agent (Claude Code, claude-opus-4-8).
Summary by CodeRabbit
New Features
Release Process
Tests
Chores
Documentation