Fix yarn berry hosted pin leaking npm auth (#404) - #465
Conversation
Assisted-by: Claude Code:claude-opus-5-5
Hosted mode pinned a patched yarn berry package as an npm: locator (npm:<v>::__archiveUrl=<url>). Yarn fetches npm: locators with its npm fetcher, which attaches the configured registry token (npmAuthToken, YARN_NPM_AUTH_TOKEN, npmScopes) to every scoped package request, and to every request under npmAlwaysAuth, so the token was sent to the patch server on each cold install. The lock now pins a plain tarball-URL locator (<name>@<url>). Yarn fetches it with its tarball fetcher, which sends no registry auth and builds the same cache zip, so the 10c0 checksum is unchanged and --immutable still passes. Rollback, VEX and the mode takeovers keep recognizing the old form, and the next hosted scan re-pins it. An artifact URL yarn could not fetch as a tarball is refused instead of written. Fixes #404 Assisted-by: Claude Code:claude-opus-5-5
896a700 to
e20d139
Compare
|
BugBot review Generated by Claude Code |
Lock-only discovery skipped a berry entry whose resolution is a tarball URL, so a package already pinned by hosted mode dropped out of the inventory. Treat a tarball-URL resolution under npm: descriptor keys as the registry package. Also refresh the redirect and VEX goldens and the vendor takeover test for the new pin form. WIP: yarn's hardened mode rejects this pin form (YN0078), see #465. Assisted-by: Claude Code:claude-opus-5-5
|
[agent] Blocked: needs a maintainer decision on the hosted yarn berry pin shape. The tarball-URL locator in this PR does stop the token leak, but CI's Yarn enables hardened mode automatically for GitHub Actions runs on public pull requests, and Measured options (yarn 4.12.0, fresh checkout, cold cache,
Option C is the only shape that fixes the leak and works everywhere. But hosted mode would then edit Decision needed: (1) move hosted berry to option C (I can rework this PR, since most of the detection, rollback and VEX changes carry over), (2) keep A with a warn/refuse gate plus docs, or (3) something else. WIP is pushed (5229052). The PR is back in draft with Generated by Claude Code |
|
[burn-down agent] Adding Generated by Claude Code |
Assisted-by: Claude Code:claude-opus-5-5
|
[agent]
No fix exists yet. The robust fix would be to make the test's request-count assertion independent of wall-clock latency, which belongs in its own PR. I'll re-run the macOS job once when the rest of this workflow finishes (GitHub refuses a job re-run while the run is still in progress).
Generated by Claude Code |
Option C seems like the best choice, leaking auth tokens is not really acceptable. However we should avoid unnecessary package.json edits if we can avoid it. These changes should only be used if we are in yarn. If we do see a yarn project we should aslo default assume they may be running in hardened mode since we can't easily check otherwise. See if you can rework this and keep things working similarly for other versions of yarn. It sucks that yarn is so complicated. |
|
[agent] Thanks, reworking to option C. I checked the shape with real yarn 4.12 (fresh checkout, cold cache,
Scope:
I'll push in steps and keep the status block current. Generated by Claude Code |
The tarball-URL pin stopped the registry token leak, but yarn's hardened mode (on by default for public pull request CI) rejects a tarball resolution under an npm: lock key (YN0078), breaking installs. Hosted mode now pins a yarn berry package the way yarn itself does for a root resolutions entry: package.json routes each locked descriptor (name@npm:<range>) to the hosted tarball, and the lock entry is re-keyed name@<url>, moved to yarn's sort order. Only that package's descriptors move; other versions and other files stay untouched. Rollback rebuilds the original key from those selectors and removes them. A user-authored resolutions entry for the package, a missing manifest, or a builtin patch: entry wrapping the same descriptor refuse the pin instead of overwriting anything. Other npm flavors are unchanged; package.json is only read beside a yarn berry lock. Assisted-by: Claude Code:claude-opus-5-5
Lock-only inventory now keeps a berry entry keyed by its hosted tarball, so already-pinned packages stay visible to later scans. The yarn berry redirect goldens gain a root package.json and expect the resolutions selectors plus the re-keyed entry; the VEX discovery golden, the vendor takeover test and the docs, CLI contract and changelog describe the new pin shape and its refusals. Assisted-by: Claude Code:claude-opus-5-5
Assisted-by: Claude Code:claude-opus-5-5
|
BugBot review Generated by Claude Code |
A yarn.lock entry already keyed by a tarball URL is now only treated as ours when it points at the patch host or names the exact artifact. A mirror tarball of the same version is left alone and refused as a user resolution instead of being re-pinned. VEX discovery no longer reports a hosted patch from a URL-keyed lock entry unless package.json still routes the package there. A lock that lost its resolutions entry is flagged as orphaned rather than counted as patched, and a resolutions value on its own does not confirm a redirect either. The orphan refusal now tells users to restore yarn.lock and package.json from version control, or delete the entry and reinstall. Assisted-by: Claude Code:claude-opus-5-5
|
BugBot review Generated by Claude Code |
The real-yarn berry suites run on CRLF files on Windows, as yarn writes them there. The new check that the lock entry is keyed by the hosted tarball expected an LF right after the key, so it failed on every Windows run even though the lock was rewritten correctly. The check now compares whole lines with the CR stripped. Assisted-by: Claude Code:claude-opus-5-5
|
BugBot review Generated by Claude Code |
The hosted->vendored takeover's per-package preflight read the still-hosted package.json / yarn.lock. Hosted wiring that also writes a Socket-owned package.json resolutions pin (#465) was then mistaken for a user override (vendor_override_conflict) and the takeover refused. The preflight now dry-runs the takeover's own restore_upstream and runs resolutions_gate / scan_berry_target on the restored text (RestoreOutcome.staged_text), so only the user's own wiring can refuse. The #369 regression test now mounts the upstream entry the restore reads and drops a leftover debug eprintln. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014hMbzxwUnf5voAqbc8g4df
The berry rewriter claimed every npm patch as its own before looking at yarn.lock. A grant without a yarnBerry10c0 checksum, or with a URL yarn cannot fetch as a tarball, was then left owned but unconfirmed, so hosted mode stopped counting a package the lock does not pin (and that another lockfile may have rewritten). Ownership is now taken only once the lock is known to pin the package version. The checksum is required only when the entry has to be rewritten, so a rescan with a checksumless grant still confirms a pin an earlier run completed. Assisted-by: Claude Code:claude-opus-5-5
|
BugBot review Generated by Claude Code |
Assisted-by: Claude Code:claude-opus-5-5
|
BugBot review Generated by Claude Code |
A patch server URL containing "$" was expanded as a regex capture reference when written into the berry lock entry, corrupting its resolution and checksum lines. The URL is now written literally. When the berry preflight refuses a lock (mixed line endings, an unsupported cacheKey), packages that lock pins are still decided by the berry rewriter, and left unconfirmed. Before, a URL from an earlier run in such a lock could confirm the package through the hosted text probe. Assisted-by: Claude Code:claude-opus-5-5
|
BugBot review Generated by Claude Code |
|
Second pass reviewed The orphaned/refused-pin handling, complete-pin checksum handling, and literal The current #465/#470 heads merge cleanly. Their exact combined tree ( This supersedes my earlier recommendation not to merge the pair unchanged: the compatibility fix is on #470 and the current pair is validated. Full platform CI remains required; the local checks ran on macOS. Follow-up on final merged head |
Main now reads the root package.json for the npm lock rewriter as advisory input (#490/#491). Beside a berry yarn.lock the manifest is still read strictly, since the berry pin writes it; otherwise the advisory read applies. The berry-only memory selection rule is dropped because main always fetches package.json. Assisted-by: Claude Code:claude-opus-5-5
|
BugBot review Generated by Claude Code |
The npm lock rewriter now also reads the root package.json (#490), so the contract says the berry pin is the one that edits it, not the only reader. Assisted-by: Claude Code:claude-opus-5-5
|
BugBot review Generated by Claude Code |
|
[agent] CI note on dc9330f: I don't believe this failure comes from this PR:
No fix exists to port. I'm re-running the failed job once, after the run finishes, since GitHub won't re-run a job while its run is still going. If it fails again, I'll treat it as real and dig into the captures. Generated by Claude Code |
A lock entry keyed `left-pad@npm:other@^1.3.0` installs the package `other` under the left-pad name. If that fork happened to be at the patched version, the hosted rewrite re-keyed it to the left-pad tarball, replacing the user's fork with the patched package. Fork aliases are now skipped and do not make the real entry ambiguous. Assisted-by: Claude Code:claude-opus-5-5
|
BugBot review Generated by Claude Code |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 8d52ba6. Configure here.
|
[agent] CI note on 8d52ba6: I don't believe this failure comes from this PR:
No fix exists to port. I'm re-running the failed job once after the run finishes. If it fails again, I'll treat it as real and dig into the captures. Generated by Claude Code |
LLM Description written by Claude Code:claude-opus-5-5
Fixes #404
Root cause
On yarn berry, hosted mode pinned a patched package by rewriting its lock
entry's resolution to
<name>@npm:<v>::__archiveUrl=<hosted tgz>. That isstill an
npm:locator, so yarn fetches it with its npm fetcher. The npmfetcher attaches the configured registry credentials (
npmAuthToken,YARN_NPM_AUTH_TOKEN,npmScopes.*.npmAuthToken) to every scoped package'srequest, and to every request under
npmAlwaysAuth: true. Those requests goto the patch host, so the registry token was sent there on every cold install.
Fix: pin the way yarn does for a root
resolutionsentryThis follows the maintainer's decision on
option C.
A tarball locator under the untouched
npm:key (the first version of thisPR) stops the leak, but yarn's hardened mode rejects it with
YN0078.Yarn turns hardened mode on by itself for public-PR CI, so hosted mode now
assumes every berry project may run hardened. For each patched package:
package.json: one descriptor-specific selector per range the lock entrycarries, routed to the hosted tarball, e.g.
"resolutions": {"left-pad@npm:^1.3.0": "https://patch.socket.dev/…/left-pad-1.3.0.tgz"}.This is the minimal edit. Other locked versions of the same package and every
other file are untouched.
yarn.lock: only that entry is re-keyed"left-pad@<url>":, with thesame URL as its
resolution:and the patched10c0checksum.version:,dependencies:and every dependent's descriptors stay byte-identical. Theentry moves to yarn's key order, because yarn sorts entries and
--immutablewould otherwise rewrite the lock.
This is exactly what yarn writes itself, verified with real yarn. Yarn then
uses its tarball fetcher, which sends no registry auth and builds the same
cache zip.
Scope and safety:
package.jsonis read only beside a berryyarn.lock,and the memory host fetches it only there. Yarn classic, npm, pnpm, bun and
vlt are unchanged.
redirect_yarn_berry_resolutions_conflict: a user-authoredresolutionsentry for the package (bare, ranged or nested). It is never overwritten.
redirect_yarn_berry_manifest_missing: no rootpackage.jsonobject.redirect_yarn_berry_shared_descriptor: yarn's builtinpatch:entries(
resolve,typescript,fsevents) wrap the same descriptor, so pinningit would change that entry too.
redirect_yarn_berry_artifact_url_unsupported: a URL yarn can't fetch asa tarball.
restores the registry resolution and checksum, moves the entry back, and
drops the selectors. An emptied
resolutionstable is removed.::__archiveUrl=) isstill recognized by rollback, VEX and the takeovers, and the next hosted scan
re-pins it.
(Bugbot's finding on the first version). Vendored ⇄ hosted takeovers work in
both directions.
Evidence
Measured with real yarn 4.12.0 (fresh checkout, cold cache,
YARN_NPM_AUTH_TOKEN+npmAlwaysAuth, logging tarball server):--immutablenpm:…::__archiveUrl=(main)npm:key (first version of this PR)resolutionsselector + URL-keyed entry (this PR)A project locking
is-number@6.0.0(patched) andis-number@7.0.0kept the7.0.0 registry entry.
Per-issue checklist (#404)
stability, re-pin on a new uuid, legacy migration, and every refusal. These
live in
patch/redirect/mod.rs; the shape tests fail onmain.patch:package (resolve) is now left untouched,with both reasons named.
byte-exact,
resolutionsdropped), and legacy rollback + re-pin.e2e_redirect_yarn_berry_buildruns the freshinstall --immutable --check-cachein hardened mode(
YARN_ENABLE_HARDENED_MODE) with a registry token configured, and assertsthe patch host got no
Authorizationheader. With the same env, the firstversion of this PR fails with YN0078.
Local runs
-D warnings: clean.socket-patch-core: 4714 lib tests pass plus all integration tests,including the regenerated redirect and VEX goldens.
in_process_rollback_hosted/vendored, in_process_scan, e2e_vex_lockfile,
hosted_memory_*, and the covgap suites for scan, rollback, vendor and vex.
SOCKET_PATCH_YARN_E2E_REQUIRED=1: redirect, vendor,pnpm-linker and workspaces suites, 53/53.
because the sandbox runs as root, and fail the same way on
main.mode_migration_npm's two berry legs can't reach the real registry fromthis sandbox (TLS through the proxy), so CI runs them.
Follow-ups
yarn install --immutablefails YN0028 #368 (yarn builtin-patch:packages) now gets a clear refusal instead of asilent no-op. Patching those packages in hosted mode remains open.
Note
Medium Risk
Changes Yarn Berry hosted lockfile and root package.json rewriting and redirect confirmation/VEX behavior; scoped to Berry hosted mode but affects real installs and rollback/migration paths.
Overview
Fixes #404 by changing how Yarn Berry hosted mode pins patched packages so installs no longer use an
npm:locator (::__archiveUrl=), which caused Yarn’s npm fetcher to send registry tokens to the patch host.Hosted redirects now mirror a root
resolutionspin:package.jsongets descriptor-specific selectors (e.g."left-pad@npm:^1.3.0"→ hosted tarball URL), andyarn.lockre-keys the entry to"<name>@<url>"with a tarballresolution:and10c0checksum (entries re-sorted when needed for--immutable/ hardened mode). Legacy::__archiveUrl=pins are still recognized for rollback/takeover and are re-pinned on the next hosted scan.The hosted engine reads
package.jsonstrictly beside a Berry lock, tracksconfirmed_yarn_berry_uuidsso a URL in the lock or manifest alone cannot confirm a redirect, and lock inventory/takeover logic treats tarball-keyed hosted entries as registry packages. Docs (CHANGELOG, CLI_CONTRACT) and broad unit/e2e tests (including noAuthorizationto the patch host under hardened mode + registry token env) are updated accordingly.Reviewed by Cursor Bugbot for commit 8d52ba6. Configure here.
Generated by Claude Code