Skip to content

Fix yarn berry hosted pin leaking npm auth (#404) - #465

Open
Mikola Lysenko (mikolalysenko) wants to merge 9 commits into
mainfrom
agent/fix-yarn-berry-hosted-auth-leak
Open

Mikola Lysenko (mikolalysenko) wants to merge 9 commits into
mainfrom
agent/fix-yarn-berry-hosted-auth-leak

Conversation

@mikolalysenko

@mikolalysenko Mikola Lysenko (mikolalysenko) commented Oct 1, 2026 •

Copy link
Copy Markdown
Collaborator

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 is
still an npm: locator, so yarn fetches it with its npm fetcher. The npm
fetcher attaches the configured registry credentials (npmAuthToken,
YARN_NPM_AUTH_TOKEN, npmScopes.*.npmAuthToken) to every scoped package's
request, and to every request under npmAlwaysAuth: true. Those requests go
to the patch host, so the registry token was sent there on every cold install.

Fix: pin the way yarn does for a root resolutions entry

This follows the maintainer's decision on
option C.
A tarball locator under the untouched npm: key (the first version of this
PR) 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 entry
    carries, 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 the
    same URL as its resolution: and the patched 10c0 checksum. version:,
    dependencies: and every dependent's descriptors stay byte-identical. The
    entry moves to yarn's key order, because yarn sorts entries and --immutable
    would 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:

  • Yarn berry only. package.json is read only beside a berry yarn.lock,
    and the memory host fetches it only there. Yarn classic, npm, pnpm, bun and
    vlt are unchanged.
  • Refusals. Each of these writes nothing and names the cause in a warning:
    • redirect_yarn_berry_resolutions_conflict: a user-authored resolutions
      entry for the package (bare, ranged or nested). It is never overwritten.
    • redirect_yarn_berry_manifest_missing: no root package.json object.
    • redirect_yarn_berry_shared_descriptor: yarn's builtin patch: entries
      (resolve, typescript, fsevents) wrap the same descriptor, so pinning
      it would change that entry too.
    • redirect_yarn_berry_artifact_url_unsupported: a URL yarn can't fetch as
      a tarball.
  • Rollback / remove: rebuilds the original lock key from the selectors,
    restores the registry resolution and checksum, moves the entry back, and
    drops the selectors. An emptied resolutions table is removed.
  • Older locks: a lock pinned by an older release (::__archiveUrl=) is
    still recognized by rollback, VEX and the takeovers, and the next hosted scan
    re-pins it.
  • Inventory and takeovers: lock-only inventory keeps the URL-keyed entry
    (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):

pin shape token sent to patch host --immutable hardened mode
npm:…::__archiveUrl= (main) yes ok ok
tarball locator under npm: key (first version of this PR) no ok YN0078
resolutions selector + URL-keyed entry (this PR) no ok, lock untouched ok (direct + transitive)

A project locking is-number@6.0.0 (patched) and is-number@7.0.0 kept the
7.0.0 registry entry.

Per-issue checklist (#404)

  • Unit tests: rewriter shape, descriptor precision and re-sorting, repeat-run
    stability, re-pin on a new uuid, legacy migration, and every refusal. These
    live in patch/redirect/mod.rs; the shape tests fail on main.
  • Layering: the builtin patch: package (resolve) is now left untouched,
    with both reasons named.
  • Inventory regression test for Bugbot's finding (red → green).
  • CLI in-process tests: the pin, CRLF/BOM pin + rollback round-trip (lock
    byte-exact, resolutions dropped), and legacy rollback + re-pin.
  • Takeovers: vendored → hosted and back, with BOM + CRLF kept.
  • Real-yarn e2e: e2e_redirect_yarn_berry_build runs the fresh
    install --immutable --check-cache in hardened mode
    (YARN_ENABLE_HARDENED_MODE) with a registry token configured, and asserts
    the patch host got no Authorization header. With the same env, the first
    version of this PR fails with YN0078.

Local runs

  • clippy -D warnings: clean.
  • socket-patch-core: 4714 lib tests pass plus all integration tests,
    including the regenerated redirect and VEX goldens.
  • CLI suites: the lib tests plus in_process_redirect, in_process_vendor,
    in_process_rollback_hosted/vendored, in_process_scan, e2e_vex_lockfile,
    hosted_memory_*, and the covgap suites for scan, rollback, vendor and vex.
  • Real yarn 4.12.0, SOCKET_PATCH_YARN_E2E_REQUIRED=1: redirect, vendor,
    pnpm-linker and workspaces suites, 53/53.
  • Known sandbox-only failures: the write-failure / permission tests fail here
    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 from
    this sandbox (TLS through the proxy), so CI runs them.

Follow-ups


Note

Medium Risk
Changes hosted-mode lockfile and package.json wiring for Yarn Berry across redirect, rollback, discovery, and takeovers; behavior is well-tested but affects CI install paths.

Overview
Fixes #404: hosted Yarn Berry no longer uses an npm: locator with ::__archiveUrl=, which caused Yarn’s npm fetcher to send registry tokens (npmAuthToken, YARN_NPM_AUTH_TOKEN, npmAlwaysAuth) to the patch host.

Hosted redirect now matches Yarn’s root resolutions pattern: package.json gets descriptor selectors (e.g. "left-pad@npm:^1.3.0") pointing at the hosted tarball, and yarn.lock re-keys the entry to "<name>@<url>" with a plain tarball resolution: and the patched 10c0 checksum. Re-keyed entries are re-sorted when the lock was already in Yarn order so yarn install --immutable and hardened mode (YN0078) stay valid.

Rollback/remove, lock inventory, and takeover detection understand the new shape and legacy ::__archiveUrl= pins (re-pinned on the next hosted scan). New fail-closed warnings cover conflicting user resolutions, missing manifest, shared patch: descriptors, and unsupported artifact URLs. Docs, fixtures, and e2e tests assert no Authorization on patch-host tarball fetches with tokens configured.

Reviewed by Cursor Bugbot for commit f44ebc6. Configure here.


Generated by Claude Code

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
@mikolalysenko
Mikola Lysenko (mikolalysenko) force-pushed the agent/fix-yarn-berry-hosted-auth-leak branch from 896a700 to e20d139 Compare October 1, 2026 12:49
@mikolalysenko
Mikola Lysenko (mikolalysenko) marked this pull request as ready for review October 1, 2026 13:05
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

BugBot review


Generated by Claude Code

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread crates/socket-patch-core/src/patch/redirect/mod.rs Outdated
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
@mikolalysenko
Mikola Lysenko (mikolalysenko) marked this pull request as draft October 1, 2026 13:15
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

[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 hosted-e2e (yarn_berry_hosted_install_proof) found that yarn's hardened mode rejects it:

YN0078: Invalid resolution minimist@npm:1.2.2 → https://patch.socket.dev/patch/npm/minimist/1.2.2/…/minimist-1.2.2.tgz

Yarn enables hardened mode automatically for GitHub Actions runs on public pull requests, and enableHardenedMode: true turns it on anywhere. I reproduced it locally with yarn 4.12.0. Hardened mode checks that each lock resolution is valid for its descriptor, and a URL locator is not valid for an npm: descriptor. So this PR as written would break installs in that common setup. It must not merge as is.

Measured options (yarn 4.12.0, fresh checkout, cold cache, YARN_NPM_AUTH_TOKEN + npmAlwaysAuth, logging tarball server):

pin shape sends registry token to patch host --immutable hardened mode
A. npm:<v>::__archiveUrl=<url> (main today) yes (scoped always; all under npmAlwaysAuth) ok ok
B. tarball locator under the npm: key (this PR) no ok YN0078, fails
C. root package.json resolutions: {"<name>@npm:<range>": "<url>"} + lock entry keyed by the URL descriptor (what yarn itself writes) no ok, lock untouched, same 10c0 checksum ok

Option C is the only shape that fixes the leak and works everywhere. But hosted mode would then edit package.json, not just the yarn.lock entry the CLI contract promises today. That is the same resolutions + lock wiring that vendored berry mode already uses with file:. The change also affects rollback/remove, the vendored⇄hosted takeovers, users' own resolutions entries, and workspaces. The other option is to keep A and refuse or warn when registry auth is configured. That can't see CI-only tokens like YARN_NPM_AUTH_TOKEN, so it would only partly help.

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 state: blocked.


Generated by Claude Code

@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

[burn-down agent] Adding agent:needs-human: this PR is waiting on the maintainer decision in #465 (comment). The question: should the hosted yarn berry pin switch to option C (root resolutions + a URL-keyed lock entry), which stops the token leak and passes hardened mode, or stay on npm: + a gate? Option B (this PR as written) fails hardened mode with YN0078. Burn-down runs will skip this PR until the label is removed. It also conflicts with main; that will be resolved after the decision.


Generated by Claude Code

Assisted-by: Claude Code:claude-opus-5-5
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

[agent] test (macos-latest) on 0c461dc failed in socket-patch-core --lib on one test, api::vendor_prefetch::tests::an_outage_mid_list_opens_the_breaker_at_the_same_package (left: 16, right: 12: "a mid-list outage must not be amplified by the plan either"). This is not caused by this PR.

  • This PR doesn't touch crates/socket-patch-core/src/api/.
  • The test scripts mock-server latencies of 10–30 ms and asserts an exact request count. On a slow runner, the prefetch can start more downloads before the circuit breaker opens, so the count is sensitive to timing.
  • On this same head it passes on test (ubuntu-latest) and in coverage. It passed 5/5 runs locally, and recent main runs are green.

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).

hosted-e2e is still red for the hardened-mode reason in the decision comment above. This PR stays blocked on that decision.


Generated by Claude Code

@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

[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 hosted-e2e (yarn_berry_hosted_install_proof) found that yarn's hardened mode rejects it:

YN0078: Invalid resolution minimist@npm:1.2.2 → https://patch.socket.dev/patch/npm/minimist/1.2.2/…/minimist-1.2.2.tgz

Yarn enables hardened mode automatically for GitHub Actions runs on public pull requests, and enableHardenedMode: true turns it on anywhere. I reproduced it locally with yarn 4.12.0. Hardened mode checks that each lock resolution is valid for its descriptor, and a URL locator is not valid for an npm: descriptor. So this PR as written would break installs in that common setup. It must not merge as is.

Measured options (yarn 4.12.0, fresh checkout, cold cache, YARN_NPM_AUTH_TOKEN + npmAlwaysAuth, logging tarball server):

pin shape sends registry token to patch host --immutable hardened mode
A. npm:<v>::__archiveUrl=<url> (main today) yes (scoped always; all under npmAlwaysAuth) ok ok
B. tarball locator under the npm: key (this PR) no ok YN0078, fails
C. root package.json resolutions: {"<name>@npm:<range>": "<url>"} + lock entry keyed by the URL descriptor (what yarn itself writes) no ok, lock untouched, same 10c0 checksum ok
Option C is the only shape that fixes the leak and works everywhere. But hosted mode would then edit package.json, not just the yarn.lock entry the CLI contract promises today. That is the same resolutions + lock wiring that vendored berry mode already uses with file:. The change also affects rollback/remove, the vendored⇄hosted takeovers, users' own resolutions entries, and workspaces. The other option is to keep A and refuse or warn when registry auth is configured. That can't see CI-only tokens like YARN_NPM_AUTH_TOKEN, so it would only partly help.

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 state: blocked.

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.

@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

[agent] Thanks, reworking to option C. I checked the shape with real yarn 4.12 (fresh checkout, cold cache, enableHardenedMode: true, YARN_NPM_AUTH_TOKEN + npmAlwaysAuth):

  • package.json gets one bare-name resolutions entry per patched package ("left-pad": "<hosted tgz url>"). This is the same wiring vendored berry mode already writes with file:, so it goes through the same gates: a user-authored entry for the package, alias consumers, or more than one locked version of the package means refuse rather than overwrite.
  • In yarn.lock only that entry's key and resolution: change, to name@<url>. The checksum, version: and the parents' dependencies: stay byte-identical, exactly what yarn itself writes.
  • Result: yarn install --immutable --check-cache passes in hardened mode for both direct and transitive packages, the lock stays untouched, and the patch host gets no Authorization header.

Scope:

  • Yarn berry only. Classic yarn, npm, pnpm, bun and vlt are unchanged, and package.json is never edited for them.
  • I'll assume hardened mode for every berry project.
  • rollback / remove, the vendored ⇄ hosted takeovers, VEX discovery, and locks pinned by older releases (npm:…::__archiveUrl=) will all be handled.

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
@mikolalysenko
Mikola Lysenko (mikolalysenko) marked this pull request as ready for review October 1, 2026 17:55
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

BugBot review


Generated by Claude Code

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread crates/socket-patch-core/src/vex/discover/yarn.rs
Comment thread crates/socket-patch-core/src/patch/redirect/mod.rs
Comment thread crates/socket-patch-core/src/patch/redirect/mod.rs
Comment thread crates/socket-patch-core/src/hosted/engine.rs
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
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

BugBot review


Generated by Claude Code

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

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
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

BugBot review


Generated by Claude Code

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ 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 f44ebc6. Configure here.

@mikolalysenko Mikola Lysenko (mikolalysenko) added the Ready for review Agent-verified: mergeable, CI green, Bugbot clean — awaiting human review label Oct 1, 2026
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Burn-down agent: ready for review at f44ebc6 (f44ebc6e8bd721ea6277b2abad7a45f43fcb66d5).

  • CI: all 490 check runs success or skipped on the head SHA, no failures or pending runs
  • Bugbot: reviewed f44ebc6, no findings; 0 unresolved review threads
  • Mergeable, no conflicts
  • Reviewer note: Large diff (48 files); branch is 1 commit behind main with no conflict. Focus on the resolutions-routed tarball entry for berry hosted pins.

Generated by Claude Code

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Ready for review Agent-verified: mergeable, CI green, Bugbot clean — awaiting human review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Hosted yarn berry redirect makes yarn send the project's npm registry auth token to the patch host

2 participants