You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Hosted yarn berry pin of a catalog: dependency keys resolutions by the resolved npm: range, so every yarn install --immutable fails YN0028 (regression from #465) #632
[agent] Found by the scheduled Yarn Berry (2+) bug-hunt routine (ledger #305).
Summary
Since #465 (203e092), a hosted Berry pin is a root package.jsonresolutions entry keyed by the locked descriptor (left-pad@npm:^1.3.0), plus the lock entry re-keyed left-pad@<url>. When the dependency is declared through a Yarn catalog ("left-pad": "catalog:", with catalog: {left-pad: ^1.3.0} in .yarnrc.yml), yarn applies resolutions to the manifest descriptor left-pad@catalog:before the catalog is expanded to npm:^1.3.0. So the left-pad@npm:^1.3.0 selector never matches. Yarn resolves the registry release again, and the lock socket-patch wrote no longer matches what yarn computes.
scan --mode hosted exits 0 with status: success, redirected: 1, and no warning. Then:
yarn install --immutable (in place or in a fresh checkout) fails with YN0028 ("The lockfile would have been modified"). Yarn wants to turn left-pad@<url> back into left-pad@npm:1.3.0.
A mutable yarn install silently installs the unpatched registry bytes and drops the pin from yarn.lock. The stale resolutions entry stays in package.json.
Re-running scan --mode hosted reports redirected: 1 again and writes the same broken pin, so the loop never converges.
Release 4.0.0, with its ::__archiveUrl= lock-only pin, handles the same project correctly: the fresh --immutable install gets the patched bytes. This is a regression.
vex does the right thing after the mutable install. The lock no longer carries a pin, so it attests nothing (manifest_not_found). There's no false attestation.
Impact
Any Yarn ≥ 4.10 project that uses catalogs, which are increasingly the norm in monorepos, can't use hosted mode. CI (enableImmutableInstalls is on by default under CI) goes red immediately after the hosted commit.
Local devs running plain yarn install silently get unpatched code. The command reports success throughout.
Repro (Linux, yarn 4.18.1, node-modules linker)
mkdir c &&cd c
echo'{"name":"c","private":true,"dependencies":{"left-pad":"catalog:"}}'> package.json
printf'nodeLinker: node-modules\nenableGlobalCache: false\ncatalog:\n left-pad: ^1.3.0\n'> .yarnrc.yml
yarn install
socket-patch scan --mode hosted --json --yes --api-url <mock> --org org --api-token x --patch-server-url <mock># -> status success, redirected 1, warnings []# package.json gains: "resolutions": {"left-pad@npm:^1.3.0": "<mock>/patch/npm/left-pad/1.3.0/tok/<uuid>/left-pad-1.3.0.tgz"}
yarn install --immutable
# ➤ YN0028: -"left-pad@<url>":# ➤ YN0028: +"left-pad@npm:^1.3.0":# ➤ YN0028: The lockfile would have been modified by this install, which is explicitly forbidden.
yarn install && head -c 12 node_modules/left-pad/index.js # unpatched registry bytes
The patch API was a local mock serving a granted yarn-berry-zip artifact whose yarnBerry10c0 was bootstrapped with a real yarn resolutions: file: install, the same approach as crates/socket-patch-cli/tests/e2e_redirect_yarn_berry_build.rs.
Which selector yarn honours for a catalog dependency (mutable install with each hand-written resolutions key → URL):
resolutions key
installed bytes
left-pad@npm:^1.3.0 (what socket-patch writes)
unpatched, lock entry left-pad@npm:^1.3.0
left-pad@catalog:
patched, lock entry left-pad@<url>
left-pad (bare)
patched, lock entry left-pad@<url>
A workspace mixing catalog: (root and member b) with a named catalog:legacy (member a, 1.3.0) fails the same way. socket-patch writes left-pad@npm:1.3.0 and left-pad@npm:^1.3.0, and yarn wants a merged "left-pad@npm:1.3.0, left-pad@npm:^1.3.0" registry entry.
Expected vs actual
Expected:docs/ecosystems.md (yarn berry hosted notes) says the redirect "pins the way yarn does for a root resolutions entry … package.json routes the locked descriptor to the hosted tarball", and CLI_CONTRACT.md says a dep counts as redirected only when the pin actually lands. The pin should be keyed by the descriptor yarn matches resolutions against (name@catalog: / name@catalog:<named> for catalog deps). Failing that, a catalog-consumed descriptor should be refused loudly, the way pnpm vendoring refuses catalog-consumed deps (fix(vendor): refuse catalog-consumed deps in pnpm vendoring #184), rather than reported as success.
Actual: exit 0, redirected: 1, no warning. The next immutable install fails YN0028, and a mutable install is silently unpatched.
Matrix
OS
yarn
socket-patch
Result
Linux
4.18.1
main 045d7ec (3 runs: minimal root, workspace + named catalog, re-scan loop)
untested; the selector choice is OS-independent. No probe branches this run: the sandbox git proxy still drops branch deletes.
First bad commit:203e092 (#465, "Fix yarn berry hosted pin leaking npm auth (#404)"), which introduced the resolutions pin. Run 8 of this routine passed the same catalog: cell with the old __archiveUrl pin.
Vendored mode isn't affected: it passed catalog: and catalog:legacy in run 8 because it pins by package name.
Suspect code
crates/socket-patch-core/src/patch/redirect/mod.rs:4081, in berry_resolutions_pin: selectors are built as format!("{name}@{r}") from the lock entry's npm ranges (key_ranges). For a catalog-consumed dependency the manifest descriptor is catalog: / catalog:<name>, which is what yarn's resolutions matcher sees.
No catalog handling exists anywhere in patch/redirect/mod.rs, so the hosted gate has no catalog refusal either.
[agent] Found by the scheduled Yarn Berry (2+) bug-hunt routine (ledger #305).
Summary
Since #465 (
203e092), a hosted Berry pin is a rootpackage.jsonresolutionsentry keyed by the locked descriptor (left-pad@npm:^1.3.0), plus the lock entry re-keyedleft-pad@<url>. When the dependency is declared through a Yarn catalog ("left-pad": "catalog:", withcatalog: {left-pad: ^1.3.0}in.yarnrc.yml), yarn appliesresolutionsto the manifest descriptorleft-pad@catalog:before the catalog is expanded tonpm:^1.3.0. So theleft-pad@npm:^1.3.0selector never matches. Yarn resolves the registry release again, and the lock socket-patch wrote no longer matches what yarn computes.scan --mode hostedexits 0 withstatus: success,redirected: 1, and no warning. Then:yarn install --immutable(in place or in a fresh checkout) fails with YN0028 ("The lockfile would have been modified"). Yarn wants to turnleft-pad@<url>back intoleft-pad@npm:1.3.0.yarn installsilently installs the unpatched registry bytes and drops the pin fromyarn.lock. The staleresolutionsentry stays inpackage.json.scan --mode hostedreportsredirected: 1again and writes the same broken pin, so the loop never converges.Release 4.0.0, with its
::__archiveUrl=lock-only pin, handles the same project correctly: the fresh--immutableinstall gets the patched bytes. This is a regression.vexdoes the right thing after the mutable install. The lock no longer carries a pin, so it attests nothing (manifest_not_found). There's no false attestation.Impact
enableImmutableInstallsis on by default under CI) goes red immediately after the hosted commit.yarn installsilently get unpatched code. The command reports success throughout.Repro (Linux, yarn 4.18.1, node-modules linker)
The patch API was a local mock serving a granted
yarn-berry-zipartifact whoseyarnBerry10c0was bootstrapped with a real yarnresolutions: file:install, the same approach ascrates/socket-patch-cli/tests/e2e_redirect_yarn_berry_build.rs.Which selector yarn honours for a catalog dependency (mutable install with each hand-written
resolutionskey → URL):resolutionskeyleft-pad@npm:^1.3.0(what socket-patch writes)left-pad@npm:^1.3.0left-pad@catalog:left-pad@<url>left-pad(bare)left-pad@<url>A workspace mixing
catalog:(root and member b) with a namedcatalog:legacy(member a,1.3.0) fails the same way. socket-patch writesleft-pad@npm:1.3.0andleft-pad@npm:^1.3.0, and yarn wants a merged"left-pad@npm:1.3.0, left-pad@npm:^1.3.0"registry entry.Expected vs actual
docs/ecosystems.md(yarn berry hosted notes) says the redirect "pins the way yarn does for a rootresolutionsentry …package.jsonroutes the locked descriptor to the hosted tarball", andCLI_CONTRACT.mdsays a dep counts as redirected only when the pin actually lands. The pin should be keyed by the descriptor yarn matches resolutions against (name@catalog:/name@catalog:<named>for catalog deps). Failing that, a catalog-consumed descriptor should be refused loudly, the way pnpm vendoring refuses catalog-consumed deps (fix(vendor): refuse catalog-consumed deps in pnpm vendoring #184), rather than reported as success.redirected: 1, no warning. The next immutable install fails YN0028, and a mutable install is silently unpatched.Matrix
045d7ec(3 runs: minimal root, workspace + named catalog, re-scan loop)045d7ec--immutablepatched;::__archiveUrl=pin)First bad commit:
203e092(#465, "Fix yarn berry hosted pin leaking npm auth (#404)"), which introduced theresolutionspin. Run 8 of this routine passed the samecatalog:cell with the old__archiveUrlpin.Vendored mode isn't affected: it passed
catalog:andcatalog:legacyin run 8 because it pins by package name.Suspect code
crates/socket-patch-core/src/patch/redirect/mod.rs:4081, inberry_resolutions_pin: selectors are built asformat!("{name}@{r}")from the lock entry's npm ranges (key_ranges). For a catalog-consumed dependency the manifest descriptor iscatalog:/catalog:<name>, which is what yarn's resolutions matcher sees.cataloghandling exists anywhere inpatch/redirect/mod.rs, so the hosted gate has no catalog refusal either.