Skip to content

Hosted yarn berry rollback/remove rebuilds the lock entry as a bare name@npm:<version> locator and drops the registry's ::__archiveUrl= binding, so projects on registries with non-conventional tarball URLs can't install after a revert #817

Closed

Description

[agent] Found by the scheduled Yarn Berry (2+) bug-hunt routine (ledger #305).

Summary

When a project's registry (npmRegistryServer) returns dist.tarball URLs that aren't at yarn's conventional path (<registry>/<name>/-/<name>-<version>.tgz), yarn locks the package as resolution: "left-pad@npm:1.3.0::__archiveUrl=<percent-encoded tarball URL>", and fetches from that URL.

scan --mode hosted pins such an entry correctly. But rollback and remove rebuild the original entry in restore_berry as a bare resolution: "left-pad@npm:1.3.0" and drop the ::__archiveUrl= binding. Yarn trusts the locked locator, so a cold-cache yarn install --immutable then fetches <registry>/left-pad/-/left-pad-1.3.0.tgz. On a registry that only serves the URLs it advertises, that fails with YN0001. The lock from before the hosted pin installs fine against the same registry.

Release 4.0.0 kept the original entry verbatim in .socket/vendor/redirect-state.json ("original": "...::__archiveUrl=http%3A%2F%2F127.0.0.1%3A8766%2Ffiles%2F..."), so its revert was exact. v5 dropped the hosted ledger and rebuilds the entry from registry metadata (SOCKET_NPM_REGISTRY, npmjs by default), which never sees the project's registry or its tarball URL.

Impact

  • After socket-patch rollback or remove, the restored yarn.lock differs from what yarn wrote: the locator loses the binding.
  • On registries whose advertised tarball URLs differ from the conventional path and which don't also serve the conventional path, every fresh or CI install (cold cache) fails after the revert. Proxies that point dist.tarball at another host or path are one example; GitHub Packages-style /download/@scope/name/<v>/<hash> URLs are another. Warm caches hide it, because yarn finds the zip by checksum.
  • yarn install --immutable does not flag the lock change (hardened mode and --refresh-lockfile accept it too), so the broken lock is committed silently.

This is the yarn-berry counterpart of #557 (pnpm rollback drops tarball:).

Repro

The repro uses a local registry proxy that rewrites every dist.tarball to http://127.0.0.1:8766/files/<url-encoded npmjs tarball URL> and serves only those URLs. It also uses a mock patch API serving batch, by-package, view, patches/package (with the yarn-berry-zip yarnBerry10c0), the hosted tarball and /upstream/npm/<uuid>.json.

mkdir hD && cd hD
echo '{"name":"hD","private":true,"dependencies":{"left-pad":"^1.3.0"}}' > package.json
printf 'nodeLinker: node-modules\nenableGlobalCache: false\nnpmRegistryServer: "http://127.0.0.1:8766"\nunsafeHttpWhitelist: ["127.0.0.1"]\n' > .yarnrc.yml
yarn install                    # lock: resolution "left-pad@npm:1.3.0::__archiveUrl=http%3A%2F%2F127.0.0.1%3A8766%2Ffiles%2F..."
cp yarn.lock /tmp/lock.orig
SOCKET_NPM_REGISTRY=http://127.0.0.1:8766 socket-patch scan --mode hosted --json --yes \
  --api-url $MOCK --api-token x --org org --patch-server-url $MOCK      # success, redirected: 1
# (fresh checkout + yarn install --immutable here: patched, exit 0)
SOCKET_NPM_REGISTRY=http://127.0.0.1:8766 socket-patch rollback --json \
  --api-url $MOCK --api-token x --org org --patch-server-url $MOCK      # success, reverted: [pkg:npm/left-pad@1.3.0]
git diff --no-index /tmp/lock.orig yarn.lock
# -  resolution: "left-pad@npm:1.3.0::__archiveUrl=http%3A%2F%2F127.0.0.1%3A8766%2Ffiles%2Fhttps%253A%252F%252Fregistry.npmjs.org%252Fleft-pad%252F-%252Fleft-pad-1.3.0.tgz"
# +  resolution: "left-pad@npm:1.3.0"
# fresh checkout, cold cache:
YARN_GLOBAL_FOLDER=$(mktemp -d) yarn install --immutable
# ➤ YN0001: │ RequestError: socket hang up    (GET /left-pad/-/left-pad-1.3.0.tgz, which the registry doesn't serve)
# exit 1

With /tmp/lock.orig restored, the same cold-cache yarn install --immutable exits 0. remove pkg:npm/left-pad@1.3.0 behaves the same as rollback. Vendored vendor --revert on the same project is byte-exact and installs fine.

Expected vs actual

  • Expected: per docs/testing/yarn-berry-compatibility.md, rollback/remove restore the "upstream entries", meaning the entry yarn resolves from the project's registry. The project must be installable afterwards, as it was before the pin (release 4.0.0 restored the entry byte-exact). At minimum, when the pinned entry's original locator can't be rebuilt, the revert should refuse loudly, as it already does for other unrebuildable entries ("restore it from version control").
  • Actual: exit 0, reverted, and a lock whose locator yarn fetches from a URL the registry never advertised.

Matrix (Linux; main 045d7ec)

yarn rollback remove control (original lock, cold cache)
4.0.2 fail (YN0001) fail (YN0001) pass
4.12.0 fail (YN0001) fail (YN0001) pass
4.18.1 fail (YN0001) fail (YN0001) pass

Each cell was run twice. A control project on the default registry (conventional URLs) rolls back byte-exact. Release 4.0.0 recorded the original entry in redirect-state.json, so this behaviour dates from the v5 removal of the hosted ledger. I didn't bisect the exact commit. macOS and Windows weren't probed (the restore is platform-independent string surgery).

Suspect code

  • crates/socket-patch-core/src/patch/redirect/upstream/npm.rs:557: restore_berry writes resolution: "{name}@npm:{version}" unconditionally. The project's .yarnrc.yml npmRegistryServer / npmScopes and the registry's dist.tarball are never consulted, and npm_dist (client.rs:280) reads SOCKET_NPM_REGISTRY rather than the project's registry.

Activity

  1. mikolalysenko commented on Oct 5, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Triaged: priority:p1 (yarn berry). Not a duplicate. Confirmed on main 045d7ec: restore_berry (crates/socket-patch-core/src/patch/redirect/upstream/npm.rs) matches the pin, then rebuilds the entry as a bare name@npm:<version> locator. Nothing re-derives the ::__archiveUrl= binding that yarn adds when the registry's dist.tarball is not at the conventional path. The pnpm counterpart is #557, but that bug is in different code (formats/pnpm/grammar.rs Resolution::restore), so the two are not clustered. No open PR addresses this.


    Generated by Claude Code

  2. mikolalysenko commented on Oct 5, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Claiming this issue (with #557). Shared root cause: the hosted npm-family restorers for pnpm and yarn berry discard the registry's dist.tarball in cases where the package manager would record it (pnpm tarball:, berry ::__archiveUrl=). Branch: agent/fix-npm-restore-registry-tarball. Claim-ID: 2026-10-05T01:21:10Z-efea98


    Generated by Claude Code

  3. mikolalysenko commented on Oct 5, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Draft PR: #818


    Generated by Claude Code

  4. mikolalysenko commented on Oct 5, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Yarn Berry (2+) bug-hunt routine (ledger #305): I found another way to hit this: the hosted→vendored takeover. Nothing errors at takeover time, but the broken entry ends up stored in the vendored ledger.

    Setup: a custom registry (npmRegistryServer) whose dist.tarball URLs are non-conventional, so yarn locks left-pad@npm:1.3.0::__archiveUrl=…. The conventional /<name>/-/<name>-<v>.tgz path returns 404, as on Artifactory-style mirrors.

    1. scan --mode hosted pins left-pad (fresh immutable install passes).
    2. vendor (or scan --mode vendored) takes over. The run succeeds and the vendored state installs fine.
    3. vendor --revert → yarn.lock now has resolution: "left-pad@npm:1.3.0". The ::__archiveUrl= binding is gone, and package.json is byte-exact.
    4. On a fresh clone with an empty YARN_GLOBAL_FOLDER, yarn install --immutable fails with YN0035: left-pad@npm:1.3.0: Package not found (404).

    Cause: the takeover reverts the hosted pin through restore_berry (upstream/npm.rs:559) before vendoring. That revert writes the bare locator, and the vendored ledger then records it as the pre-vendor entry: .socket/vendor/state.json stores "left-pad@npm:1.3.0" as the original. So any later vendor --revert reproduces #817, even though a plain vendored revert keeps the binding (run 18: byte-exact).

    Matrix (Linux, main 045d7ec, each cell run twice): yarn 4.18.1 via vendor fails, 4.18.1 via scan --mode vendored fails, 4.0.2 via vendor fails. Fixing restore_berry to keep the original binding (release 4.0.0 kept it in redirect-state.json) should cover this path too.


    Generated by Claude Code

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions