diff --git a/CHANGELOG.md b/CHANGELOG.md index d2370a539..4c9895e03 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -163,6 +163,15 @@ limits, and required install commands. fetches honor `GOPROXY` and private-module settings. - Yarn Berry preserves supported line endings and checksum spellings. Mode preflights, including Bun's, run before discarding existing protection. +- Yarn Berry hosted references no longer send npm registry credentials to the + patch server. The old `npm:` locator made yarn attach `npmAuthToken` / + `YARN_NPM_AUTH_TOKEN` to scoped packages (and to every package under + `npmAlwaysAuth`). Hosted mode now pins the way yarn does for a root + `resolutions` entry: `package.json` routes the locked descriptor to the + hosted tarball and the lock entry is keyed by it, which also passes yarn's + hardened mode (on by default for public pull request CI). A user-authored + `resolutions` entry for the package is never overwritten. Locks pinned by + earlier releases are re-pinned on the next hosted `scan`. - Composer hosted references remove upstream source fallbacks and mirrors; RubyGems hosted locks preserve source order; NuGet edits use the active config and survive `` entries. diff --git a/crates/socket-patch-cli/CLI_CONTRACT.md b/crates/socket-patch-cli/CLI_CONTRACT.md index 4d8c3b65f..dc04373fa 100644 --- a/crates/socket-patch-cli/CLI_CONTRACT.md +++ b/crates/socket-patch-cli/CLI_CONTRACT.md @@ -163,7 +163,7 @@ For a **9.0 root lock**, the CLI ensures `pnpm-workspace.yaml` carries `trustLoc `scan --mode hosted` swaps the in-place apply for the registry-redirect pipeline: discover → resolve hosted-patch references (grant token + integrity + per-dep registry override) → rewrite ONLY the patched dependencies' lockfile / registry-config entries to point at the hosted packages. A dep counts as **redirected** only when its hosted-artifact URL (or per-dep registry index URL) actually landed in a project file — a granted reference whose rewriter found nothing to edit is neither counted nor attested. **No ledger (v5.0)**: hosted mode writes ONLY the lockfile / registry-config edits — `.socket/vendor/redirect-state.json` is never written (on success or failure), and a pre-v5 one on disk is ignored (never read for planning, never quarantined, left byte-identical). The lockfiles are the only record of a hosted patch: `list`, `vex`, `rollback`, `remove`, `vendor` and `repair` all discover the hosted pins from them (a hosted URL counts only on `https://patch.socket.dev` or the `--patch-server-url` / `SOCKET_PATCH_SERVER_URL` origin), and commit-ready output is just the lockfile / config changes. Cargo and golang are confirmed only by their rewriter's own report (`confirmed_cargo_uuids` / `confirmed_golang_uuids`): a golang dep counts only when its go.mod `replace M V => patch.socket.dev/gopatch/ ` and both go.sum lines are in place, never because the patch-server origin or leftover go.sum lines appear somewhere. A golang module that go.mod does not require and go.sum does not list at the patched version is outside the build graph and is refused with `redirect_golang_not_in_module_graph` (nothing written). Only the exact module `patch.socket.dev/gopatch/` is socket-owned; any other module path is refused with `redirect_golang_untrusted_module_path`. A vendored golang module is taken over like cargo and the npm family: its vendor wiring, committed copy and ledger entry are reverted first (`redirect_takeover_reverted_vendored`). Re-runs over already-rewritten output plan from the current lock text and are idempotent (exit 0, lock unchanged). **Lock (v5.0)**: the hosted engine acquires `<.socket>/apply.lock` around its first wet write (the takeover pre-reverts) — not on `--dry-run`, and not when the run would write nothing (zero redirects, all skipped) — so previews and no-op runs never create `.socket/`; contention is `lock_held` and a lock-file I/O fault (a read-only project root, a file squatting on `.socket/`) is `lock_io` — both exit 1, refused BEFORE any project file is written, and rendered like every other lock holder: human `Error (): ` on stderr (+ the `--lock-timeout` hint for a live holder); JSON keeps the hosted shape — top-level `status: "error"`, `errorCode: "lock_held" | "lock_io"`, a string `error`, and `redirect: {mode: "hosted"}` retained (NOT the vendored `error: {code, message}` object). **Takeover symlink pre-check (v5.0)**: a vendored→hosted takeover whose recorded wiring file is a symlink is refused up front with `redirect_symlinked_file_unsupported` — wet and `--dry-run` alike, before any revert — so "nothing was written" holds. **Human mode (v5.0)**: hosted `scan` prints the results table and update detection like the other modes, then rewrites without a prompt (scan never prompts); `--dry-run` previews through the engine, and a detail fetch that leaves nothing to redirect enters the engine as a no-op (`Redirected 0 packages; rewrote 0 files.`, no lock, no `.socket/`). The detail fetch prints the same progress counter and per-package `Warning: could not fetch details for …` lines as the agent arm. An EMPTY hosted discovery prints `No patches available for installed packages.` and exits 0 without entering the engine; a discovery whose every offer is paid-tier for an org without paid access prints the table's paid nudge, then `No downloadable patches (paid subscription required).`, and exits 0 without entering the engine (parity with the agent/vendored arms). JSON output gains a `redirect` sub-object: `{ mode: "hosted", redirected, rewrittenFiles, skipped, warnings, dryRun }` (`mode` is additive so consumers can dispatch without inferring it). Rewriter warnings carry stable `redirect_*` codes (e.g. `redirect_npm_no_lockfile`, `redirect_gradle_manual_snippet`, `redirect_golang_unsupported`); new codes are additive (MINOR). v5.0 additive codes: `redirect_composer_no_lockfile` / `redirect_gem_no_gemfile` (composer / gem: neither manifest nor lock present — once per run, after the intake gates), `redirect_gem_bundle_gemfile_unsupported` (gem: `BUNDLE_GEMFILE` — `BUNDLE_GEMFILE:` in the bundler app config, which outranks the environment variable as in `Bundler::Settings`, else the environment variable — names a manifest other than the project's `Gemfile` / `gems.rb`, so no gem is redirected or attested; a value naming one of those two selects that pair even when the other spelling is present), `redirect_maven_no_pom` (no `pom.xml` and no Gradle build), `redirect_nuget_lock_unparseable` (a present-but-corrupt `packages.lock.json` — warned once, nothing mutated; an absent lock still proceeds), `redirect_cargo_lock_pkg_ambiguous` (several same-name+version `[[package]]` blocks and none carries the index `source` — transactional skip). Also v5.0: a registry override of the wrong kind (or none at all) warns the arm's missing-override code for nuget/gem/golang. Refusals stay fail-closed with a diagnosis that names the actual cause: a yarn-berry lock entry resolving through a non-`npm:` protocol keeps `redirect_yarn_berry_unsupported_protocol` with the entry's ACTUAL protocol in the detail — except socket-patch's OWN vendored wiring (a `file:` range into `.socket/vendor/`), which gets the distinct `redirect_yarn_berry_vendored_entry` code whose detail names the retirement path (`remove ` per package, or `vendor --revert` which unwinds every vendored package, then re-run `scan --mode hosted`). Both leave the entry byte-identical; neither changes exit code or status. **yarn berry line endings (v5.0)**: yarn writes a NEW `yarn.lock` with the OS line ending (`os.EOL` — CRLF on Windows) and keeps an existing lock's majority ending on every later write, and a `core.autocrlf` checkout turns an LF lock CRLF on any OS — so a uniformly CRLF lock is rewritten in its own ending: every untouched byte (a leading BOM included) round-trips (and `rollback`'s upstream restore keeps the lock's own ending). A lock that MIXES CRLF and LF (or holds a bare CR) has no single ending to keep — yarn's own `--immutable` check rejects it too (YN0028) — so it is refused untouched with `redirect_yarn_berry_mixed_line_endings` (the detail names `yarn install`, which normalizes it). This replaces v4's `redirect_yarn_berry_crlf_unsupported`, which refused every CRLF lock and is no longer emitted. A vendored→hosted takeover runs these berry gates (mixed line endings, unsupported `cacheKey`, a non-zero `.yarnrc.yml` `compressionLevel`) BEFORE reverting a vendored berry purl — wet and `--dry-run` alike — so a refused purl keeps its vendored wiring, ledger entry and artifact byte-identical and is skipped with the gate's code (never announced as `redirect_takeover_reverted_vendored` and then left unpatched in both modes). -The rewriter reads a fixed set of candidate files from the project root: the npm-family locks (`package-lock.json`, `npm-shrinkwrap.json`, `pnpm-lock.yaml`, `shrinkwrap.yaml`, `yarn.lock`, plus `.yarnrc.yml` for the berry cache-config gate, `bun.lock` / `bun.lockb`, and `vlt-lock.json` with `vlt.json` and `node_modules/.vlt-lock.json` read only), `requirements.txt` / `uv.lock` / `Pipfile.lock` (pipfile-spec 6; see the Pipenv section below) / `poetry.lock` (every Poetry lock generation from 1.0 on — the 0.12 `[metadata.hashes]` layout is refused because that installer ignores URL sources; a Poetry < 1.4 writer additionally gets `redirect_poetry_stale_install_risk`, see `docs/testing/poetry-compatibility.md`) / `pdm.lock` (PDM lock formats `2` and `4.3`–`4.5.1`; the identity-losing `3.1` / `4.0`–`4.2` formats and unknown future formats are refused with `redirect_pdm_refused`, and a lock-format-`2` writer additionally gets `redirect_pdm_legacy_sync_required`, see `docs/testing/pdm-compatibility.md`; when `uv.lock` or `poetry.lock` sits beside it they drive and `pdm.lock` is left alone), `Cargo.toml` / `Cargo.lock` / `.cargo/config.toml` (plus the legacy extensionless `.cargo/config` — cargo reads that spelling in preference when both exist, so the managed `[registries.…]` block is written into whichever one is present; **cargo also reads every workspace-member manifest** — the `[workspace] members` globs minus `exclude` — and every in-root path-dependency manifest, recursively, reached without crossing a symbolic link and never under `.socket/`, and pins the crate in each one that declares it, so those `/Cargo.toml` files can appear in `rewrittenFiles`. A crate is redirected only when every declaration pins and every other `Cargo.lock` package depending on it is a planned member: one a registry or git crate — or a path package outside the root or behind a link — also depends on is refused `redirect_cargo_transitive_dependents` (a pin reaches only the declarations it sits on), a crate no manifest declares keeps `redirect_cargo_toml_dep_not_found` with a transitive-only detail naming `--mode vendored`, a crate every declaration of which requires another version (no requirement accepts the patched version) is refused `redirect_cargo_toml_dep_unrewritable`, and so is a requirement that also matches another locked version of the crate — each a transactional skip, never recorded or attested. With NO `Cargo.lock` there is no resolved graph to ask, so the dependents question is answered from the manifests instead: a crate declared beside any other dependency — anything but a path dependency on a manifest this run also pins, or a `workspace = true` inheritor of a table it scans — or beside a workspace member this run did not read (a `members` glob, or a member outside the project or behind a symbolic link, which member discovery drops) is refused `redirect_cargo_lockless_dependents`, whose detail names the remedies (commit a lockfile, or `--mode vendored`); a project whose only dependency is the patched crate has nothing that could pull it in and still redirects. All-CRLF manifests, locks and configs are rewritten with CRLF kept (mixed endings keep refusing where the grammar does not match), and `remove` / rollback match the recorded fragments across a later CRLF↔LF checkout conversion), `composer.lock`, `nuget.config` / `packages.lock.json`, `Gemfile` / `Gemfile.lock`, `pom.xml` (+ `.mvn/maven.config` / `.mvn/checksums/checksums.sha256` for maven Trusted Checksums merge, and the Gradle build scripts read only to trigger the manual-snippet warning). **npm-family flavor coverage**: package-lock / npm-shrinkwrap, pnpm (root OR any nested `*/pnpm-lock.yaml`), yarn classic, **yarn berry** (`yarn.lock` entry only — `resolution: ::__archiveUrl=` + `yarnBerry10c0` checksum; cacheKey `10c0` and `.yarnrc.yml compressionLevel 0` gated by `redirect_yarn_berry_cache_unsupported`), and **bun** (text `bun.lock` lockfileVersion 0, 1 or 2 — 0 is the `--save-text-lockfile` opt-in lock of Bun 1.1.39–1.1.45, 1 the 1.2–1.3 default, 2 the 1.4+ default; all three emit one `packages` grammar, so the registry 4-tuple → URL 3-tuple rewrite is version-independent and the lock's own version line is kept. Any other or missing version, or a `packages` section outside bun's single-line grammar, is refused `redirect_bun_lock_unsupported` — the detail is the shared version gate's text (a newer version: update socket-patch, re-locking would reproduce it; no integer: re-lock with Bun ≥ 1.2), identical to the vendored refusal. A version-0 lock holding `workspace:` packages is refused `redirect_bun_workspace_unsupported` (its 2-tuple workspace grammar cannot keep the hosted tuple through a frozen install); the remedy is to delete `bun.lock` and re-run `bun install` with Bun ≥ 1.2, which writes lockfileVersion 1 (accepted). A plain in-place `bun install` bumps the version only when a workspace depends on another workspace (e.g. root → member — the shape the matrix measured); otherwise Bun 1.2.0 keeps version 0 and Bun 1.2.23+ fail to resolve, so the in-place bump is not the documented remedy. Bun lock version, grammar and workspace compatibility are checked before a vendored takeover, including during dry-run: these refusals preserve the existing lock, artifact and vendor ledger. Version-1 and version-2 workspace locks are rewritten, nested versions included. A granted dep with no rewritable entry warns `redirect_bun_entry_not_found`, a grant without a sha512 `redirect_bun_missing_sha512`; a CRLF lock keeps `\r\n` on the rewritten line, and a hosted URL left by an earlier grant of the same `name@version` is re-pinned in place. **Digest-less re-saves (Bun 1.1.39–1.3.9)**: every text-lock Bun below 1.3.10 re-saves a URL tuple WITHOUT its `sha512` whenever the lock is re-saved for another reason (`bun add`, `bun install` after a package.json or workspace change), leaving the 2-tuple `["name@", {meta}]` — the spec Bun installs from is intact. The CLI treats that spelling as its own wiring: a repeat hosted run counts the dep as redirected (no `redirect_bun_entry_not_found`) and HEALS the line back to the 3-tuple with the current `sha512`, recording the heal as a further `redirect_bun_lock_package` edit whose `original` is the 2-tuple (a stale URL is re-pinned from either spelling); `rollback`, scoped `rollback ` / `remove ` and the vendored takeover accept the digest-less spelling of a recorded `new` line (same key, spec and meta, only the trailing `"sha512-…"` missing) and restore the recorded original over it, so the chain always unwinds to the pristine registry line. Anything else — another uuid/token, another version, a re-laid meta object — is still drift. **Native `bun.lockb`**: when no text `bun.lock` exists, binary format versions 1, 2 and 3 are read and rewritten directly. Socket Patch does not invoke Bun or convert the project to a text lockfile. Exact matching package records are rewritten to hosted tarballs with the granted integrity, preserving dependency resolution IDs, workspace/dependency topology and unrelated package metadata; binary pointers and the package metadata hash are updated. Per-package `redirect_bun_lockb_package` snapshots support scoped rollback, repeat runs, superseding grants and hosted ↔ vendored takeover. A regular binary lock is discoverable even with no Bun runtime or `node_modules`; a dry run previews the same binary edits without writing them. A malformed, unreadable, unsupported or unverified binary structure is `redirect_bun_lockb_invalid` (exit 0, `redirected: 0`), and it refuses the npm rewrite before any takeover or sibling npm-family lock mutation. A symlinked binary write target is `redirect_symlinked_file_unsupported` (exit 1, including dry-run). `bun.lock` wins when both spellings exist. Binary-only projects do not receive `redirect_npm_no_lockfile`. Measured boundaries and the real-Bun matrix: `docs/testing/bun-compatibility.md`), and **vlt** (`vlt-lock.json` without `lockfileVersion`, `0` or `1`; see the vlt hosted-mode contract below). **Rush monorepos**: when `rush.json` is present the rewriter also reads `common/config/rush/pnpm-lock.yaml` and each `common/config/subspaces//pnpm-lock.yaml` (sorted for determinism) under their repo-relative keys and repoints them in place; editing them emits `redirect_rush_repo_state_stale` when `common/config/rush/repo-state.json` exists (the `pnpmShrinkwrapHash` desync is refreshed by `rush update`, which the redirect survives). **maven** is fail-closed via version suffixing: a `mavenSuffixedVersion` + `mavenPomSha256` override pins the Socket-only `-socket.` by rewriting the literal `` (`redirect_maven_dep_version`) or adding a `` entry (`redirect_maven_dep_management_added`), plus optional Trusted Checksums (`redirect_maven_trusted_checksums`, conflicts as `redirect_maven_trusted_checksums_conflict`; when `.mvn/wrapper/maven-wrapper.properties` pins a Maven older than 3.9.4, which ignores those files, the additive warning `redirect_maven_trusted_checksums_unenforced`); a `${property}` version is refused (`redirect_maven_dep_unpinned`), a non-matching literal skipped (`redirect_maven_dep_version_mismatch`), and an override without a suffixed version falls back to same-GAV repository injection (`redirect_maven_same_gav_fallback`, NOT fail-closed). +The rewriter reads a fixed set of candidate files from the project root: the npm-family locks (`package-lock.json`, `npm-shrinkwrap.json`, `pnpm-lock.yaml`, `shrinkwrap.yaml`, `yarn.lock`, plus `.yarnrc.yml` for the berry cache-config gate, `bun.lock` / `bun.lockb`, and `vlt-lock.json` with `vlt.json` and `node_modules/.vlt-lock.json` read only), `requirements.txt` / `uv.lock` / `Pipfile.lock` (pipfile-spec 6; see the Pipenv section below) / `poetry.lock` (every Poetry lock generation from 1.0 on — the 0.12 `[metadata.hashes]` layout is refused because that installer ignores URL sources; a Poetry < 1.4 writer additionally gets `redirect_poetry_stale_install_risk`, see `docs/testing/poetry-compatibility.md`) / `pdm.lock` (PDM lock formats `2` and `4.3`–`4.5.1`; the identity-losing `3.1` / `4.0`–`4.2` formats and unknown future formats are refused with `redirect_pdm_refused`, and a lock-format-`2` writer additionally gets `redirect_pdm_legacy_sync_required`, see `docs/testing/pdm-compatibility.md`; when `uv.lock` or `poetry.lock` sits beside it they drive and `pdm.lock` is left alone), `Cargo.toml` / `Cargo.lock` / `.cargo/config.toml` (plus the legacy extensionless `.cargo/config` — cargo reads that spelling in preference when both exist, so the managed `[registries.…]` block is written into whichever one is present; **cargo also reads every workspace-member manifest** — the `[workspace] members` globs minus `exclude` — and every in-root path-dependency manifest, recursively, reached without crossing a symbolic link and never under `.socket/`, and pins the crate in each one that declares it, so those `/Cargo.toml` files can appear in `rewrittenFiles`. A crate is redirected only when every declaration pins and every other `Cargo.lock` package depending on it is a planned member: one a registry or git crate — or a path package outside the root or behind a link — also depends on is refused `redirect_cargo_transitive_dependents` (a pin reaches only the declarations it sits on), a crate no manifest declares keeps `redirect_cargo_toml_dep_not_found` with a transitive-only detail naming `--mode vendored`, a crate every declaration of which requires another version (no requirement accepts the patched version) is refused `redirect_cargo_toml_dep_unrewritable`, and so is a requirement that also matches another locked version of the crate — each a transactional skip, never recorded or attested. With NO `Cargo.lock` there is no resolved graph to ask, so the dependents question is answered from the manifests instead: a crate declared beside any other dependency — anything but a path dependency on a manifest this run also pins, or a `workspace = true` inheritor of a table it scans — or beside a workspace member this run did not read (a `members` glob, or a member outside the project or behind a symbolic link, which member discovery drops) is refused `redirect_cargo_lockless_dependents`, whose detail names the remedies (commit a lockfile, or `--mode vendored`); a project whose only dependency is the patched crate has nothing that could pull it in and still redirects. All-CRLF manifests, locks and configs are rewritten with CRLF kept (mixed endings keep refusing where the grammar does not match), and `remove` / rollback match the recorded fragments across a later CRLF↔LF checkout conversion), `composer.lock`, `nuget.config` / `packages.lock.json`, `Gemfile` / `Gemfile.lock`, `pom.xml` (+ `.mvn/maven.config` / `.mvn/checksums/checksums.sha256` for maven Trusted Checksums merge, and the Gradle build scripts read only to trigger the manual-snippet warning). **npm-family flavor coverage**: package-lock / npm-shrinkwrap, pnpm (root OR any nested `*/pnpm-lock.yaml`), yarn classic, **yarn berry** (the pin yarn writes for a root `resolutions` entry: the root `package.json` — edited only beside a berry `yarn.lock` — gains one `"@npm:": ""` selector per locked range (`redirect_yarn_berry_resolution` edits), and only that `yarn.lock` entry is re-keyed `"@"` with the same `resolution:` + `yarnBerry10c0` checksum (`redirect_yarn_berry_entry`), moved to yarn's key order; never an `npm:` locator, whose fetcher sends npm registry auth to the patch host, nor a tarball locator under an `npm:` key, which hardened mode rejects (YN0078). An older release's `npm:::__archiveUrl=` pin is still recognized and is re-pinned on the next run; rollback rebuilds the key from the selectors and drops them. Refused, nothing written: a user-authored `resolutions` entry for the package `redirect_yarn_berry_resolutions_conflict`, no root manifest `redirect_yarn_berry_manifest_missing`, a builtin `patch:` entry wrapping the same descriptor `redirect_yarn_berry_shared_descriptor`, an artifact URL yarn cannot fetch as a tarball `redirect_yarn_berry_artifact_url_unsupported`; cacheKey `10c0` and `.yarnrc.yml compressionLevel 0` gated by `redirect_yarn_berry_cache_unsupported`), and **bun** (text `bun.lock` lockfileVersion 0, 1 or 2 — 0 is the `--save-text-lockfile` opt-in lock of Bun 1.1.39–1.1.45, 1 the 1.2–1.3 default, 2 the 1.4+ default; all three emit one `packages` grammar, so the registry 4-tuple → URL 3-tuple rewrite is version-independent and the lock's own version line is kept. Any other or missing version, or a `packages` section outside bun's single-line grammar, is refused `redirect_bun_lock_unsupported` — the detail is the shared version gate's text (a newer version: update socket-patch, re-locking would reproduce it; no integer: re-lock with Bun ≥ 1.2), identical to the vendored refusal. A version-0 lock holding `workspace:` packages is refused `redirect_bun_workspace_unsupported` (its 2-tuple workspace grammar cannot keep the hosted tuple through a frozen install); the remedy is to delete `bun.lock` and re-run `bun install` with Bun ≥ 1.2, which writes lockfileVersion 1 (accepted). A plain in-place `bun install` bumps the version only when a workspace depends on another workspace (e.g. root → member — the shape the matrix measured); otherwise Bun 1.2.0 keeps version 0 and Bun 1.2.23+ fail to resolve, so the in-place bump is not the documented remedy. Bun lock version, grammar and workspace compatibility are checked before a vendored takeover, including during dry-run: these refusals preserve the existing lock, artifact and vendor ledger. Version-1 and version-2 workspace locks are rewritten, nested versions included. A granted dep with no rewritable entry warns `redirect_bun_entry_not_found`, a grant without a sha512 `redirect_bun_missing_sha512`; a CRLF lock keeps `\r\n` on the rewritten line, and a hosted URL left by an earlier grant of the same `name@version` is re-pinned in place. **Digest-less re-saves (Bun 1.1.39–1.3.9)**: every text-lock Bun below 1.3.10 re-saves a URL tuple WITHOUT its `sha512` whenever the lock is re-saved for another reason (`bun add`, `bun install` after a package.json or workspace change), leaving the 2-tuple `["name@", {meta}]` — the spec Bun installs from is intact. The CLI treats that spelling as its own wiring: a repeat hosted run counts the dep as redirected (no `redirect_bun_entry_not_found`) and HEALS the line back to the 3-tuple with the current `sha512`, recording the heal as a further `redirect_bun_lock_package` edit whose `original` is the 2-tuple (a stale URL is re-pinned from either spelling); `rollback`, scoped `rollback ` / `remove ` and the vendored takeover accept the digest-less spelling of a recorded `new` line (same key, spec and meta, only the trailing `"sha512-…"` missing) and restore the recorded original over it, so the chain always unwinds to the pristine registry line. Anything else — another uuid/token, another version, a re-laid meta object — is still drift. **Native `bun.lockb`**: when no text `bun.lock` exists, binary format versions 1, 2 and 3 are read and rewritten directly. Socket Patch does not invoke Bun or convert the project to a text lockfile. Exact matching package records are rewritten to hosted tarballs with the granted integrity, preserving dependency resolution IDs, workspace/dependency topology and unrelated package metadata; binary pointers and the package metadata hash are updated. Per-package `redirect_bun_lockb_package` snapshots support scoped rollback, repeat runs, superseding grants and hosted ↔ vendored takeover. A regular binary lock is discoverable even with no Bun runtime or `node_modules`; a dry run previews the same binary edits without writing them. A malformed, unreadable, unsupported or unverified binary structure is `redirect_bun_lockb_invalid` (exit 0, `redirected: 0`), and it refuses the npm rewrite before any takeover or sibling npm-family lock mutation. A symlinked binary write target is `redirect_symlinked_file_unsupported` (exit 1, including dry-run). `bun.lock` wins when both spellings exist. Binary-only projects do not receive `redirect_npm_no_lockfile`. Measured boundaries and the real-Bun matrix: `docs/testing/bun-compatibility.md`), and **vlt** (`vlt-lock.json` without `lockfileVersion`, `0` or `1`; see the vlt hosted-mode contract below). **Rush monorepos**: when `rush.json` is present the rewriter also reads `common/config/rush/pnpm-lock.yaml` and each `common/config/subspaces//pnpm-lock.yaml` (sorted for determinism) under their repo-relative keys and repoints them in place; editing them emits `redirect_rush_repo_state_stale` when `common/config/rush/repo-state.json` exists (the `pnpmShrinkwrapHash` desync is refreshed by `rush update`, which the redirect survives). **maven** is fail-closed via version suffixing: a `mavenSuffixedVersion` + `mavenPomSha256` override pins the Socket-only `-socket.` by rewriting the literal `` (`redirect_maven_dep_version`) or adding a `` entry (`redirect_maven_dep_management_added`), plus optional Trusted Checksums (`redirect_maven_trusted_checksums`, conflicts as `redirect_maven_trusted_checksums_conflict`; when `.mvn/wrapper/maven-wrapper.properties` pins a Maven older than 3.9.4, which ignores those files, the additive warning `redirect_maven_trusted_checksums_unenforced`); a `${property}` version is refused (`redirect_maven_dep_unpinned`), a non-matching literal skipped (`redirect_maven_dep_version_mismatch`), and an override without a suffixed version falls back to same-GAV repository injection (`redirect_maven_same_gav_fallback`, NOT fail-closed). **Gem stale-install guard (additive warning — the canonical narrative; other mentions point here)**: the gem hosted rewrite is pure Gemfile/lock text, so a gem ALREADY materialized under the project's bundle paths keeps its upstream bytes — the next `bundle install` prints `Using ` and never refetches, on **every** bundler major (live-verified 2026-08-19 on 1.17.3 / 2.7.2 / 4.0.18: bundler 4's CHECKSUMS verify at download time only, and nothing is downloaded; `bundle install --force`/`--redownload` re-install from the stale cached `.gem` instead of re-fetching — bundler 1 silently, bundler 4 with an exit-37 checksum refusal that still leaves the upstream bytes installed; the **verified** remedy is removing the installed dir + cache `.gem` + `specifications` entry, then `bundle install`). After the rewrite, a hosted run therefore probes the installed-gem discovery paths (the same ruby-crawler discovery `apply` uses, honoring `--global`/`--global-prefix` like scan's own discovery) for each confirmed gem redirect and judges the materialization against the patch record's `afterHash` file map. Judgment rules: records are found **by uuid** among this run's fetched records (v5.0: hosted mode persists no records, so a purl whose `/patches/view` fetch failed this run is not judged; the warning re-fires on every re-scan whose fetch succeeds, until the stale materialization is gone); a materialization with every file at `afterHash` is already patched and never warns (an agent→hosted migration stays quiet by construction), and when several confirmed variant purls resolve to one installed dir, ANY of them judging it patched keeps it quiet; staleness needs **positive evidence** — at least one record file whose bytes were actually read and hash to neither state's expectation — so missing or unreadable files never produce a warning. Warnings emit `redirect_gem_stale_install` (JSON `redirect.warnings[]` + a code-tagged stderr line) in three flavors: a PROJECT-LOCAL dir gets the verified delete-list remedy (installed dir, cache `.gem`, `specifications` entry — plus the project's committed `/.gem` when present and not proven to be the patched artifact, since bundler installs from its cache dir in preference to fetching); a SHARED gem-env home gets a caveat that the home is shared machine-wide and prefers migrating the project to a local bundle path over deleting shared files; and a committed cache-dir archive whose sha256 differs from the patched artifact's warns standalone even with no installed dir at all (a fresh checkout with a committed stale cache re-materializes the upstream bytes forever). A stale-flagged purl is additionally **excluded from the same run's `--vex` `assume_applied` set** — the envelope must never attest a CVE its own warning says is live; the purl falls back to normal installed-tree verification (a patched install still attests, a stale one is omitted). The cache dir is bundler's `cache_path` setting (`Bundler.app_cache`), resolved in `Bundler::Settings` priority: `BUNDLE_CACHE_PATH:` in the bundler app config (`$BUNDLE_APP_CONFIG/config`, else `.bundle/config`) first, then the `BUNDLE_CACHE_PATH` environment variable, else `vendor/cache`; a relative value is read against the project root. With `BUNDLE_IGNORE_CONFIG` set (any value) bundler reads no config file, so the app config is skipped here too and only the environment and the default count — the same holds for the `BUNDLE_GEMFILE:` app-config setting. The probe is read-only (nothing is deleted) and skipped on `--dry-run` — deliberately explicit, since nothing was rewritten. Exit code and `status` are unchanged (warning-only, the hosted-refusal posture); a same-run `--vex` may still fail on "nothing to attest" per the embedded-VEX contract. @@ -360,7 +360,7 @@ Discovery is read-only, never touches the network, and never fails the run: a ma |---|---|---|---|---| | npm | `package-lock.json` and `npm-shrinkwrap.json` (both when both exist) | `resolved` on the patch host (`packages` in v2/v3; `dependencies` only in v1; `link` / `inBundle` / `bundled` entries skipped, and so is any entry npm installs from a git, URL or `file:` spec, together with every ref for the same `name@version`) | `resolved: file:.socket/vendor/npm//-.tgz` | `integrity`, required | | pnpm | `pnpm-lock.yaml` (every `lockfileVersion`); `shrinkwrap.yaml` only when there is no `pnpm-lock.yaml`; with `rush.json`, `common/config/rush/pnpm-lock.yaml` + `common/config/subspaces/*/pnpm-lock.yaml` | `packages:` `resolution.tarball` on the patch host | `file:.socket/vendor/npm/…` tarball + key | `integrity`, required | -| yarn | `yarn.lock` (classic and berry) | classic `resolved`; berry `resolution: …::__archiveUrl=` | classic `resolved "file:./.socket/vendor/npm/…#"`; berry `file:` entry **plus** a root `package.json` `resolutions` mapping onto the same artifact (without it the entry is orphaned: diagnosed, no ref) | classic `integrity` / `#sha1`, berry `checksum`, required | +| yarn | `yarn.lock` (classic and berry) | classic `resolved`; berry entry keyed and resolved `@` + root `package.json` `resolutions` `"@npm:": ""` (older releases: `resolution: …::__archiveUrl=`) | classic `resolved "file:./.socket/vendor/npm/…#"`; berry `file:` entry **plus** a root `package.json` `resolutions` mapping onto the same artifact (without it the entry is orphaned: diagnosed, no ref) | classic `integrity` / `#sha1`, berry `checksum`, required | | bun | `bun.lock`; `bun.lockb` only when there is no `bun.lock` (bun reads exactly one) | URL tuple / binary remote-tarball resolution; version from the URL leaf | `.socket/vendor/npm//-.tgz` tuple / local-tarball resolution | `sha512-…`, required. A 2-tuple that Bun < 1.3.10 re-saved without its digest is still a reference, but it attests only from an installed tree. | | vlt | `vlt-lock.json` (lockfileVersion absent, `0` or `1`; a BOM-prefixed, unparseable, non-object or other-version lock is not read: diagnosed, no ref). `vlt.json` (read only for its `modifiers`) and `node_modules/.vlt-lock.json` are never wiring. | a registry node (any segment) whose slot [3] is a `/patch/npm/…` URL on the patch host with the leaf `-.tgz` of its DepID's `name@version`, whose embedded `/` path (when present) is that `name@version`, and slot [1] == name; version from the DepID | a `file` node `.socket/vendor/npm//-/node_modules/` (or a user-installed `-.tgz`) with slot [1] == name; version from the path. A same-`name@version` registry node, or a diagnosed Socket-shaped one, beside it is diagnosed, no ref. | slot [2] `sha512-…`, required (a hosted node without one is no reference). A same-`name@version` node on another registry, or a diagnosed Socket-shaped one, keeps the reference but withholds the lockfile basis: only an installed tree whose every store copy verifies attests. So does a lock some vlt release discards, the conditions of `redirect_vlt_lockfile_version_missing`, `redirect_vlt_old_lockfile_ignored` and `redirect_vlt_scalar_registry_ignored` (which in-run `--vex` withholds too): every hosted reference in it keeps no pin, and one `patched_ref_unattributable` names them. | | cargo | `Cargo.lock`, `Cargo.toml`, `.cargo/config` (else `.cargo/config.toml`) | `Cargo.lock` `source = "sparse+…//index/"`, confirmed by `Cargo.toml`: a crate the root manifest declares must pin `registry = "socket-patch-"`. A reverted pin is diagnosed, no ref. | `[patch.] = { path = ".socket/vendor/cargo//-" }` — primarily the root `Cargo.toml` (v5 `vendor`; key-agnostic: `` is `package` when renamed, else the key, so `-socket-` keys count), also the project config (pre-v5 wiring), live only while the lock holds a sourceless entry for it that is not in `[[patch.unused]]`; a manifest entry cargo ignores — the project config redefines its key with another path, or a `[patch."https://github.com/rust-lang/crates.io-index"]` table replaces `[patch.crates-io]` — is diagnosed (`patched_ref_invalid`), no ref | `checksum` (v1: `[metadata]`), required | diff --git a/crates/socket-patch-cli/src/commands/scan/mod.rs b/crates/socket-patch-cli/src/commands/scan/mod.rs index d2621b28f..8ce80d9f1 100644 --- a/crates/socket-patch-cli/src/commands/scan/mod.rs +++ b/crates/socket-patch-cli/src/commands/scan/mod.rs @@ -4343,6 +4343,32 @@ mod tests { assert!(takeover.vendored.is_empty(), "{takeover:?}"); } + #[tokio::test] + async fn hosted_direction_provable_for_berry_tarball_locator() { + // Today's berry pin is the plain tarball-URL locator (#404). + let tmp = tempfile::tempdir().unwrap(); + let root = tmp.path(); + write_vendor_ledger_wired(root, &["pkg:npm/minimist@1.2.2"]).await; + tokio::fs::write( + root.join("yarn.lock"), + format!( + "__metadata:\n version: 8\n cacheKey: 10c0\n\n\ + \"minimist@npm:1.2.2\":\n version: 1.2.2\n \ + resolution: \"minimist@https://patch.socket.dev/patch/npm/{TAKEOVER_TOKEN}/{TAKEOVER_UUID}/minimist-1.2.2.tgz\"\n" + ), + ) + .await + .unwrap(); + + let takeover = classify_overlap_takeover(&common_at(root), root).await; + assert_eq!( + takeover.redirect, + vec!["pkg:npm/minimist@1.2.2".to_string()], + "a berry tarball locator must prove hosted is live" + ); + assert!(takeover.vendored.is_empty(), "{takeover:?}"); + } + #[tokio::test] async fn vendored_path_uuid_is_not_a_hosted_pin() { // The vendored wiring embeds the SAME patch uuid in its diff --git a/crates/socket-patch-cli/tests/e2e_hosted_production.rs b/crates/socket-patch-cli/tests/e2e_hosted_production.rs index c18c0312e..69098cbb6 100644 --- a/crates/socket-patch-cli/tests/e2e_hosted_production.rs +++ b/crates/socket-patch-cli/tests/e2e_hosted_production.rs @@ -1467,16 +1467,19 @@ fn yarn_berry_hosted_install_proof() { } assert_pristine(&minimist_entry(&fx.proj), PATCH_MARKER, LEG); let registry_lock = std::fs::read(fx.proj.join("yarn.lock")).expect("registry yarn.lock"); + // The hosted pin also routes through package.json `resolutions` (#404), + // so a revert to the registry restores both files. + let registry_pkg = std::fs::read(fx.proj.join("package.json")).expect("registry package.json"); let env_json = scan_hosted(&fx.proj, &[]); assert_redirected(&env_json, "yarn.lock"); let lock = read(&fx.proj.join("yarn.lock")); - // Berry pins the hosted artifact through a percent-encoded `__archiveUrl` - // resolution field, so the plain host string is encoded — check both the - // encoded host and the (unencoded) patch UUID. + // Berry pins the hosted artifact as a plain tarball-URL locator — never + // an `npm:` one (`::__archiveUrl=`), whose fetcher would send the npm + // registry token to the patch host (#404). assert!( - lock.contains("__archiveUrl") && lock.contains("patch.socket.dev"), - "{LEG}: berry lock carries no __archiveUrl pointing at the patch \ + lock.contains("@https://patch.socket.dev/") && !lock.contains("__archiveUrl"), + "{LEG}: berry lock carries no tarball locator pointing at the patch \ host:\n{lock}" ); assert!( @@ -1497,7 +1500,7 @@ fn yarn_berry_hosted_install_proof() { ); assert_patched(&minimist_entry(&fx.proj), PATCH_MARKER, LEG); - yarn_berry_hosted_manifestless_vex(&fx, ®istry_lock, &env); + yarn_berry_hosted_manifestless_vex(&fx, ®istry_lock, ®istry_pkg, &env); } /// One `vex --json --output /berry.vex.json` run in `proj` (production @@ -1582,7 +1585,12 @@ fn berry_skip_code(env: &serde_json::Value) -> String { /// `--offline` (`record_unavailable`), and not once the lock is reverted to /// the registry and reinstalled (nothing names the patch, even with /// `--no-verify`). -fn yarn_berry_hosted_manifestless_vex(fx: &NpmFixture, registry_lock: &[u8], env: &[(&str, &str)]) { +fn yarn_berry_hosted_manifestless_vex( + fx: &NpmFixture, + registry_lock: &[u8], + registry_pkg: &[u8], + env: &[(&str, &str)], +) { const LEG: &str = "yarn_berry_hosted_install_proof (manifest-less vex)"; let proj = &fx.proj; assert!( @@ -1600,8 +1608,10 @@ fn yarn_berry_hosted_manifestless_vex(fx: &NpmFixture, registry_lock: &[u8], env assert_eq!(berry_skip_code(&env_json), "record_unavailable", "{LEG}"); assert!(doc.is_none(), "{LEG}: no document"); - // Revert the lock to the registry and reinstall. + // Revert the lock and the package.json `resolutions` pin to the + // registry and reinstall. std::fs::write(proj.join("yarn.lock"), registry_lock).unwrap(); + std::fs::write(proj.join("package.json"), registry_pkg).unwrap(); std::fs::remove_dir_all(proj.join("node_modules")).ok(); let reinstall = tool(proj, "yarn", &["install", "--immutable"], env); assert!( diff --git a/crates/socket-patch-cli/tests/e2e_redirect_yarn_berry_build.rs b/crates/socket-patch-cli/tests/e2e_redirect_yarn_berry_build.rs index 0ffb717d4..e021d9015 100644 --- a/crates/socket-patch-cli/tests/e2e_redirect_yarn_berry_build.rs +++ b/crates/socket-patch-cli/tests/e2e_redirect_yarn_berry_build.rs @@ -3,8 +3,8 @@ //! `e2e_redirect_npm_build.rs`. //! //! `scan --mode hosted` never lands patched bytes in the repo: it rewrites -//! `yarn.lock` so the patched dependency resolves via -//! `npm:::__archiveUrl=` with `checksum: 10c0/` (yarn's +//! `yarn.lock` so the patched dependency resolves via the tarball-URL +//! locator `@` with `checksum: 10c0/` (yarn's //! cache-zip sha512); v5 keeps no redirect ledger — the lock pin is the //! whole hosted state. This test proves every link against the REAL //! `corepack yarn@4.12.0`: @@ -16,16 +16,19 @@ //! extract the EXACT `10c0/` checksum yarn computes for that //! tarball's cache zip — the value the redirect mock must hand back //! (yarn recomputes the same zip checksum whether the locator is `file:` -//! or `::__archiveUrl=`, so `--check-cache` will accept it). +//! or a tarball URL, so `--check-cache` will accept it). //! 3. `scan --mode hosted --json --vex` (the real binary): yarn.lock now -//! pins the hosted `__archiveUrl` + the `10c0` checksum, NO ledger is +//! pins the hosted tarball URL + the `10c0` checksum, NO ledger is //! written, the in-run VEX is the `(redirected)` attestation. //! 4. FRESH-CHECKOUT PROOF: only package.json + yarn.lock + .yarnrc.yml + //! .socket/ travel; `yarn install --immutable --check-cache` (offline //! from the registry, `unsafeHttpWhitelist` for the wiremock host) MUST -//! install the patched bytes from the hosted tarball. +//! install the patched bytes from the hosted tarball — with an npm +//! registry token configured (`YARN_NPM_AUTH_TOKEN` + `npmAlwaysAuth`) +//! that the patch host must never receive (#404: an `npm:` locator made +//! yarn's npm fetcher send it). //! -//! The negative twin serves a DIFFERENT tarball at the archiveUrl while the +//! The negative twin serves a DIFFERENT tarball at the hosted URL while the //! lock keeps the real `10c0` checksum: the fresh `--check-cache` install MUST //! fail with a YN0018 checksum error — the lock pin is enforcement. //! @@ -267,6 +270,9 @@ struct BerryRedirectFixture { host: String, /// `yarn.lock` as the real yarn wrote it, BEFORE the hosted rewrite. registry_lock: Vec, + /// The root `package.json` BEFORE the hosted rewrite (#404 option C + /// pins through its `resolutions`, so a revert restores both files). + registry_pkg: Vec, _server: MockServer, } @@ -286,7 +292,7 @@ enum HostedDriver { /// Steps 1–3: real install, patched tarball + bootstrap checksum + API mocks, /// the hosted rewrite (per `driver`: `scan --mode hosted --vex` or /// `get --mode hosted`), and the envelope/lockfile/ledger assertions. -/// `tamper_served_tarball` serves DIFFERENT bytes at the archiveUrl than the +/// `tamper_served_tarball` serves DIFFERENT bytes at the hosted URL than the /// checksum pins. `None` = skip (message printed). async fn berry_hosted_project( tag: &str, @@ -347,6 +353,7 @@ async fn berry_hosted_project( let installed_dir = proj.join("node_modules").join(DEP); let orig = std::fs::read(installed_dir.join("index.js")).expect("installed index.js"); let registry_lock = std::fs::read(proj.join("yarn.lock")).expect("registry yarn.lock"); + let registry_pkg = std::fs::read(proj.join("package.json")).expect("registry package.json"); assert!( !orig.starts_with(MARKER.as_bytes()), "pristine install must not carry the marker" @@ -549,12 +556,33 @@ async fn berry_hosted_project( ); } - // Lockfile pin: the encoded __archiveUrl + the 10c0 checksum. + // Lockfile pin: the tarball-URL locator + the 10c0 checksum. Never an + // `npm:` locator (`::__archiveUrl=`): yarn's npm fetcher sends registry + // auth to whatever host that locator names (#404). let lock = std::fs::read_to_string(proj.join("yarn.lock")).unwrap(); - let encoded = socket_patch_core::utils::uri::encode_uri_component(&hosted_url); assert!( - lock.contains("::__archiveUrl=") && lock.contains(&encoded), - "yarn.lock must carry the encoded __archiveUrl; got:\n{lock}" + lock.contains(&format!("\n resolution: \"{DEP}@{hosted_url}\"")), + "yarn.lock must pin the hosted tarball locator; got:\n{lock}" + ); + // #404 option C: the entry is keyed by the tarball descriptor, and the + // root package.json routes the locked descriptor there. + assert!( + lock.lines() + .any(|l| l.trim_end_matches('\r') == format!("\"{DEP}@{hosted_url}\":")), + "yarn.lock entry must be keyed by the tarball descriptor; got:\n{lock}" + ); + let root_pkg = std::fs::read_to_string(proj.join("package.json")).unwrap(); + let root_pkg: serde_json::Value = serde_json::from_str(&root_pkg).unwrap(); + assert!( + root_pkg["resolutions"] + .as_object() + .is_some_and(|r| r.iter().any(|(sel, v)| sel.starts_with(&format!("{DEP}@npm:")) + && v.as_str() == Some(hosted_url.as_str()))), + "package.json must route {DEP} to the hosted tarball: {root_pkg}" + ); + assert!( + !lock.contains("__archiveUrl"), + "the hosted pin must not be an npm: locator; got:\n{lock}" ); let checksum_line = yarn_berry_common::expected_checksum_line( &String::from_utf8_lossy(®istry_lock), @@ -579,6 +607,7 @@ async fn berry_hosted_project( patched, host, registry_lock, + registry_pkg, _server: server, }) } @@ -598,7 +627,11 @@ fn fresh_yarnrc(fx: &BerryRedirectFixture) -> String { /// Fresh dir with only the committable files, then `yarn install --immutable /// --check-cache` offline-from-registry (the wiremock host is whitelisted for -/// http). Returns the fresh dir + the install output. +/// http). The install runs with an npm registry token that yarn must apply to +/// every registry request (`YARN_NPM_AUTH_TOKEN` + `YARN_NPM_ALWAYS_AUTH`), +/// the CI shape #404 leaked to the patch host: [`assert_patch_host_got_no_auth`] +/// checks the hosted tarball request carried none. Returns the fresh dir + +/// the install output. fn fresh_checkout_yarn_install(fx: &BerryRedirectFixture) -> (PathBuf, Output) { let fresh = fx.tmp.path().join("fresh"); std::fs::create_dir_all(&fresh).unwrap(); @@ -619,11 +652,62 @@ fn fresh_checkout_yarn_install(fx: &BerryRedirectFixture) -> (PathBuf, Output) { &[ ("YARN_GLOBAL_FOLDER", fresh_global.to_str().unwrap()), ("YARN_ENABLE_GLOBAL_CACHE", "false"), + ("YARN_NPM_AUTH_TOKEN", REGISTRY_TOKEN), + ("YARN_NPM_ALWAYS_AUTH", "true"), + // Hardened mode (yarn enables it on its own for public-PR CI) + // re-validates every lock resolution against its descriptor; a + // tarball locator under an `npm:` key fails it with YN0078 — + // why the pin routes through `resolutions` (#404). + ("YARN_ENABLE_HARDENED_MODE", "true"), ], ); (fresh, ci) } +/// The npm registry token the fresh install is configured with. +const REGISTRY_TOKEN: &str = "SOCKET-E2E-REGISTRY-TOKEN"; + +/// #404: the patch host fetched the hosted tarball, and no request it +/// received carried an `Authorization` header (or the registry token in any +/// header) — the hosted pin must never hand registry credentials to it. +async fn assert_patch_host_got_no_auth(fx: &BerryRedirectFixture) { + let requests = fx + ._server + .received_requests() + .await + .expect("wiremock request recording is on"); + let tarball_gets: Vec<_> = requests + .iter() + .filter(|r| r.url.path().ends_with(".tgz")) + .collect(); + assert!( + !tarball_gets.is_empty(), + "the fresh install must fetch the hosted tarball from the patch host" + ); + // The same wiremock also plays the Socket API, whose requests carry the + // CLI's own API token — only the yarn-made tarball fetches are judged + // for an Authorization header; the registry token must appear nowhere. + for r in &tarball_gets { + assert!( + !r.headers.contains_key("authorization"), + "{} {} carried an Authorization header to the patch host: {:?}", + r.method, + r.url, + r.headers.get("authorization") + ); + } + for r in &requests { + for (name, value) in r.headers.iter() { + assert!( + !value.to_str().unwrap_or("").contains(REGISTRY_TOKEN), + "{} {} leaked the registry token in header {name}", + r.method, + r.url + ); + } + } +} + /// The manifest-less VEX matrix over the hosted rewrite `driver` produced /// (see `yarn_berry_common`): fresh checkouts without the manifest, without /// the ledgers, offline, tampered, reverted to the registry and installed @@ -631,7 +715,10 @@ fn fresh_checkout_yarn_install(fx: &BerryRedirectFixture) -> (PathBuf, Output) { /// the REAL binary against a mock patch API. fn hosted_manifestless_vex_matrix(fx: &BerryRedirectFixture, driver: HostedDriver) { let yarnrc = fresh_yarnrc(fx); - let registry_state = [("yarn.lock", fx.registry_lock.clone())]; + let registry_state = [ + ("yarn.lock", fx.registry_lock.clone()), + ("package.json", fx.registry_pkg.clone()), + ]; let yarn = |cwd: &Path, args: &[&str], env: &[(&str, &str)]| corepack(cwd, yarn_berry(), args, env); let api_url = fx._server.uri(); @@ -698,6 +785,7 @@ async fn berry_redirect_fresh_checkout_installs_patched_bytes() { installed, fx.patched, "fresh install must be byte-identical to the patched content" ); + assert_patch_host_got_no_auth(&fx).await; hosted_manifestless_vex_matrix(&fx, HostedDriver::Scan); } @@ -706,7 +794,7 @@ async fn berry_redirect_fresh_checkout_installs_patched_bytes() { /// routes through the SAME hosted engine as `scan --mode hosted`, so the /// berry chain must hold unchanged — including the `10c0` cacheKey bootstrap /// (the fixture still resolves the patched tarball with a real yarn to pin -/// the exact cache-zip checksum) and the lock's `::__archiveUrl=` + +/// the exact cache-zip checksum) and the lock's tarball-URL locator + /// `checksum: 10c0/` splice — and the fresh `yarn install --immutable /// --check-cache` pulls the patched bytes from the hosted tarball. The uuid /// identifier path is exempt from installed narrowing, so only the view + @@ -740,7 +828,7 @@ async fn berry_get_uuid_hosted_fresh_checkout_installs() { hosted_manifestless_vex_matrix(&fx, HostedDriver::GetUuid); } -/// Negative twin: the archiveUrl serves a DIFFERENT tarball while the lock +/// Negative twin: the hosted URL serves a DIFFERENT tarball while the lock /// pins the real `10c0` checksum — the fresh `--check-cache` install must fail /// with YN0018. #[tokio::test(flavor = "multi_thread")] diff --git a/crates/socket-patch-cli/tests/e2e_yarn4_pnpm_linker_build.rs b/crates/socket-patch-cli/tests/e2e_yarn4_pnpm_linker_build.rs index d5440efab..efae4ab74 100644 --- a/crates/socket-patch-cli/tests/e2e_yarn4_pnpm_linker_build.rs +++ b/crates/socket-patch-cli/tests/e2e_yarn4_pnpm_linker_build.rs @@ -14,7 +14,7 @@ //! (`e2e_redirect_yarn_berry_build.rs` / `e2e_vendor_yarn_berry_build.rs`): //! //! * hosted — `scan --mode hosted` rewires `yarn.lock` to the hosted -//! `__archiveUrl` + `10c0` checksum (bootstrap-resolution trick, see the +//! tarball-URL locator + `10c0` checksum (bootstrap-resolution trick, see the //! redirect sibling); a fresh checkout of only the committable files //! passes `yarn install --immutable --check-cache` offline-from-registry //! and serves the patched bytes THROUGH the `.store` symlink layout. @@ -557,10 +557,29 @@ async fn yarn4_pnpm_linker_hosted_redirect_fresh_checkout_installs_patched_bytes ); let lock = std::fs::read_to_string(proj.join("yarn.lock")).unwrap(); - let encoded = socket_patch_core::utils::uri::encode_uri_component(&hosted_url); assert!( - lock.contains("::__archiveUrl=") && lock.contains(&encoded), - "yarn.lock must carry the encoded __archiveUrl; got:\n{lock}" + lock.contains(&format!("\n resolution: \"{DEP}@{hosted_url}\"")), + "yarn.lock must pin the hosted tarball locator; got:\n{lock}" + ); + // #404 option C: the entry is keyed by the tarball descriptor, and the + // root package.json routes the locked descriptor there. + assert!( + lock.lines() + .any(|l| l.trim_end_matches('\r') == format!("\"{DEP}@{hosted_url}\":")), + "yarn.lock entry must be keyed by the tarball descriptor; got:\n{lock}" + ); + let root_pkg = std::fs::read_to_string(proj.join("package.json")).unwrap(); + let root_pkg: serde_json::Value = serde_json::from_str(&root_pkg).unwrap(); + assert!( + root_pkg["resolutions"] + .as_object() + .is_some_and(|r| r.iter().any(|(sel, v)| sel.starts_with(&format!("{DEP}@npm:")) + && v.as_str() == Some(hosted_url.as_str()))), + "package.json must route {DEP} to the hosted tarball: {root_pkg}" + ); + assert!( + !lock.contains("__archiveUrl"), + "the hosted pin must not be an npm: locator (#404); got:\n{lock}" ); let checksum_line = yarn_berry_common::expected_checksum_line( &String::from_utf8_lossy(®istry_lock), @@ -571,11 +590,14 @@ async fn yarn4_pnpm_linker_hosted_redirect_fresh_checkout_installs_patched_bytes "yarn.lock must carry the cache checksum in yarn's own spelling \ ({checksum_line:?}); got:\n{lock}" ); - assert_eq!( - std::fs::read(proj.join("package.json")).unwrap(), - pkg_before, - "hosted redirect must not touch package.json" - ); + // #404 option C: the only package.json change is the `resolutions` pin. + { + let mut after: serde_json::Value = + serde_json::from_slice(&std::fs::read(proj.join("package.json")).unwrap()).unwrap(); + let before: serde_json::Value = serde_json::from_slice(&pkg_before).unwrap(); + after.as_object_mut().unwrap().shift_remove("resolutions"); + assert_eq!(after, before, "the hosted pin only adds `resolutions` to package.json"); + } eprintln!("HOSTED REWIRE OK"); // FRESH-CHECKOUT PROOF: committable files only, offline from the @@ -603,7 +625,7 @@ async fn yarn4_pnpm_linker_hosted_redirect_fresh_checkout_installs_patched_bytes eprintln!("FRESH INSTALL + YARN NODE RESOLUTION OK"); // MANIFEST-LESS VEX over the hosted wiring (see `yarn_berry_common`). - let registry_state = [("yarn.lock", registry_lock)]; + let registry_state = [("yarn.lock", registry_lock), ("package.json", pkg_before.clone())]; let yarn = |cwd: &Path, args: &[&str], env: &[(&str, &str)]| corepack(cwd, yarn_berry(), args, env); let api_url = server.uri(); diff --git a/crates/socket-patch-cli/tests/e2e_yarn4_workspaces_build.rs b/crates/socket-patch-cli/tests/e2e_yarn4_workspaces_build.rs index 2c8ca559b..d2aec0078 100644 --- a/crates/socket-patch-cli/tests/e2e_yarn4_workspaces_build.rs +++ b/crates/socket-patch-cli/tests/e2e_yarn4_workspaces_build.rs @@ -17,8 +17,8 @@ //! `corepack yarn@4.12.0` (network for fixture setup only) and prove: //! //! * hosted — `scan --mode hosted` from the root rewires the member's -//! `left-pad@npm:1.3.0` lock entry to the hosted `__archiveUrl` + `10c0` -//! checksum (bootstrap-resolution trick, see the redirect sibling) +//! `left-pad@npm:1.3.0` lock entry to the hosted tarball-URL locator + +//! `10c0` checksum (bootstrap-resolution trick, see the redirect sibling) //! without touching either package.json; a fresh checkout of only the //! committable files installs the patched bytes offline-from-registry, //! and the member resolves them through `yarn node`. @@ -552,10 +552,29 @@ async fn yarn4_workspaces_hosted_redirect_rewires_member_dep_from_root_scan() { // The single root lock carries the member dep's hosted pin; the // workspace entries stay workspace-resolved and no package.json moved. let lock = std::fs::read_to_string(proj.join("yarn.lock")).unwrap(); - let encoded = socket_patch_core::utils::uri::encode_uri_component(&hosted_url); assert!( - lock.contains("::__archiveUrl=") && lock.contains(&encoded), - "yarn.lock must carry the encoded __archiveUrl; got:\n{lock}" + lock.contains(&format!("\n resolution: \"{DEP}@{hosted_url}\"")), + "yarn.lock must pin the hosted tarball locator; got:\n{lock}" + ); + // #404 option C: the entry is keyed by the tarball descriptor, and the + // root package.json routes the locked descriptor there. + assert!( + lock.lines() + .any(|l| l.trim_end_matches('\r') == format!("\"{DEP}@{hosted_url}\":")), + "yarn.lock entry must be keyed by the tarball descriptor; got:\n{lock}" + ); + let root_pkg = std::fs::read_to_string(proj.join("package.json")).unwrap(); + let root_pkg: serde_json::Value = serde_json::from_str(&root_pkg).unwrap(); + assert!( + root_pkg["resolutions"] + .as_object() + .is_some_and(|r| r.iter().any(|(sel, v)| sel.starts_with(&format!("{DEP}@npm:")) + && v.as_str() == Some(hosted_url.as_str()))), + "package.json must route {DEP} to the hosted tarball: {root_pkg}" + ); + assert!( + !lock.contains("__archiveUrl"), + "the hosted pin must not be an npm: locator (#404); got:\n{lock}" ); let checksum_line = yarn_berry_common::expected_checksum_line( &String::from_utf8_lossy(®istry_lock), @@ -570,11 +589,15 @@ async fn yarn4_workspaces_hosted_redirect_rewires_member_dep_from_root_scan() { lock.contains("\"app@workspace:packages/app\""), "the workspace member entry must stay workspace-resolved:\n{lock}" ); - assert_eq!( - std::fs::read(proj.join("package.json")).unwrap(), - root_pkg_before, - "hosted redirect must not touch the root package.json" - ); + // #404 option C: the root package.json gains only the `resolutions` pin + // (it is the one yarn reads resolutions from); the member is untouched. + { + let mut after: serde_json::Value = + serde_json::from_slice(&std::fs::read(proj.join("package.json")).unwrap()).unwrap(); + let before: serde_json::Value = serde_json::from_slice(&root_pkg_before).unwrap(); + after.as_object_mut().unwrap().shift_remove("resolutions"); + assert_eq!(after, before, "the hosted pin only adds `resolutions` to the root package.json"); + } assert_eq!( std::fs::read(proj.join("packages/app/package.json")).unwrap(), member_pkg_before, @@ -607,7 +630,7 @@ async fn yarn4_workspaces_hosted_redirect_rewires_member_dep_from_root_scan() { eprintln!("FRESH INSTALL + MEMBER RESOLUTION OK"); // MANIFEST-LESS VEX over the hosted wiring (see `yarn_berry_common`). - let registry_state = [("yarn.lock", registry_lock)]; + let registry_state = [("yarn.lock", registry_lock), ("package.json", root_pkg_before.clone())]; let yarn = |cwd: &Path, args: &[&str], env: &[(&str, &str)]| corepack(cwd, yarn_berry(), args, env); let api_url = server.uri(); diff --git a/crates/socket-patch-cli/tests/in_process_redirect.rs b/crates/socket-patch-cli/tests/in_process_redirect.rs index d9e51ca5b..7ae650809 100644 --- a/crates/socket-patch-cli/tests/in_process_redirect.rs +++ b/crates/socket-patch-cli/tests/in_process_redirect.rs @@ -697,8 +697,10 @@ fn write_berry_project_spelled(root: &Path, spell: impl Fn(&str) -> String) { .unwrap(); } -/// The berry leg: the yarn.lock entry is repointed via `::__archiveUrl=` (the -/// URL percent-encoded) and its `checksum:` becomes the yarnBerry10c0. The +/// The berry leg: the yarn.lock entry's resolution becomes the hosted +/// tarball-URL locator (never an `npm:` one, whose fetcher sends npm +/// registry auth to the patch host — #404) and its `checksum:` becomes the +/// yarnBerry10c0. The /// descriptor KEY is preserved (so `--immutable` still passes), no ledger is /// written, and a second run is a no-op. #[tokio::test] @@ -715,20 +717,30 @@ async fn scan_redirect_rewrites_yarn_berry_lock() { assert_eq!(code, 0, "scan --mode hosted (berry) should succeed"); let lock = std::fs::read_to_string(tmp.path().join("yarn.lock")).unwrap(); - // yarn writes `__archiveUrl=`; assert both the - // binding marker and the encoded URL landed. - let encoded = socket_patch_core::utils::uri::encode_uri_component(HOSTED_URL); assert!( - lock.contains("::__archiveUrl=") && lock.contains(&encoded), - "resolution must carry the encoded __archiveUrl; got:\n{lock}" + lock.contains(&format!("\n resolution: \"{NAME}@{HOSTED_URL}\"\n")), + "resolution must be the hosted tarball locator; got:\n{lock}" + ); + assert!( + !lock.contains("__archiveUrl") && !lock.contains(&format!("resolution: \"{NAME}@npm:")), + "the hosted pin must not be an npm: locator; got:\n{lock}" ); assert!( lock.contains(BERRY_CHECKSUM), "checksum must be the yarnBerry10c0" ); + // Option C (#404): the entry is re-keyed by the tarball descriptor, and + // the root package.json routes the original descriptor there. assert!( - lock.contains(&format!("\"{NAME}@npm:^{VERSION}\":")), - "the descriptor key must be preserved verbatim; got:\n{lock}" + lock.contains(&format!("\"{NAME}@{HOSTED_URL}\":")), + "the entry is keyed by the tarball descriptor; got:\n{lock}" + ); + let pkg = std::fs::read_to_string(tmp.path().join("package.json")).unwrap(); + let pkg: serde_json::Value = serde_json::from_str(&pkg).unwrap(); + assert_eq!( + pkg["resolutions"], + serde_json::json!({ format!("{NAME}@npm:^{VERSION}"): HOSTED_URL }), + "{pkg}" ); vlt_hosted_common::assert_no_ledger(tmp.path()); @@ -770,9 +782,7 @@ async fn scan_redirect_rewrites_crlf_and_bom_yarn_berry_locks_and_rollback_resto Some(tarball), ) .await; - let encoded = socket_patch_core::utils::uri::encode_uri_component( - &HOSTED_URL.replace("http://patch.test", &server.uri()), - ); + let hosted_url = HOSTED_URL.replace("http://patch.test", &server.uri()); for (label, bom) in [("crlf", ""), ("bom+crlf", "\u{feff}")] { let tmp = tempfile::tempdir().unwrap(); @@ -792,7 +802,7 @@ async fn scan_redirect_rewrites_crlf_and_bom_yarn_berry_locks_and_rollback_resto ); let lock = std::fs::read_to_string(&lock_path).unwrap(); assert!( - lock.contains(&format!("::__archiveUrl={encoded}\"\r\n")) + lock.contains(&format!(" resolution: \"{NAME}@{hosted_url}\"\r\n")) && lock.contains(&format!(" checksum: {BERRY_CHECKSUM}\r\n")), "{label}: the entry is redirected in CRLF: {lock:?}" ); @@ -841,10 +851,89 @@ async fn scan_redirect_rewrites_crlf_and_bom_yarn_berry_locks_and_rollback_resto "{label}: rollback restores the pristine CRLF lock (upstream checksum \ re-derived from the registry tarball)" ); + let pkg: serde_json::Value = + serde_json::from_str(&std::fs::read_to_string(tmp.path().join("package.json")).unwrap()) + .unwrap(); + assert!( + pkg.get("resolutions").is_none(), + "{label}: rollback drops the resolutions pin: {pkg}" + ); vlt_hosted_common::assert_no_ledger(tmp.path()); } } +/// #404 upgrade path: a lock pinned by an earlier release carries the old +/// `npm:::__archiveUrl=` resolution, which makes yarn's npm fetcher +/// send registry auth to the patch host. `rollback` must still recognize and +/// restore that legacy pin, and a repeat hosted `scan` must re-pin it to the +/// tarball-URL locator. +#[tokio::test] +#[serial] +async fn yarn_berry_legacy_archive_url_pin_is_rolled_back_and_repinned() { + let server = MockServer::start().await; + mock_discovery(&server).await; + let hosted_url = HOSTED_URL.replace("http://patch.test", &server.uri()); + mock_reference_with_berry_url(&server, &hosted_url).await; + mock_view(&server).await; + let tarball = upstream_tarball(); + mock_npm_registry( + &server, + &vlt_hosted_common::sha512_sri(&tarball), + Some(tarball), + ) + .await; + let legacy = |t: &str| { + t.replace( + &format!("resolution: \"{NAME}@npm:{VERSION}\""), + &format!( + "resolution: \"{NAME}@npm:{VERSION}::__archiveUrl={}\"", + socket_patch_core::utils::uri::encode_uri_component(&hosted_url) + ), + ) + .replace(&format!("10c0/{}", "3".repeat(128)), BERRY_CHECKSUM) + }; + + // Rollback of the legacy pin restores the registry entry. + let tmp = tempfile::tempdir().unwrap(); + write_berry_project_spelled(tmp.path(), legacy); + let lock_path = tmp.path().join("yarn.lock"); + assert!(std::fs::read_to_string(&lock_path) + .unwrap() + .contains("::__archiveUrl=")); + let (code, env) = rollback_json_with_origin(tmp.path(), &server, &server.uri()); + assert_eq!(code, Some(0), "rollback: {env:#}"); + assert_eq!( + env["hosted"]["reverted"], + serde_json::json!([PURL]), + "{env:#}" + ); + let restored = std::fs::read_to_string(&lock_path).unwrap(); + assert!( + restored.contains(&format!("\n resolution: \"{NAME}@npm:{VERSION}\"\n")) + && !restored.contains("__archiveUrl") + && !restored.contains(BERRY_CHECKSUM), + "the registry entry is restored: {restored}" + ); + + // A repeat hosted scan re-pins the legacy entry as a tarball locator. + let tmp = tempfile::tempdir().unwrap(); + write_berry_project_spelled(tmp.path(), legacy); + let lock_path = tmp.path().join("yarn.lock"); + let env = run_redirect_subprocess_with( + tmp.path(), + &server.uri(), + &["--patch-server-url", &server.uri()], + ); + assert_eq!(env["redirect"]["redirected"], 1, "{env:#}"); + let lock = std::fs::read_to_string(&lock_path).unwrap(); + assert!( + lock.contains(&format!("\n resolution: \"{NAME}@{hosted_url}\"\n")) + && !lock.contains("__archiveUrl"), + "the legacy pin is migrated: {lock}" + ); + vlt_hosted_common::assert_no_ledger(tmp.path()); +} + /// A berry lock whose line endings are MIXED (CRLF and LF, or a bare CR) /// cannot be kept in one style — and yarn itself rejects it under /// `--immutable` — so the hosted run refuses it untouched with a code that diff --git a/crates/socket-patch-cli/tests/in_process_vendor.rs b/crates/socket-patch-cli/tests/in_process_vendor.rs index 461b6d41a..41d9734ab 100644 --- a/crates/socket-patch-cli/tests/in_process_vendor.rs +++ b/crates/socket-patch-cli/tests/in_process_vendor.rs @@ -1260,7 +1260,8 @@ async fn berry_crlf_takeovers_round_trip_both_directions() { npm_tgz("left-pad", "1.3.0", ORIG_INDEX), ) .await; - let encoded = socket_patch_core::utils::uri::encode_uri_component(&hosted_url); + // The hosted pin is the tarball-URL locator `left-pad@` (#404). + let encoded = format!("left-pad@{hosted_url}\""); let (pkg, lock) = ( windows_shape(BERRY_WIN_PKG, true), windows_shape(&berry_win_lock(), false), @@ -1347,11 +1348,20 @@ async fn berry_crlf_takeovers_round_trip_both_directions() { .contains("redirect_takeover_reverted_vendored"), "the takeover is surfaced: {env:#}" ); - assert_eq!( - std::fs::read_to_string(root.join("package.json")).unwrap(), - pkg, - "the vendored resolutions entry is reverted byte-exactly (BOM + CRLF kept)" + // The vendored `resolutions` entry is gone and the hosted pin (#404 + // option C) took its place, in the manifest's own layout: BOM + CRLF. + let hosted_pkg = std::fs::read_to_string(root.join("package.json")).unwrap(); + assert!(hosted_pkg.starts_with('\u{feff}'), "BOM kept: {hosted_pkg:?}"); + let pin_line = format!(" \"left-pad@npm:1.3.0\": \"{hosted_url}\"\r\n"); + assert!( + hosted_pkg.contains(&pin_line) && !hosted_pkg.contains(".socket/vendor/"), + "the hosted pin replaced the vendored resolutions entry: {hosted_pkg:?}" + ); + let unpinned = hosted_pkg.replace( + &format!(",\r\n \"resolutions\": {{\r\n{pin_line} }}"), + "", ); + assert_eq!(unpinned, pkg, "nothing else in package.json changed"); let hosted_lock = std::fs::read_to_string(root.join("yarn.lock")).unwrap(); assert!( hosted_lock.contains(&encoded) && !hosted_lock.contains(".socket/vendor/"), diff --git a/crates/socket-patch-cli/tests/mode_migration_npm.rs b/crates/socket-patch-cli/tests/mode_migration_npm.rs index ab7f98ee8..7fb28b5f0 100644 --- a/crates/socket-patch-cli/tests/mode_migration_npm.rs +++ b/crates/socket-patch-cli/tests/mode_migration_npm.rs @@ -948,7 +948,7 @@ async fn berry_hosted_then_vendored_takeover_round_trips_to_registry() { assert_eq!(code, 0, "hosted scan failed: {stdout}\n{stderr}"); let lock = read(&proj, "yarn.lock"); assert!( - lock.contains("::__archiveUrl="), + lock.contains(&format!("@{hosted_url}\"")), "hosted wiring present:\n{lock}" ); @@ -1240,9 +1240,8 @@ async fn berry_vendored_then_hosted_takeover_leaves_pure_hosted() { "the berry resolutions entry must be reverted" ); let lock = read(&proj, "yarn.lock"); - let encoded = socket_patch_core::utils::uri::encode_uri_component(&hosted_url); assert!( - lock.contains("::__archiveUrl=") && lock.contains(&encoded) && lock.contains(&checksum), + lock.contains(&format!("@{hosted_url}\"")) && lock.contains(&checksum), "lock points hosted:\n{lock}" ); assert!( diff --git a/crates/socket-patch-cli/tests/yarn_berry_common/mod.rs b/crates/socket-patch-cli/tests/yarn_berry_common/mod.rs index 04ce2881e..e8ff74216 100644 --- a/crates/socket-patch-cli/tests/yarn_berry_common/mod.rs +++ b/crates/socket-patch-cli/tests/yarn_berry_common/mod.rs @@ -371,7 +371,7 @@ pub fn off_runtime(f: impl FnOnce() -> R + Send) -> R { /// How the flow wired the patched dependency. #[derive(Clone, Debug)] pub enum BerryWiring { - /// `yarn.lock` resolves it via `::__archiveUrl=` on `patch_server` + /// `yarn.lock` resolves it via a tarball-URL locator on `patch_server` /// (the mock tarball host — not a Socket host, so every VEX run passes /// it as `--patch-server-url`). Hosted { patch_server: String }, diff --git a/crates/socket-patch-core/src/hosted/engine.rs b/crates/socket-patch-core/src/hosted/engine.rs index 0b8ff09bc..aebeac1ab 100644 --- a/crates/socket-patch-core/src/hosted/engine.rs +++ b/crates/socket-patch-core/src/hosted/engine.rs @@ -424,12 +424,23 @@ pub async fn read_candidate_files( } } - // The root manifest's `overrides` decide which git / url / `file:` - // dependent specs npm really installs from (#490). Only the npm lock - // rewriter reads it, as advisory input: no rewriter edits it, so a link - // or an unreadable in-memory entry is left out (the rewriter then keeps - // its conservative reading) rather than refused. + // A yarn berry lock is pinned through the root manifest's `resolutions` + // (see `patch::redirect::rewrite_yarn_berry`), so beside one the + // manifest is a rewrite target: read strictly, a link or an unreadable + // in-memory entry refused like any other file the rewrite writes. if candidates.iter().any(|c| c.dep.ecosystem == "npm") + && out + .files + .get("yarn.lock") + .is_some_and(|lock| crate::patch::redirect::is_berry_lock(lock)) + { + out.read(view, unreadable, "package.json").await; + // Otherwise the root manifest's `overrides` decide which git / url / + // `file:` dependent specs npm really installs from (#490). Only the npm + // lock rewriter reads it, as advisory input: no rewriter edits it, so a + // link or an unreadable in-memory entry is left out (the rewriter then + // keeps its conservative reading) rather than refused. + } else if candidates.iter().any(|c| c.dep.ecosystem == "npm") && NPM_LOCKS.iter().any(|lock| out.files.contains_key(*lock)) { let rel = crate::hosted::memory::select::NPM_MANIFEST_REL; @@ -777,8 +788,9 @@ enum ProbeStep { /// ([`artifact_url_spellings`], raw or the `\/`-escaped slashes an old /// composer.lock spells them with), so a writer's spelling can never be /// one this probe misses. -/// - The percent-encoded URL: the berry rewriter writes it into the lock's -/// `::__archiveUrl=` binding, so the raw form is absent. +/// - The percent-encoded URL: releases up to 5.0 wrote it into a berry +/// lock's `::__archiveUrl=` binding (today's berry pin is the raw URL), so +/// a lock pinned by them carries no raw form. /// - The registry index URL and the maven suffixed version, when present. pub fn candidate_presence_needles(dep: &DepOverride) -> Vec { let artifact_url = dep.artifact_url.as_str(); @@ -1334,15 +1346,20 @@ fn confirm( // artifact failed the preflight beside another npm-family lock) may // still hold an earlier run's pin: only the sibling lock this run // rewrote can confirm that dep. + // The yarn berry pin's `package.json` `resolutions` entry is only half of + // it — the URL-keyed `yarn.lock` entry is what installs — so a hosted URL + // left in the manifest (an earlier run, a refused rewrite) proves + // nothing on its own: the manifest never feeds the probe. let final_texts: Vec<(&str, &String)> = files .iter() .filter(|(name, _)| !(pdm_inactive && name.as_str() == "pdm.lock")) + .filter(|(name, _)| name.as_str() != "package.json") .map(|(name, content)| (name.as_str(), rewrite.files.get(name).unwrap_or(content))) .chain( rewrite .files .iter() - .filter(|(name, _)| !files.contains_key(*name)) + .filter(|(name, _)| !files.contains_key(*name) && name.as_str() != "package.json") .map(|(name, content)| (name.as_str(), content)), ) .collect(); @@ -1407,6 +1424,13 @@ fn confirm( if rewrite.refused_pnpm_uuids.contains(uuid) { return ProbeStep::Decided(false); } + // A yarn berry pin is the URL-keyed lock entry AND the manifest + // `resolutions` routing to it; the URL in `yarn.lock` alone (the + // routing removed, a refused re-pin) installs nothing, so the + // berry rewriter's own report decides every dep its lock holds. + if rewrite.yarn_berry_uuids.contains(uuid) { + return ProbeStep::Decided(rewrite.confirmed_yarn_berry_uuids.contains(uuid)); + } // Cargo is transactional: the rewriter reports exactly which // patch uuids FULLY landed (manifest pin + lock + registry // block). Substring presence must never confirm a cargo dep — @@ -1672,6 +1696,143 @@ mod tests { assert!(read.unreadable_reads.is_empty()); } + /// A hosted URL left in a berry project's `package.json` `resolutions` + /// while `yarn.lock` still resolves the registry entry confirms nothing: + /// only the lock pin installs (#404). + #[test] + fn a_resolutions_url_alone_does_not_confirm_a_berry_redirect() { + use crate::patch::redirect::Integrity; + let url = "https://patch.socket.dev/patch/npm/left-pad/1.3.0/tok/uuid/left-pad-1.3.0.tgz"; + let candidate = Candidate { + purl: "pkg:npm/left-pad@1.3.0".into(), + dep: DepOverride { + ecosystem: "npm".into(), + name: "left-pad".into(), + namespace: None, + version: "1.3.0".into(), + token: "tok".into(), + patch_uuid: "uuid".into(), + artifact_url: url.into(), + registry_override: None, + integrity: Integrity::default(), + }, + }; + let lock = "__metadata:\n version: 8\n cacheKey: 10c0\n\n\"left-pad@npm:^1.3.0\":\n \ + version: 1.3.0\n resolution: \"left-pad@npm:1.3.0\"\n"; + let mut files = BTreeMap::new(); + files.insert("yarn.lock".to_string(), lock.to_string()); + files.insert( + "package.json".to_string(), + format!("{{\"resolutions\": {{\"left-pad@npm:^1.3.0\": \"{url}\"}}}}"), + ); + let none = confirm( + &files, + &RewriteResult::default(), + std::slice::from_ref(&candidate), + false, + &BTreeSet::new(), + ); + assert!(none.is_empty(), "{none:?}"); + // The lock pin itself still confirms. + files.insert( + "yarn.lock".to_string(), + lock.replace( + "resolution: \"left-pad@npm:1.3.0\"", + &format!("resolution: \"left-pad@{url}\""), + ), + ); + let confirmed = confirm( + &files, + &RewriteResult::default(), + std::slice::from_ref(&candidate), + false, + &BTreeSet::new(), + ); + assert_eq!(confirmed.len(), 1, "{confirmed:?}"); + } + + /// Review of #465: a hosted berry pin whose `resolutions` routing was + /// removed keeps its URL-keyed lock entry. The rewriter refuses to re-pin + /// it (the routing is not ours to recreate silently), so nothing + /// installs the patch — and the URL in `yarn.lock` must not confirm it + /// (a confirmed dep feeds the in-run VEX `not_affected` exemption). + #[test] + fn an_orphaned_berry_lock_pin_is_not_confirmed() { + use crate::patch::redirect::{rewrite_registry_redirect, Integrity}; + let url = "https://patch.socket.dev/patch/npm/left-pad/1.3.0/tok/uuid/left-pad-1.3.0.tgz"; + let candidate = Candidate { + purl: "pkg:npm/left-pad@1.3.0".into(), + dep: DepOverride { + ecosystem: "npm".into(), + name: "left-pad".into(), + namespace: None, + version: "1.3.0".into(), + token: "tok".into(), + patch_uuid: "uuid".into(), + artifact_url: url.into(), + registry_override: None, + integrity: Integrity { + yarn_berry10c0: Some(format!("10c0/{}", "7".repeat(128))), + ..Default::default() + }, + }, + }; + let manifest = "{\n \"name\": \"app\"\n}\n".to_string(); + let mut files = BTreeMap::new(); + files.insert( + "yarn.lock".to_string(), + format!( + "__metadata:\n version: 8\n cacheKey: 10c0\n\n\"left-pad@npm:^1.3.0\":\n \ + version: 1.3.0\n resolution: \"left-pad@npm:1.3.0\"\n checksum: 10c0/{}\n \ + languageName: node\n linkType: hard\n", + "3".repeat(128) + ), + ); + files.insert("package.json".to_string(), manifest.clone()); + let run = |files: &BTreeMap| { + let rewrite = rewrite_registry_redirect(files, std::slice::from_ref(&candidate.dep)); + let confirmed = confirm( + files, + &rewrite, + std::slice::from_ref(&candidate), + false, + &BTreeSet::new(), + ); + (rewrite, confirmed) + }; + let (first, confirmed) = run(&files); + assert_eq!(confirmed.len(), 1, "{:?}", first.warnings); + let pinned_lock = first.files["yarn.lock"].clone(); + assert!( + pinned_lock.contains(&format!("\"left-pad@{url}\":")), + "{pinned_lock}" + ); + + // Rescan with the pin intact: still confirmed. + let mut pinned = files.clone(); + pinned.insert("yarn.lock".to_string(), pinned_lock.clone()); + pinned.insert( + "package.json".to_string(), + first.files["package.json"].clone(), + ); + assert_eq!(run(&pinned).1.len(), 1); + + // The routing removed: the URL is still in the lock, nothing installs it. + let mut orphan = files; + orphan.insert("yarn.lock".to_string(), pinned_lock); + orphan.insert("package.json".to_string(), manifest); + let (rewrite, confirmed) = run(&orphan); + assert!( + rewrite + .warnings + .iter() + .any(|w| w.code == "redirect_yarn_berry_resolutions_conflict"), + "{:?}", + rewrite.warnings + ); + assert!(confirmed.is_empty(), "{confirmed:?}"); + } + fn left_pad_candidate() -> Candidate { use crate::patch::redirect::Integrity; Candidate { diff --git a/crates/socket-patch-core/src/patch/redirect/mod.rs b/crates/socket-patch-core/src/patch/redirect/mod.rs index d6d282eae..38ce4b433 100644 --- a/crates/socket-patch-core/src/patch/redirect/mod.rs +++ b/crates/socket-patch-core/src/patch/redirect/mod.rs @@ -19,7 +19,7 @@ use std::borrow::Cow; use std::collections::BTreeMap; use std::sync::LazyLock; -use regex::Regex; +use regex::{NoExpand, Regex}; use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; @@ -65,7 +65,7 @@ mod python_lock_equivalence_tests; mod requirements; mod staged; mod state; -mod hosted_url; +pub(crate) mod hosted_url; pub mod upstream; pub mod vlt; pub mod vlt_heal; @@ -239,6 +239,24 @@ pub struct RewriteResult { /// An incomplete pnpm rewrite must not be confirmed by finding its URL /// in another instance, a comment, or another lockfile. pub refused_pnpm_uuids: std::collections::BTreeSet, + /// Patch uuids whose package version a yarn berry `yarn.lock` locks, so + /// the berry rewriter alone decides them: the hosted pin is the + /// URL-keyed lock entry AND the root `package.json` `resolutions` + /// routing to it, and a URL found in the lock proves only the first half. + // Unserialized when empty, so the blessed rewrite goldens (which hash the + // whole result) are unchanged by these fields for every non-berry case. + #[cfg_attr( + test, + serde(skip_serializing_if = "std::collections::BTreeSet::is_empty") + )] + pub yarn_berry_uuids: std::collections::BTreeSet, + /// The [`Self::yarn_berry_uuids`] whose pin is complete — lock entry and + /// manifest routing — written by this run or already in place. + #[cfg_attr( + test, + serde(skip_serializing_if = "std::collections::BTreeSet::is_empty") + )] + pub confirmed_yarn_berry_uuids: std::collections::BTreeSet, pub python_lock_uuids: std::collections::BTreeSet, pub confirmed_python_lock_uuids: std::collections::BTreeSet, pub refused_python_lock_uuids: std::collections::BTreeSet, @@ -538,6 +556,8 @@ fn merge_group_delta(result: &mut RewriteResult, delta: RewriteResult) { confirmed_pdm_uuids, refused_pdm_uuids, refused_pnpm_uuids, + yarn_berry_uuids, + confirmed_yarn_berry_uuids, python_lock_uuids, confirmed_python_lock_uuids, refused_python_lock_uuids, @@ -564,6 +584,10 @@ fn merge_group_delta(result: &mut RewriteResult, delta: RewriteResult) { result.confirmed_pdm_uuids.extend(confirmed_pdm_uuids); result.refused_pdm_uuids.extend(refused_pdm_uuids); result.refused_pnpm_uuids.extend(refused_pnpm_uuids); + result.yarn_berry_uuids.extend(yarn_berry_uuids); + result + .confirmed_yarn_berry_uuids + .extend(confirmed_yarn_berry_uuids); result.python_lock_uuids.extend(python_lock_uuids); result .confirmed_python_lock_uuids @@ -3221,15 +3245,43 @@ fn yarn_classic_block_head(block: &str) -> Option<(String, Option)> { } // ── yarn.lock (berry / v2+) ────────────────────────────────────────────────── -// Berry derives its fetch URL from the descriptor's `npm:` resolution and +// Berry fetches each package from its lock entry's `resolution:` locator and // verifies the CONVERTED CACHE ZIP against the lock's `checksum:` (a // `10c0/` over the zip, not the tarball). To redirect ONE dep we -// rewrite only the lock entry: `resolution:` gains yarn's own -// `::__archiveUrl=` binding, and `checksum:` becomes -// our precomputed `integrity.yarnBerry10c0`. The descriptor KEY + package.json -// are untouched (the `name@npm:^range` descriptor still satisfies, so -// `--immutable` passes). Byte-for-byte twin of the TS `rewriteYarnBerry` on -// LF locks; the CRLF / BOM round trip below has no TS counterpart yet. +// rewrite only the lock entry: `resolution:` becomes the plain tarball-URL +// locator `@`, and `checksum:` becomes our precomputed +// `integrity.yarnBerry10c0`. The descriptor KEY + package.json are untouched +// (yarn maps the `name@npm:^range` descriptor to the stored locator, so +// `--immutable` passes). +// +// The locator must NOT keep the `npm:` protocol (e.g. yarn's own +// `npm:::__archiveUrl=` binding, which releases up to 5.0 wrote): +// yarn fetches `npm:` locators with its npm fetcher, which attaches the npm +// registry's credentials (`npmAuthToken`, `YARN_NPM_AUTH_TOKEN`, `npmScopes`) +// to the request for every scoped package, and for every package under +// `npmAlwaysAuth` — handing the registry token to the patch host (#404). A +// tarball-URL locator goes through yarn's tarball fetcher, which sends no +// registry auth and builds the identical cache zip, so the checksum is the +// same. An old `::__archiveUrl=` pin is re-pinned by the next hosted run. + +/// Whether yarn berry fetches `url`, as a `name@` locator, with its +/// tarball HTTP fetcher: an `http(s)://` URL whose path ends in `.tgz` / +/// `.tar.gz` (yarn's `TARBALL_REGEXP` + `PROTOCOL_REGEXP`). A query or +/// fragment is refused too — yarn's regex rejects a `?`, and a `#` would be +/// read as a range selector — as is anything that could break out of the +/// lock's double-quoted `resolution:` string or yarn's `::` binding grammar. +fn yarn_berry_tarball_url_ok(url: &str) -> bool { + let Some((scheme, rest)) = url.split_once("://") else { + return false; + }; + matches!(scheme, "https" | "http") + && !rest.is_empty() + && !url.contains("::") + && !url + .chars() + .any(|c| c.is_whitespace() || c.is_control() || matches!(c, '"' | '\\' | '?' | '#')) + && (url.ends_with(".tgz") || url.ends_with(".tar.gz")) +} /// Only cacheKey `10c0` (yarn 4, compressionLevel 0 default) has a checksum we /// can reproduce offline; matches the vendored backend's `SUPPORTED_CACHE_KEY`. @@ -3351,13 +3403,39 @@ fn rewrite_yarn_berry( preflight_yarn_berry_hosted(raw, files.get(".yarnrc.yml").map(String::as_str)) { result.warnings.push(warning); + // Nothing is verified, so nothing is confirmed — but a dep this lock + // locks is still this rewriter's to decide: an earlier run's URL in + // the lock must not confirm it through the text probe. + let lf = to_lf(body); + for dep in &npm { + if berry_lock_locks(&lf, &full_name(dep), &dep.version) { + result.yarn_berry_uuids.insert(dep.patch_uuid.clone()); + } + } return; } let eol = LineEndings::of(body); let normalized = to_lf(body); let content: &str = &normalized; - let mut blocks: Vec = content.split("\n\n").map(String::from).collect(); + // The trailing newline(s) ride outside the blocks, so an entry moved to + // its sorted position (see [`berry_reposition_blocks`]) never carries + // the file's final newline into the middle of the lock. + let trimmed = content.trim_end_matches('\n'); + let trailing_newlines = &content[trimmed.len()..]; + let mut blocks: Vec = trimmed.split("\n\n").map(String::from).collect(); + let was_sorted = berry_entries_sorted(&blocks); + // The root manifest whose `resolutions` route each pinned descriptor to + // the hosted tarball (see the section header). Parsed once; written back + // in its own layout when a pin changes it. + let manifest_text = files.get(BERRY_MANIFEST).map(String::as_str); + let mut manifest: Option = manifest_text + .and_then(|t| serde_json::from_str::(t.strip_prefix('\u{feff}').unwrap_or(t)).ok()) + .filter(Value::is_object); + let mut manifest_changed = false; + // Keys of the entries re-keyed to a tarball descriptor; yarn sorts lock + // entries by key, so each one moves to its sorted position at the end. + let mut moved_keys: Vec = Vec::new(); let resolution_re = Regex::new(r#"\n {2}resolution: "[^"]*""#).expect("static resolution-line regex is valid"); let checksum_re = @@ -3367,28 +3445,28 @@ fn rewrite_yarn_berry( let fname = full_name(dep); // The API hands the prefixed `10c0/`; a yarn 4.0.x lock spells // its checksums bare, and `--immutable` rejects a respelled one. - let Some(checksum) = dep + // Only needed when the entry must change (checked below): a pin + // already complete keeps the checksum it was written with. + let checksum: Option = dep .integrity .yarn_berry10c0 .as_deref() - .map(|c| crate::vendor::yarn_berry_lock::checksum_in_lock_spelling(content, c)) - else { - result.warnings.push(RewriteWarning { - code: "redirect_yarn_berry_missing_checksum".into(), - detail: format!( - "{fname}@{} has no yarnBerry10c0 cache checksum", - dep.version - ), - }); - continue; - }; + .map(|c| crate::vendor::yarn_berry_lock::checksum_in_lock_spelling(content, c)); // Berry versions are UNQUOTED (` version: 1.3.0`). let version_re = Regex::new(&(String::from(r"\n {2}version: ") + ®ex::escape(&dep.version) + "\n")) .expect("version regex from the escaped version is valid"); let mut matched_any = false; let mut alias_skipped = false; - for block in blocks.iter_mut() { + // Every entry this dep's pin would re-key: `(index, npm ranges)` — + // the ranges are empty for an entry an earlier hosted run already + // keyed by its tarball URL (they are recovered from the manifest). + let mut targets: Vec<(usize, Vec)> = Vec::new(); + // A non-npm entry of the same package version (yarn's builtin + // `patch:` compat entries, `workspace:`, …): its descriptor wraps or + // shares the npm one, so re-keying the npm entry would break it. + let mut shared_descriptor = false; + for (block_idx, block) in blocks.iter().enumerate() { // A block's key is its first line up to a trailing colon; skip // header comment blocks and the leading `__metadata` block. let Some(first_line) = block.lines().next() else { @@ -3463,6 +3541,23 @@ fn rewrite_yarn_berry( // under such a key corrupts the key/resolution protocol pairing. // Mirrors the vendor backend's fail-closed gate // (vendor/yarn_berry_lock.rs). + // A fork alias (`@npm:@`) installs another + // package under this name: its entry is not the patched package, + // whatever its version, so it is never re-keyed. + if parsed.iter().any(|p| { + p.and_then(|(_, range)| berry_npm_alias_target(range)) + .is_some_and(|real| real != fname) + }) { + continue; + } + // An entry an earlier hosted run already keyed by its tarball + // descriptor (`"@"`): ours to re-pin. + if let [Some((_, range))] = parsed.as_slice() { + if berry_hosted_pin_is_ours(range, &fname, Some(&dep.version), &dep.artifact_url) { + targets.push((block_idx, Vec::new())); + continue; + } + } if !parsed.iter().all(|p| { p.expect("every pattern parsed — None-bearing keys are skipped above") .1 @@ -3521,6 +3616,7 @@ fn rewrite_yarn_berry( .collect(); protocols.sort(); protocols.dedup(); + shared_descriptor = true; result.warnings.push(RewriteWarning { code: "redirect_yarn_berry_unsupported_protocol".into(), detail: format!( @@ -3535,61 +3631,489 @@ fn rewrite_yarn_berry( }); continue; } - // Rewrite the resolution wholesale from name+version — handles a - // pre-existing `::__archiveUrl=` (custom-registry lock) for free. - let resolution = format!( - "{fname}@npm:{}::__archiveUrl={}", - dep.version, - crate::utils::uri::encode_uri_component(&dep.artifact_url) - ); - let mut rewritten = resolution_re - .replace(block, format!("\n resolution: \"{resolution}\"").as_str()) - .to_string(); - if checksum_re.is_match(&rewritten) { + targets.push(( + block_idx, + parsed + .iter() + .map(|p| { + p.expect("every pattern parsed — None-bearing keys are skipped above") + .1 + .to_string() + }) + .collect(), + )); + } + if !targets.is_empty() { + matched_any = true; + } + // This lock locks the package version, so this rewriter alone decides + // the dep from here on (see [`RewriteResult::yarn_berry_uuids`]); a + // lock that does not lock it leaves the dep to the other lockfiles. + if matched_any || shared_descriptor { + result.yarn_berry_uuids.insert(dep.patch_uuid.clone()); + } + if shared_descriptor && !targets.is_empty() { + result.warnings.push(RewriteWarning { + code: "redirect_yarn_berry_shared_descriptor".into(), + detail: format!( + "{fname}@{} is also locked through a non-npm entry (e.g. yarn's builtin \ + `patch:` compatibility entry) that wraps the same npm descriptor; pinning \ + that descriptor to the hosted tarball would change the other entry too, \ + so the hosted redirect leaves {fname} untouched — use `scan --mode \ + vendored` or `--mode agent` for this package", + dep.version + ), + }); + continue; + } + if targets.len() > 1 { + result.warnings.push(RewriteWarning { + code: "redirect_yarn_berry_ambiguous_entry".into(), + detail: format!( + "yarn.lock holds {} separate entries resolving {fname}@{}; run `yarn \ + install` once to dedupe the lock, then re-run", + targets.len(), + dep.version + ), + }); + continue; + } + let Some((target_idx, key_ranges)) = targets.pop() else { + if !matched_any && !alias_skipped { + result.warnings.push(RewriteWarning { + code: "redirect_yarn_berry_entry_not_found".into(), + detail: format!("no npm: lock entry resolving {fname}@{}", dep.version), + }); + } + continue; + }; + if !yarn_berry_tarball_url_ok(&dep.artifact_url) { + result.warnings.push(RewriteWarning { + code: "redirect_yarn_berry_artifact_url_unsupported".into(), + detail: format!( + "{fname}@{}: hosted artifact URL {:?} is not an http(s) `.tgz` URL \ + without a query or fragment, so yarn could not fetch it as a \ + tarball locator; leaving the lock entry untouched", + dep.version, dep.artifact_url + ), + }); + continue; + } + // Without a checksum only an entry already keyed by this artifact + // (an earlier run wrote its checksum) can be kept; any other needs + // the checksum written. + let already_keyed = blocks[target_idx] + .lines() + .next() + .is_some_and(|l| l == format!("\"{fname}@{}\":", dep.artifact_url)); + if checksum.is_none() && !already_keyed { + result.warnings.push(RewriteWarning { + code: "redirect_yarn_berry_missing_checksum".into(), + detail: format!( + "{fname}@{} has no yarnBerry10c0 cache checksum", + dep.version + ), + }); + continue; + } + let Some(manifest_obj) = manifest.as_mut().and_then(Value::as_object_mut) else { + result.warnings.push(RewriteWarning { + code: "redirect_yarn_berry_manifest_missing".into(), + detail: format!( + "the root package.json is missing or is not a JSON object; the hosted \ + redirect pins {fname}@{} through its `resolutions` field, so the lock \ + entry is left untouched", + dep.version + ), + }); + continue; + }; + let block = blocks[target_idx].clone(); + // An entry keyed by its tarball URL (an earlier hosted run) recovers + // its ranges from the manifest selectors routed to that URL. + let current_url = if key_ranges.is_empty() { + block + .lines() + .next() + .and_then(|l| l.strip_suffix(':')) + .map(|k| k.trim_matches('"')) + .and_then(split_pattern) + .map(|(_, r)| r.to_string()) + } else { + None + }; + let pin = match berry_resolutions_pin( + manifest_obj, + &fname, + &dep.version, + &key_ranges, + current_url.as_deref(), + &dep.artifact_url, + ) { + Ok(pin) => pin, + Err(warning) => { + result.warnings.push(warning); + continue; + } + }; + // The entry yarn writes for those resolutions: only the key and the + // resolution change (plus our checksum); every other line — version, + // dependencies, bin, languageName — carries over verbatim. + let new_key = format!("\"{fname}@{}\"", dep.artifact_url); + let resolution = format!("{fname}@{}", dep.artifact_url); + let body_lines = block.split_once('\n').map(|(_, rest)| rest).unwrap_or(""); + let mut rewritten = format!("\n{new_key}:\n{body_lines}"); + // `NoExpand`: the URL is literal text, and a `$` in it (a patch + // server path) must never be read as a capture-group reference. + rewritten = resolution_re + .replace( + &rewritten, + NoExpand(&format!("\n resolution: \"{resolution}\"")), + ) + .to_string(); + match &checksum { + Some(checksum) if checksum_re.is_match(&rewritten) => { rewritten = checksum_re - .replace(&rewritten, format!("\n checksum: {checksum}").as_str()) + .replace(&rewritten, NoExpand(&format!("\n checksum: {checksum}"))) .to_string(); - } else { + } + Some(checksum) => { rewritten = resolution_re .replace( &rewritten, - format!("\n resolution: \"{resolution}\"\n checksum: {checksum}") - .as_str(), + NoExpand(&format!( + "\n resolution: \"{resolution}\"\n checksum: {checksum}" + )), ) .to_string(); } - matched_any = true; - if rewritten != *block { - // The ledger records the lock's on-disk bytes (CRLF lines for - // a CRLF lock), so every revert's byte-exact `replacen` - // matches what the file really holds. - result.edits.push(FileEdit { - path: "yarn.lock".into(), - kind: "redirect_yarn_berry_entry".into(), - action: "rewritten".into(), - key: Some(format!("{fname}@{}", dep.version)), - original: Some(Value::String(eol.restore(block).into_owned())), - new: Some(Value::String(eol.restore(&rewritten).into_owned())), - }); - *block = rewritten; - changed = true; - } + None => {} } - if !matched_any && !alias_skipped { - result.warnings.push(RewriteWarning { - code: "redirect_yarn_berry_entry_not_found".into(), - detail: format!("no npm: lock entry resolving {fname}@{}", dep.version), + let rewritten = rewritten[1..].to_string(); + for (selector, original) in pin.apply(manifest_obj, &dep.artifact_url) { + manifest_changed = true; + result.edits.push(FileEdit { + path: BERRY_MANIFEST.into(), + kind: "redirect_yarn_berry_resolution".into(), + action: if original.is_some() { "rewritten" } else { "added" }.into(), + key: Some(selector), + original: original.map(Value::String), + new: Some(Value::String(dep.artifact_url.clone())), }); } + if rewritten != block { + // The ledger records the lock's on-disk bytes (CRLF lines for a + // CRLF lock), so every revert's byte-exact `replacen` matches + // what the file really holds. + result.edits.push(FileEdit { + path: "yarn.lock".into(), + kind: "redirect_yarn_berry_entry".into(), + action: "rewritten".into(), + key: Some(format!("{fname}@{}", dep.version)), + original: Some(Value::String(eol.restore(&block).into_owned())), + new: Some(Value::String(eol.restore(&rewritten).into_owned())), + }); + blocks[target_idx] = rewritten; + moved_keys.push(new_key); + changed = true; + } + result + .confirmed_yarn_berry_uuids + .insert(dep.patch_uuid.clone()); } if changed { - let out = blocks.join("\n\n"); + berry_reposition_blocks(&mut blocks, &moved_keys, was_sorted); + let out = format!("{}{trailing_newlines}", blocks.join("\n\n")); result .files .insert("yarn.lock".into(), format!("{bom}{}", eol.restore(&out))); } + if manifest_changed { + if let (Some(text), Some(value)) = (manifest_text, manifest.as_ref()) { + match crate::vendor::common::JsonLayout::of(text) + .render(value) + .map(String::from_utf8) + { + Ok(Ok(rendered)) => { + result.files.insert(BERRY_MANIFEST.into(), rendered); + } + _ => { + // Unreachable for a parsed object; fail closed anyway: a + // lock pin without its resolutions is not installable. + result.files.remove("yarn.lock"); + result.edits.retain(|e| { + e.kind != "redirect_yarn_berry_entry" + && e.kind != "redirect_yarn_berry_resolution" + }); + for dep in &npm { + result.confirmed_yarn_berry_uuids.remove(&dep.patch_uuid); + } + result.warnings.push(RewriteWarning { + code: "redirect_yarn_berry_manifest_missing".into(), + detail: "the root package.json could not be re-serialized; nothing \ + was redirected" + .into(), + }); + } + } + } + } +} + +/// A berry lock block's sort key: its unquoted key, or `None` for header +/// comment blocks and `__metadata`. +fn berry_sort_key(block: &str) -> Option<&str> { + let first = block.lines().next()?; + if first.starts_with([' ', '\t', '#']) || !first.ends_with(':') { + return None; + } + let key = first[..first.len() - 1].trim_matches('"'); + (key != "__metadata").then_some(key) +} + +/// Whether a berry lock's entries are in yarn's key order (see +/// [`berry_reposition_blocks`]). +pub(crate) fn berry_entries_sorted(blocks: &[String]) -> bool { + let keys: Vec<&str> = blocks.iter().filter_map(|b| berry_sort_key(b)).collect(); + keys.windows(2).all(|w| w[0] <= w[1]) +} + +/// Whether an LF-normalized berry lock holds an entry for `name` at +/// `version`, under any descriptor (npm, tarball, `patch:`, …). +fn berry_lock_locks(content: &str, name: &str, version: &str) -> bool { + use crate::vendor::yarn_classic_lock::{split_berry_key_patterns, split_pattern}; + let version_line = format!("\n version: {version}\n"); + content.split("\n\n").any(|block| { + let Some(key) = block.lines().next().and_then(|l| l.strip_suffix(':')) else { + return false; + }; + if key.starts_with([' ', '\t', '#']) || key == "__metadata" { + return false; + } + format!("{block}\n").contains(&version_line) + && split_berry_key_patterns(key).iter().any(|p| { + split_pattern(p).is_some_and(|(n, range)| { + n == name && berry_npm_alias_target(range).is_none_or(|real| real == name) + }) + }) + }) +} + +/// The package an `npm:@` alias range installs (`None` for a +/// plain `npm:` or any other protocol). +fn berry_npm_alias_target(range: &str) -> Option<&str> { + let body = range.strip_prefix("npm:")?; + crate::vendor::yarn_classic_lock::split_pattern(body).map(|(real, _)| real) +} + +/// The root manifest the yarn berry hosted pin edits. +const BERRY_MANIFEST: &str = "package.json"; + +/// Whether `url` (a URL lock key or `resolutions` value) is a hosted +/// tarball pin socket-patch wrote for `name` (at `version`, when given): +/// this run's own artifact URL, or a URL on the same patch server (scheme, +/// host and port of `artifact_url`) whose leaf names the package version. +/// A user's own tarball for the package — a mirror, a fork — is on another +/// origin and is never ours to rewrite. +fn berry_hosted_pin_is_ours( + url: &str, + name: &str, + version: Option<&str>, + artifact_url: &str, +) -> bool { + if url == artifact_url { + return true; + } + let names_it = match version { + Some(v) => hosted_url::hosted_url_names(url, name, v), + None => hosted_url::hosted_url_version(url, name).is_some(), + }; + let same_origin = match (reqwest::Url::parse(url), reqwest::Url::parse(artifact_url)) { + (Ok(a), Ok(b)) => { + a.scheme() == b.scheme() + && a.host_str().is_some() + && a.host_str() == b.host_str() + && a.port_or_known_default() == b.port_or_known_default() + } + _ => false, + }; + names_it && same_origin } +/// Whether `value` (a `resolutions` value) is a hosted tarball pin written +/// for some version of `name` — ours to rewrite or drop (see +/// [`berry_hosted_pin_is_ours`]). +fn berry_resolution_is_ours(value: &Value, name: &str, artifact_url: &str) -> bool { + value + .as_str() + .is_some_and(|v| berry_hosted_pin_is_ours(v, name, None, artifact_url)) +} + +/// The manifest side of one berry hosted pin, computed by +/// [`berry_resolutions_pin`] and written by [`BerryResolutionsPin::apply`]. +struct BerryResolutionsPin { + /// `@` selectors to route to the hosted tarball, one per + /// descriptor the lock entry carries (yarn's own `npm:` form). + selectors: Vec, + /// Our selectors left by an earlier pin of the same version that no + /// longer name a locked descriptor: dropped. + stale: Vec, +} + +impl BerryResolutionsPin { + /// Write the pin into `manifest`: every selector routed to `url`, stale + /// ones dropped, and an emptied `resolutions` table removed. Returns + /// `(selector, previous value)` for each selector it added or changed. + fn apply( + &self, + manifest: &mut serde_json::Map, + url: &str, + ) -> Vec<(String, Option)> { + let table = manifest + .entry("resolutions".to_string()) + .or_insert_with(|| Value::Object(serde_json::Map::new())); + let Some(table) = table.as_object_mut() else { + return Vec::new(); + }; + for selector in &self.stale { + table.shift_remove(selector); + } + let mut changed = Vec::new(); + for selector in &self.selectors { + let previous = table.get(selector).and_then(Value::as_str).map(str::to_string); + if previous.as_deref() != Some(url) { + table.insert(selector.clone(), Value::String(url.to_string())); + changed.push((selector.clone(), previous)); + } + } + if table.is_empty() { + manifest.shift_remove("resolutions"); + } + changed + } +} + +/// Plan the `resolutions` entries that route `name@version`'s locked +/// descriptors to the hosted tarball. `key_ranges` are the lock entry's npm +/// ranges (`npm:^1.3.0`); for an entry an earlier run already keyed by its +/// tarball URL they are empty and recovered from our selectors routed to +/// `current_url`. +/// +/// Refuses (fail closed, nothing written) when the manifest already carries +/// a user-authored `resolutions` entry for the package — any selector whose +/// target is `name`, bare or scoped — since yarn would apply it alongside +/// (or instead of) ours, and overwriting it would silently change what the +/// user pinned. +fn berry_resolutions_pin( + manifest: &serde_json::Map, + name: &str, + version: &str, + key_ranges: &[String], + current_url: Option<&str>, + artifact_url: &str, +) -> Result { + use crate::vendor::yarn_berry_lock::resolution_selector_target; + let empty = serde_json::Map::new(); + let table = match manifest.get("resolutions") { + None => &empty, + Some(Value::Object(table)) => table, + Some(_) => { + return Err(RewriteWarning { + code: "redirect_yarn_berry_resolutions_conflict".into(), + detail: "package.json `resolutions` is not an object; the hosted redirect \ + will not rewrite it" + .into(), + }) + } + }; + let mut ours: Vec<(&String, &Value)> = Vec::new(); + for (selector, value) in table { + if resolution_selector_target(selector) != Some(name) { + continue; + } + if berry_resolution_is_ours(value, name, artifact_url) { + ours.push((selector, value)); + continue; + } + return Err(RewriteWarning { + code: "redirect_yarn_berry_resolutions_conflict".into(), + detail: format!( + "package.json already has a user-authored resolutions entry for `{selector}` \ + ({value}); the hosted redirect pins {name}@{version} through `resolutions` \ + and will not overwrite it — remove that entry (or use `scan --mode \ + vendored`) and re-run" + ), + }); + } + let selectors: Vec = if key_ranges.is_empty() { + ours.iter() + .filter(|(_, value)| value.as_str().is_some() && value.as_str() == current_url) + .map(|(selector, _)| (*selector).clone()) + .collect() + } else { + key_ranges.iter().map(|r| format!("{name}@{r}")).collect() + }; + if selectors.is_empty() { + return Err(RewriteWarning { + code: "redirect_yarn_berry_resolutions_conflict".into(), + detail: format!( + "yarn.lock pins {name}@{version} to a hosted tarball but package.json has no \ + resolutions entry routing a descriptor to it, so the original descriptor is \ + unknown and neither this run nor `socket-patch rollback` can rebuild the \ + entry — restore yarn.lock and package.json from version control (or delete \ + the entry and run `yarn install`), then re-run" + ), + }); + } + let stale = ours + .iter() + .filter(|(selector, value)| { + !selectors.contains(selector) + && value + .as_str() + .is_some_and(|v| berry_hosted_pin_is_ours(v, name, Some(version), artifact_url)) + }) + .map(|(selector, _)| (*selector).clone()) + .collect(); + Ok(BerryResolutionsPin { selectors, stale }) +} + +/// Move each entry keyed `moved` to where yarn sorts it. Yarn writes lock +/// entries sorted by their (unquoted) key — `__metadata` first — so an entry +/// re-keyed from `name@npm:…` to `name@` can move past a sibling (e.g. +/// `name@npm:7.0.0` now sorts after `name@https://…`); a lock in any other +/// order is rewritten by yarn and fails `--immutable`. Header comment blocks +/// and `__metadata` keep their place; the moved entry is inserted before the +/// first entry whose key sorts after it. A lock that was not in yarn's +/// order before the edit (`was_sorted`, from [`berry_entries_sorted`]; a +/// hand-edited lock) keeps the entry in place, so a pin and its rollback +/// still round-trip byte-exactly. +pub(crate) fn berry_reposition_blocks(blocks: &mut Vec, moved: &[String], was_sorted: bool) { + if !was_sorted { + return; + } + for key in moved { + let line = format!("{key}:"); + let Some(from) = blocks + .iter() + .position(|b| b.lines().next() == Some(line.as_str())) + else { + continue; + }; + let block = blocks.remove(from); + let Some(own) = berry_sort_key(&block).map(str::to_string) else { + blocks.insert(from, block); + continue; + }; + let to = blocks + .iter() + .position(|b| berry_sort_key(b).is_some_and(|k| k > own.as_str())) + .unwrap_or(blocks.len()); + blocks.insert(to, block); + } +} + + // ── bun.lock (text lockfile) ───────────────────────────────────────────────── // A registry 4-tuple `["name@version", "", {deps}, "sha512-…"]` is // rewritten to a URL 3-tuple `["name@", {deps verbatim}, @@ -7502,6 +8026,12 @@ mod tests { } } + /// A minimal root manifest: the berry hosted pin writes its + /// `resolutions` into it. + fn berry_manifest() -> String { + "{\n \"name\": \"app\",\n \"version\": \"1.0.0\"\n}\n".to_string() + } + fn berry_lock(cache_key: &str) -> String { format!( "# header\n\n__metadata:\n version: 8\n cacheKey: {cache_key}\n\n\ @@ -7511,6 +8041,549 @@ mod tests { ) } + /// Files for a berry hosted rewrite: the lock plus the root manifest. + fn berry_files(lock: String, manifest: String) -> BTreeMap { + let mut files = BTreeMap::new(); + files.insert("yarn.lock".to_string(), lock); + files.insert("package.json".to_string(), manifest); + files + } + + const BERRY_UUID: &str = "11111111-1111-4111-8111-111111111111"; + + fn berry_hosted_url(path_name: &str, leaf: &str, version: &str) -> String { + format!("https://patch.socket.dev/patch/npm/{path_name}/{version}/tok/{BERRY_UUID}/{leaf}-{version}.tgz") + } + + /// #404: the hosted pin must never be an `npm:` locator. Yarn fetches an + /// `npm:` locator (`::__archiveUrl=` included) with its npm fetcher, + /// which attaches the npm registry's auth (`npmAuthToken`, + /// `YARN_NPM_AUTH_TOKEN`, `npmScopes`) for every scoped package and, + /// under `npmAlwaysAuth`, for every package — the registry token went to + /// the patch host. A tarball locator under the untouched `npm:` key is + /// rejected by yarn's hardened mode (YN0078, on by default for public PR + /// CI), so the pin is what yarn itself writes for a root `resolutions` + /// entry: a `@npm:` selector routed to the tarball, and the + /// lock entry re-keyed `@`, fetched with the tarball fetcher + /// (no registry auth, identical cache zip and `10c0` checksum). + #[test] + fn yarn_berry_hosted_pin_routes_resolutions_to_a_tarball_entry() { + let checksum = format!("10c0/{}", "7".repeat(128)); + let scoped_url = berry_hosted_url("@isaacs/string-locale-compare", "string-locale-compare", "1.1.0"); + let plain_url = berry_hosted_url("left-pad", "left-pad", "1.3.0"); + let scoped = DepOverride { + namespace: Some("@isaacs".into()), + ..berry_override("string-locale-compare", "1.1.0", &scoped_url, &checksum) + }; + let plain = berry_override("left-pad", "1.3.0", &plain_url, &checksum); + let lock = format!( + "{}\n\"@isaacs/string-locale-compare@npm:^1.1.0\":\n version: 1.1.0\n \ + resolution: \"@isaacs/string-locale-compare@npm:1.1.0\"\n checksum: 10c0/{}\n \ + languageName: node\n linkType: hard\n", + berry_lock("10c0"), + "4".repeat(128) + ); + let files = berry_files(lock, berry_manifest()); + let mut r = RewriteResult::default(); + rewrite_yarn_berry(&files, &[scoped, plain], &mut r); + assert!(r.warnings.is_empty(), "{:?}", r.warnings); + let out = &r.files["yarn.lock"]; + assert!( + out.contains(&format!( + "\n\"@isaacs/string-locale-compare@{scoped_url}\":\n version: 1.1.0\n \ + resolution: \"@isaacs/string-locale-compare@{scoped_url}\"\n checksum: {checksum}\n" + )), + "scoped entry re-keyed to its tarball: {out}" + ); + assert!( + out.contains(&format!( + "\n\"left-pad@{plain_url}\":\n version: 1.3.0\n \ + resolution: \"left-pad@{plain_url}\"\n checksum: {checksum}\n" + )), + "unscoped entry re-keyed to its tarball: {out}" + ); + assert!(!out.contains("__archiveUrl") && !out.contains("@npm:"), "{out}"); + assert!(out.ends_with("linkType: hard\n"), "trailing newline kept: {out:?}"); + let manifest: Value = serde_json::from_str(&r.files["package.json"]).unwrap(); + assert_eq!( + manifest["resolutions"], + json!({ + "@isaacs/string-locale-compare@npm:^1.1.0": scoped_url, + "left-pad@npm:^1.3.0": plain_url, + }), + "{manifest}" + ); + assert_eq!(manifest["name"], "app", "the rest of the manifest is kept"); + assert_eq!( + r.edits.iter().filter(|e| e.kind == "redirect_yarn_berry_entry").count(), + 2 + ); + assert_eq!( + r.edits.iter().filter(|e| e.kind == "redirect_yarn_berry_resolution").count(), + 2 + ); + } + + /// The selectors are descriptor-specific: another locked version of the + /// same package keeps its registry entry, a multi-range key gets one + /// selector per range, and the re-keyed entry moves to where yarn sorts + /// it (`is-number@https://…` sorts before `is-number@npm:7.0.0`), or + /// `yarn install --immutable` would rewrite the lock. + #[test] + fn yarn_berry_pin_is_descriptor_specific_and_resorted() { + let checksum = format!("10c0/{}", "7".repeat(128)); + let url = berry_hosted_url("is-number", "is-number", "6.0.0"); + let ovr = berry_override("is-number", "6.0.0", &url, &checksum); + let lock = format!( + "# header\n\n__metadata:\n version: 8\n cacheKey: 10c0\n\n\ + \"is-number@npm:7.0.0\":\n version: 7.0.0\n resolution: \"is-number@npm:7.0.0\"\n \ + checksum: 10c0/{a}\n languageName: node\n linkType: hard\n\n\ + \"is-number@npm:^6.0.0, is-number@npm:~6.0.0\":\n version: 6.0.0\n \ + resolution: \"is-number@npm:6.0.0\"\n checksum: 10c0/{b}\n languageName: node\n \ + linkType: hard\n\n\ + \"is-odd@npm:3.0.1\":\n version: 3.0.1\n resolution: \"is-odd@npm:3.0.1\"\n \ + dependencies:\n is-number: \"npm:^6.0.0\"\n checksum: 10c0/{c}\n \ + languageName: node\n linkType: hard\n", + a = "1".repeat(128), + b = "2".repeat(128), + c = "3".repeat(128) + ); + let files = berry_files(lock, berry_manifest()); + let mut r = RewriteResult::default(); + rewrite_yarn_berry(&files, std::slice::from_ref(&ovr), &mut r); + assert!(r.warnings.is_empty(), "{:?}", r.warnings); + let out = &r.files["yarn.lock"]; + let keys: Vec<&str> = out + .lines() + .filter(|l| l.starts_with('"')) + .collect(); + assert_eq!( + keys, + vec![ + format!("\"is-number@{url}\":").as_str(), + "\"is-number@npm:7.0.0\":", + "\"is-odd@npm:3.0.1\":", + ], + "{out}" + ); + assert!( + out.contains("resolution: \"is-number@npm:7.0.0\""), + "the other version is untouched: {out}" + ); + assert!( + out.contains(" is-number: \"npm:^6.0.0\""), + "dependents keep their descriptors: {out}" + ); + let manifest: Value = serde_json::from_str(&r.files["package.json"]).unwrap(); + assert_eq!( + manifest["resolutions"], + json!({"is-number@npm:^6.0.0": url, "is-number@npm:~6.0.0": url}), + "{manifest}" + ); + } + + /// A repeat run over its own pin is a no-op; a superseding patch (new + /// uuid) re-pins the tarball-keyed entry and its selectors in place. + #[test] + fn yarn_berry_repeat_run_is_stable_and_a_new_uuid_repins() { + let checksum = format!("10c0/{}", "7".repeat(128)); + let url = berry_hosted_url("left-pad", "left-pad", "1.3.0"); + let ovr = berry_override("left-pad", "1.3.0", &url, &checksum); + let files = berry_files(berry_lock("10c0"), berry_manifest()); + let mut first = RewriteResult::default(); + rewrite_yarn_berry(&files, std::slice::from_ref(&ovr), &mut first); + let pinned = berry_files( + first.files["yarn.lock"].clone(), + first.files["package.json"].clone(), + ); + assert!(first.confirmed_yarn_berry_uuids.contains(BERRY_UUID)); + let mut again = RewriteResult::default(); + rewrite_yarn_berry(&pinned, std::slice::from_ref(&ovr), &mut again); + assert!(again.warnings.is_empty(), "{:?}", again.warnings); + assert!(again.files.is_empty(), "repeat run rewrites nothing: {:?}", again.files); + // A pin already complete is confirmed without a write. + assert!(again.confirmed_yarn_berry_uuids.contains(BERRY_UUID)); + + let new_url = url.replace(BERRY_UUID, "22222222-2222-4222-8222-222222222222"); + let newer = berry_override("left-pad", "1.3.0", &new_url, &checksum); + let mut repin = RewriteResult::default(); + rewrite_yarn_berry(&pinned, std::slice::from_ref(&newer), &mut repin); + assert!(repin.warnings.is_empty(), "{:?}", repin.warnings); + let out = &repin.files["yarn.lock"]; + assert!(out.contains(&format!("\"left-pad@{new_url}\":")), "{out}"); + assert!(!out.contains(BERRY_UUID), "{out}"); + let manifest: Value = serde_json::from_str(&repin.files["package.json"]).unwrap(); + assert_eq!(manifest["resolutions"], json!({"left-pad@npm:^1.3.0": new_url})); + } + + /// The URL-keyed lock entry alone is half a pin: with its manifest + /// `resolutions` routing removed, yarn installs nothing from it, so the + /// rewriter owns the dep yet never confirms it — a URL in the lock must + /// not let hosted confirmation (and the in-run VEX) attest the patch. + #[test] + fn yarn_berry_pin_without_its_routing_is_owned_but_never_confirmed() { + let checksum = format!("10c0/{}", "7".repeat(128)); + let url = berry_hosted_url("left-pad", "left-pad", "1.3.0"); + let ovr = berry_override("left-pad", "1.3.0", &url, &checksum); + let mut first = RewriteResult::default(); + rewrite_yarn_berry( + &berry_files(berry_lock("10c0"), berry_manifest()), + std::slice::from_ref(&ovr), + &mut first, + ); + let orphan = berry_files(first.files["yarn.lock"].clone(), berry_manifest()); + let mut r = RewriteResult::default(); + rewrite_yarn_berry(&orphan, std::slice::from_ref(&ovr), &mut r); + assert!(r.files.is_empty(), "{:?}", r.files); + assert!( + r.warnings + .iter() + .any(|w| w.code == "redirect_yarn_berry_resolutions_conflict"), + "{:?}", + r.warnings + ); + assert!(r.yarn_berry_uuids.contains(BERRY_UUID)); + assert!(r.confirmed_yarn_berry_uuids.is_empty()); + + // A lock that does not lock the version hands the dep back. + let other = berry_override("left-pad", "9.9.9", &url, &checksum); + let mut none = RewriteResult::default(); + rewrite_yarn_berry( + &berry_files(berry_lock("10c0"), berry_manifest()), + std::slice::from_ref(&other), + &mut none, + ); + assert!(none.yarn_berry_uuids.is_empty()); + assert!(none.confirmed_yarn_berry_uuids.is_empty()); + // ... before any per-grant gate: a grant this rewriter cannot use + // (no checksum, an unfetchable URL) still leaves an unlocked + // package to the other lockfiles. + for bad in [ + DepOverride { + integrity: Integrity::default(), + ..other.clone() + }, + berry_override( + "left-pad", + "9.9.9", + "https://patch.socket.dev/x.zip", + &checksum, + ), + ] { + let mut r = RewriteResult::default(); + rewrite_yarn_berry( + &berry_files(berry_lock("10c0"), berry_manifest()), + std::slice::from_ref(&bad), + &mut r, + ); + assert!(r.yarn_berry_uuids.is_empty(), "{:?}", r.warnings); + } + } + + /// A berry lock the preflight refuses (here an unsupported cacheKey) + /// verifies nothing, so it confirms nothing — yet the deps it locks stay + /// the berry rewriter's: an earlier run's URL in the lock must not + /// confirm them through the hosted text probe. + #[test] + fn yarn_berry_preflight_refusal_owns_locked_deps_without_confirming() { + let checksum = format!("10c0/{}", "7".repeat(128)); + let url = berry_hosted_url("left-pad", "left-pad", "1.3.0"); + let ovr = berry_override("left-pad", "1.3.0", &url, &checksum); + let mut first = RewriteResult::default(); + rewrite_yarn_berry( + &berry_files(berry_lock("10c0"), berry_manifest()), + std::slice::from_ref(&ovr), + &mut first, + ); + let refused = berry_files( + first.files["yarn.lock"].replace("cacheKey: 10c0", "cacheKey: 8c0"), + berry_manifest(), + ); + let unlocked = berry_override("right-pad", "1.0.0", &url, &checksum); + let mut r = RewriteResult::default(); + rewrite_yarn_berry(&refused, &[ovr, unlocked], &mut r); + assert_eq!(r.warnings[0].code, "redirect_yarn_berry_cache_unsupported"); + assert!(r.files.is_empty()); + assert_eq!( + r.yarn_berry_uuids.iter().collect::>(), + vec![BERRY_UUID], + "only the locked dep is owned" + ); + assert!(r.confirmed_yarn_berry_uuids.is_empty()); + } + + /// A fork alias key (`left-pad@npm:other@^1.3.0`) installs `other` + /// under the `left-pad` name: even at the patched version it is not the + /// patched package, so it is never re-keyed nor makes the real entry + /// ambiguous. + #[test] + fn yarn_berry_fork_alias_entry_is_never_re_keyed() { + let checksum = format!("10c0/{}", "7".repeat(128)); + let url = berry_hosted_url("left-pad", "left-pad", "1.3.0"); + let ovr = berry_override("left-pad", "1.3.0", &url, &checksum); + let fork = format!( + "\"left-pad@npm:other@^1.3.0\":\n version: 1.3.0\n resolution: \"other@npm:1.3.0\"\n \ + checksum: 10c0/{}\n languageName: node\n linkType: hard\n", + "4".repeat(128) + ); + let fork_only = + format!("# header\n\n__metadata:\n version: 8\n cacheKey: 10c0\n\n{fork}"); + let mut r = RewriteResult::default(); + rewrite_yarn_berry( + &berry_files(fork_only, berry_manifest()), + std::slice::from_ref(&ovr), + &mut r, + ); + assert!(r.files.is_empty(), "{:?}", r.files); + assert_eq!(r.warnings[0].code, "redirect_yarn_berry_entry_not_found"); + assert!(r.yarn_berry_uuids.is_empty()); + + // Beside the real entry: only the real one is pinned. + let both = format!("{}\n{fork}", berry_lock("10c0")); + let mut r = RewriteResult::default(); + rewrite_yarn_berry( + &berry_files(both, berry_manifest()), + std::slice::from_ref(&ovr), + &mut r, + ); + assert!(r.warnings.is_empty(), "{:?}", r.warnings); + let out = &r.files["yarn.lock"]; + assert!(out.contains(&format!("\"left-pad@{url}\":")), "{out}"); + assert!( + out.contains(&fork), + "the fork entry is byte-identical: {out}" + ); + assert!(r.confirmed_yarn_berry_uuids.contains(BERRY_UUID)); + } + + /// The artifact URL is spliced as literal text: a `$` in it (legal in a + /// URL path) must not be expanded as a regex capture reference. + #[test] + fn yarn_berry_artifact_url_dollar_is_written_literally() { + let checksum = format!("10c0/{}", "7".repeat(128)); + let url = "https://patch.socket.dev/patch/npm/left-pad/1.3.0/t$0k$1/u/left-pad-1.3.0.tgz"; + let ovr = berry_override("left-pad", "1.3.0", url, &checksum); + let mut r = RewriteResult::default(); + rewrite_yarn_berry( + &berry_files(berry_lock("10c0"), berry_manifest()), + std::slice::from_ref(&ovr), + &mut r, + ); + assert!(r.warnings.is_empty(), "{:?}", r.warnings); + let out = &r.files["yarn.lock"]; + assert!( + out.contains(&format!( + "\"left-pad@{url}\":\n version: 1.3.0\n resolution: \"left-pad@{url}\"\n \ + checksum: {checksum}\n" + )), + "{out}" + ); + // ... and the checksum-line insertion path, for a lock without one. + let no_checksum_line = + berry_lock("10c0").replace(&format!(" checksum: 10c0/{}\n", "3".repeat(128)), ""); + let mut r = RewriteResult::default(); + rewrite_yarn_berry( + &berry_files(no_checksum_line, berry_manifest()), + std::slice::from_ref(&ovr), + &mut r, + ); + let out = &r.files["yarn.lock"]; + assert!( + out.contains(&format!( + " resolution: \"left-pad@{url}\"\n checksum: {checksum}\n" + )), + "{out}" + ); + } + + /// A rescan whose grant lacks the `yarnBerry10c0` checksum still + /// confirms a pin an earlier run completed (the lock already holds the + /// checksum it was written with); an entry that would need the checksum + /// written is owned and refused, never confirmed. + #[test] + fn yarn_berry_checksumless_grant_keeps_a_complete_pin_only() { + let checksum = format!("10c0/{}", "7".repeat(128)); + let url = berry_hosted_url("left-pad", "left-pad", "1.3.0"); + let ovr = berry_override("left-pad", "1.3.0", &url, &checksum); + let mut first = RewriteResult::default(); + rewrite_yarn_berry( + &berry_files(berry_lock("10c0"), berry_manifest()), + std::slice::from_ref(&ovr), + &mut first, + ); + let pinned = berry_files( + first.files["yarn.lock"].clone(), + first.files["package.json"].clone(), + ); + let checksumless = DepOverride { + integrity: Integrity::default(), + ..ovr.clone() + }; + let mut again = RewriteResult::default(); + rewrite_yarn_berry(&pinned, std::slice::from_ref(&checksumless), &mut again); + assert!(again.warnings.is_empty(), "{:?}", again.warnings); + assert!(again.files.is_empty(), "{:?}", again.files); + assert!(again.confirmed_yarn_berry_uuids.contains(BERRY_UUID)); + + let mut fresh = RewriteResult::default(); + rewrite_yarn_berry( + &berry_files(berry_lock("10c0"), berry_manifest()), + std::slice::from_ref(&checksumless), + &mut fresh, + ); + assert_eq!( + fresh.warnings[0].code, + "redirect_yarn_berry_missing_checksum" + ); + assert!(fresh.files.is_empty()); + assert!(fresh.yarn_berry_uuids.contains(BERRY_UUID)); + assert!(fresh.confirmed_yarn_berry_uuids.is_empty()); + } + + /// A lock written by an earlier release carries the old + /// `npm:…::__archiveUrl=` pin; a repeat hosted run re-pins it (#404). + #[test] + fn yarn_berry_legacy_archive_url_pin_is_migrated() { + let checksum = format!("10c0/{}", "7".repeat(128)); + let url = berry_hosted_url("left-pad", "left-pad", "1.3.0"); + let ovr = berry_override("left-pad", "1.3.0", &url, &checksum); + let legacy = berry_lock("10c0").replace( + "resolution: \"left-pad@npm:1.3.0\"", + &format!( + "resolution: \"left-pad@npm:1.3.0::__archiveUrl={}\"", + crate::utils::uri::encode_uri_component(&url) + ), + ); + let files = berry_files(legacy, berry_manifest()); + let mut r = RewriteResult::default(); + rewrite_yarn_berry(&files, std::slice::from_ref(&ovr), &mut r); + assert!(r.warnings.is_empty(), "{:?}", r.warnings); + let out = &r.files["yarn.lock"]; + assert!( + out.contains(&format!("\"left-pad@{url}\":\n version: 1.3.0\n resolution: \"left-pad@{url}\"\n")), + "{out}" + ); + assert!(!out.contains("__archiveUrl"), "{out}"); + assert!(r.files["package.json"].contains("\"left-pad@npm:^1.3.0\"")); + } + + /// The pin never overwrites a user-authored `resolutions` entry for the + /// package (bare, ranged or nested), never runs without a root + /// manifest, and leaves a package yarn also locks through a builtin + /// `patch:` entry alone (that entry wraps the same npm descriptor). + /// Each is a skip with a warning, nothing written. + #[test] + fn yarn_berry_pin_refusals_leave_everything_untouched() { + let checksum = format!("10c0/{}", "7".repeat(128)); + let url = berry_hosted_url("left-pad", "left-pad", "1.3.0"); + let ovr = berry_override("left-pad", "1.3.0", &url, &checksum); + for (label, selector) in [ + ("bare", "left-pad"), + ("ranged", "left-pad@npm:^1.3.0"), + ("nested", "app/left-pad"), + ] { + let manifest = format!( + "{{\n \"name\": \"app\",\n \"resolutions\": {{\n \"{selector}\": \"1.3.0\"\n }}\n}}\n" + ); + let mut r = RewriteResult::default(); + rewrite_yarn_berry(&berry_files(berry_lock("10c0"), manifest), std::slice::from_ref(&ovr), &mut r); + assert!(r.files.is_empty(), "{label}: {:?}", r.files); + assert_eq!( + r.warnings.iter().map(|w| w.code.as_str()).collect::>(), + vec!["redirect_yarn_berry_resolutions_conflict"], + "{label}" + ); + } + // A user's own tarball for the package — same `-.tgz` + // leaf, another origin (a mirror, a fork) — is user-authored too. + let manifest = "{\n \"name\": \"app\",\n \"resolutions\": {\n \ + \"left-pad@npm:^1.3.0\": \"https://mirror.example/left-pad-1.3.0.tgz\"\n }\n}\n"; + let mut r = RewriteResult::default(); + rewrite_yarn_berry( + &berry_files(berry_lock("10c0"), manifest.into()), + std::slice::from_ref(&ovr), + &mut r, + ); + assert!(r.files.is_empty(), "mirror tarball: {:?}", r.files); + assert_eq!( + r.warnings + .iter() + .map(|w| w.code.as_str()) + .collect::>(), + vec!["redirect_yarn_berry_resolutions_conflict"], + "mirror tarball" + ); + // An unrelated user entry is kept as-is next to ours. + let manifest = "{\n \"name\": \"app\",\n \"resolutions\": {\n \"other\": \"2.0.0\"\n }\n}\n"; + let mut r = RewriteResult::default(); + rewrite_yarn_berry(&berry_files(berry_lock("10c0"), manifest.into()), std::slice::from_ref(&ovr), &mut r); + assert!(r.warnings.is_empty(), "{:?}", r.warnings); + let m: Value = serde_json::from_str(&r.files["package.json"]).unwrap(); + assert_eq!(m["resolutions"], json!({"other": "2.0.0", "left-pad@npm:^1.3.0": url})); + + let mut files = BTreeMap::new(); + files.insert("yarn.lock".to_string(), berry_lock("10c0")); + let mut r = RewriteResult::default(); + rewrite_yarn_berry(&files, std::slice::from_ref(&ovr), &mut r); + assert!(r.files.is_empty(), "{:?}", r.files); + assert_eq!( + r.warnings.iter().map(|w| w.code.as_str()).collect::>(), + vec!["redirect_yarn_berry_manifest_missing"] + ); + + let with_patch = format!( + "{}\n\"left-pad@patch:left-pad@npm%3A^1.3.0#~builtin\":\n \ + version: 1.3.0\n resolution: \"left-pad@patch:left-pad@npm%3A1.3.0#~builtin::version=1.3.0&hash=abc\"\n \ + languageName: node\n linkType: hard\n", + berry_lock("10c0") + ); + let mut r = RewriteResult::default(); + rewrite_yarn_berry(&berry_files(with_patch, berry_manifest()), std::slice::from_ref(&ovr), &mut r); + assert!(r.files.is_empty(), "{:?}", r.files); + let codes: Vec<&str> = r.warnings.iter().map(|w| w.code.as_str()).collect(); + assert!(codes.contains(&"redirect_yarn_berry_shared_descriptor"), "{codes:?}"); + } + + /// Yarn routes a URL locator to its tarball fetcher only when it is an + /// `http(s)://` URL whose path ends in `.tgz`/`.tar.gz` with no query + /// (yarn's `TARBALL_REGEXP`). Any other artifact URL would make yarn + /// reject the lock, so the entry is refused and left byte-identical. + #[test] + fn yarn_berry_refuses_artifact_url_yarn_cannot_fetch_as_tarball() { + let checksum = format!("10c0/{}", "7".repeat(128)); + for url in [ + "https://p.test/left-pad-1.3.0.zip", + "https://p.test/left-pad-1.3.0.tgz?sig=1", + "https://p.test/left-pad-1.3.0.tgz#frag", + "ftp://p.test/left-pad-1.3.0.tgz", + "https://p.test/a b/left-pad-1.3.0.tgz", + "https://p.test/a\"b/left-pad-1.3.0.tgz", + ] { + let ovr = berry_override("left-pad", "1.3.0", url, &checksum); + let files = berry_files(berry_lock("10c0"), berry_manifest()); + let mut r = RewriteResult::default(); + rewrite_yarn_berry(&files, std::slice::from_ref(&ovr), &mut r); + assert!(r.files.is_empty(), "{url}: nothing written"); + assert!(r.edits.is_empty(), "{url}: {:?}", r.edits); + assert_eq!( + r.warnings.iter().map(|w| w.code.as_str()).collect::>(), + vec!["redirect_yarn_berry_artifact_url_unsupported"], + "{url}" + ); + } + for url in [ + "https://p.test/left-pad-1.3.0.tgz", + "http://127.0.0.1:8080/patch/npm/@s/n/1.0.0/t/u/n-1.0.0.tar.gz", + ] { + let ovr = berry_override("left-pad", "1.3.0", url, &checksum); + let files = berry_files(berry_lock("10c0"), berry_manifest()); + let mut r = RewriteResult::default(); + rewrite_yarn_berry(&files, std::slice::from_ref(&ovr), &mut r); + assert!(r.warnings.is_empty(), "{url}: {:?}", r.warnings); + assert!(r.files.contains_key("yarn.lock"), "{url}"); + } + } + /// yarn 4.0.x: a lock that spells its `10c0` checksums bare (yarn /// 4.0.0–4.0.2) gets the hosted entry's checksum spelled bare — the /// API's prefixed `yarnBerry10c0` would make `yarn install --immutable` @@ -7533,10 +8606,14 @@ mod tests { ] { let mut files = BTreeMap::new(); files.insert("yarn.lock".to_string(), lock.clone()); + files.insert("package.json".to_string(), berry_manifest()); let mut r = RewriteResult::default(); rewrite_yarn_berry(&files, std::slice::from_ref(&ovr), &mut r); let out = &r.files["yarn.lock"]; - assert!(out.contains("::__archiveUrl="), "{out}"); + assert!( + out.contains("resolution: \"left-pad@http://p.test/lp.tgz\""), + "{out}" + ); assert!(out.contains(&want), "want {want:?} in:\n{out}"); assert_eq!(out.matches("checksum:").count(), 1, "{out}"); } @@ -14502,9 +15579,16 @@ packages: let lf = berry_lock_two_entries(); let mut lf_files = BTreeMap::new(); lf_files.insert("yarn.lock".to_string(), lf.clone()); + lf_files.insert("package.json".to_string(), berry_manifest()); let mut lf_result = RewriteResult::default(); rewrite_yarn_berry(&lf_files, std::slice::from_ref(&ovr), &mut lf_result); let lf_out = lf_result.files["yarn.lock"].clone(); + let lf_edit = lf_result + .edits + .iter() + .find(|e| e.path == "yarn.lock") + .expect("the LF lock edit") + .clone(); for (label, bom, crlf) in [ ("crlf", "", true), @@ -14522,6 +15606,7 @@ packages: let input = respell(&lf); let mut files = BTreeMap::new(); files.insert("yarn.lock".to_string(), input.clone()); + files.insert("package.json".to_string(), berry_manifest()); let mut r = RewriteResult::default(); rewrite_yarn_berry(&files, std::slice::from_ref(&ovr), &mut r); assert!(r.warnings.is_empty(), "{label}: {:?}", r.warnings); @@ -14544,13 +15629,14 @@ packages: "{label}: BOM kept" ); assert!( - out.contains(&crate::utils::uri::encode_uri_component(url)), + out.contains(&format!("resolution: \"left-pad@{url}\"")), "{label}: {out}" ); // One edit; its fragments are the on-disk bytes of the entry. - assert_eq!(r.edits.len(), 1, "{label}"); - let edit = &r.edits[0]; + let lock_edits: Vec<&FileEdit> = r.edits.iter().filter(|e| e.path == "yarn.lock").collect(); + assert_eq!(lock_edits.len(), 1, "{label}"); + let edit = lock_edits[0]; let (orig, new) = ( edit.original.as_ref().and_then(Value::as_str).unwrap(), edit.new.as_ref().and_then(Value::as_str).unwrap(), @@ -14559,7 +15645,7 @@ packages: (orig, new), ( respell( - lf_result.edits[0] + lf_edit .original .as_ref() .unwrap() @@ -14567,7 +15653,7 @@ packages: .unwrap() ) .trim_start_matches('\u{feff}'), - respell(lf_result.edits[0].new.as_ref().unwrap().as_str().unwrap()) + respell(lf_edit.new.as_ref().unwrap().as_str().unwrap()) .trim_start_matches('\u{feff}'), ), "{label}: fragments in the lock's on-disk form" @@ -14586,6 +15672,7 @@ packages: // Re-run over the rewritten lock: nothing to do. let mut files = BTreeMap::new(); files.insert("yarn.lock".to_string(), out.clone()); + files.insert("package.json".to_string(), r.files["package.json"].clone()); let mut again = RewriteResult::default(); rewrite_yarn_berry(&files, std::slice::from_ref(&ovr), &mut again); assert!( @@ -14712,11 +15799,15 @@ packages: assert!(is_berry_lock(&lock)); let mut files = BTreeMap::new(); files.insert("yarn.lock".to_string(), lock); + files.insert("package.json".to_string(), berry_manifest()); let r = rewrite_registry_redirect(&files, std::slice::from_ref(&ovr)); assert!(r.warnings.is_empty(), "{:?}", r.warnings); let out = &r.files["yarn.lock"]; assert!(out.starts_with("\u{feff}__metadata:"), "{out:?}"); - assert!(out.contains("::__archiveUrl="), "{out}"); + assert!( + out.contains("resolution: \"left-pad@http://p.test/lp.tgz\""), + "{out}" + ); } /// CRLF locks preserve their newline style through hosted rewriting. @@ -16486,7 +17577,7 @@ packages: /// An UNQUOTED single-descriptor berry key (yarn emits unquoted keys for /// names that need no YAML quoting) whose entry has no `checksum:` line: - /// the resolution gains `::__archiveUrl=` and a checksum line is INSERTED + /// the resolution becomes the tarball locator and a checksum line is INSERTED /// after it. #[test] fn yarn_berry_unquoted_key_without_checksum_gains_inserted_line() { @@ -16497,18 +17588,19 @@ packages: resolution: \"left-pad@npm:1.3.0\"\n languageName: node\n linkType: hard\n"; let mut files = BTreeMap::new(); files.insert("yarn.lock".to_string(), lock.to_string()); + files.insert("package.json".to_string(), berry_manifest()); let r = rewrite_registry_redirect(&files, std::slice::from_ref(&ovr)); let out = r.files.get("yarn.lock").expect("lock rewritten"); assert!( - out.contains("\n resolution: \"left-pad@npm:1.3.0::__archiveUrl="), - "resolution gains the archiveUrl binding: {out}" + out.contains("\n resolution: \"left-pad@http://p.test/lp.tgz\""), + "resolution becomes the tarball locator: {out}" ); assert!( out.contains(&format!("\"\n checksum: {checksum}\n languageName: node")), "checksum inserted right after the resolution: {out}" ); assert!(r.warnings.is_empty(), "{:?}", r.warnings); - assert_eq!(r.edits.len(), 1); + assert_eq!(r.edits.iter().filter(|e| e.path == "yarn.lock").count(), 1); } /// A bun URL 3-tuple already at the CURRENT artifact URL but with a stale @@ -17529,6 +18621,7 @@ packages: let ovr = berry_override("left-pad", "1.3.0", "http://p.test/lp.tgz", &checksum); let mut files = BTreeMap::new(); files.insert("yarn.lock".to_string(), berry_lock("10c0")); + files.insert("package.json".to_string(), berry_manifest()); files.insert( ".yarnrc.yml".to_string(), "compressionLevel: 0\n".to_string(), @@ -17541,7 +18634,7 @@ packages: .get("yarn.lock") .expect("explicit level 0 must not refuse"); assert!( - out.contains("__archiveUrl=") && out.contains(&checksum), + out.contains("left-pad@http://p.test/lp.tgz") && out.contains(&checksum), "{out}" ); } @@ -17561,6 +18654,7 @@ packages: ); let mut files = BTreeMap::new(); files.insert("yarn.lock".to_string(), lock); + files.insert("package.json".to_string(), berry_manifest()); let mut r = RewriteResult::default(); rewrite_yarn_berry(&files, std::slice::from_ref(&ovr), &mut r); assert!(r.warnings.is_empty(), "{:?}", r.warnings); @@ -17575,12 +18669,12 @@ packages: "the rangeless key stays byte-identical: {out}" ); assert_eq!( - r.edits.len(), + r.edits.iter().filter(|e| e.path == "yarn.lock").count(), 1, "only the real entry is edited: {:?}", r.edits ); - assert!(out.contains("__archiveUrl="), "{out}"); + assert!(out.contains("left-pad@http://p.test/lp.tgz"), "{out}"); } /// A descriptor with NO protocol at all (`left-pad@1.3.0`) is refused diff --git a/crates/socket-patch-core/src/patch/redirect/upstream/npm.rs b/crates/socket-patch-core/src/patch/redirect/upstream/npm.rs index 65d590299..fe1938991 100644 --- a/crates/socket-patch-core/src/patch/redirect/upstream/npm.rs +++ b/crates/socket-patch-core/src/patch/redirect/upstream/npm.rs @@ -372,16 +372,18 @@ async fn restore_berry( ctx: &Ctx<'_>, result: &mut FormatResult, ) { + use crate::vendor::yarn_berry_lock::resolution_selector_target; use crate::vendor::yarn_classic_lock::{split_berry_key_patterns, split_pattern}; let (bom, body) = match raw.strip_prefix('\u{feff}') { Some(rest) => ("\u{feff}", rest), None => ("", raw), }; - let yarnrc_rel = match rel.rsplit_once('/') { - Some((dir, _)) => format!("{dir}/.yarnrc.yml"), - None => ".yarnrc.yml".to_string(), + let dir_prefix = match rel.rsplit_once('/') { + Some((dir, _)) => format!("{dir}/"), + None => String::new(), }; + let yarnrc_rel = format!("{dir_prefix}.yarnrc.yml"); let yarnrc = view.read(&yarnrc_rel).await.ok().flatten(); if let Err(w) = super::super::preflight_yarn_berry_hosted(raw, yarnrc.as_deref()) { refuse_all_in(pins, rel, result, w.detail); @@ -389,7 +391,10 @@ async fn restore_berry( } let eol = LineEndings::of(body); let content = to_lf(body).into_owned(); - let mut blocks: Vec = content.split("\n\n").map(String::from).collect(); + let trimmed = content.trim_end_matches('\n'); + let trailing_newlines = content[trimmed.len()..].to_string(); + let mut blocks: Vec = trimmed.split("\n\n").map(String::from).collect(); + let was_sorted = super::super::berry_entries_sorted(&blocks); let resolution_re = Regex::new(r#"\n {2}resolution: "([^"]*)""#) .expect("static resolution-line regex is valid"); let checksum_re = @@ -397,15 +402,45 @@ async fn restore_berry( let version_re = Regex::new(r"\n {2}version: ([^\n]*)").expect("static version-line regex is valid"); - let mut hits: Vec<(usize, String, String, String)> = Vec::new(); + // The root manifest: a hosted pin keyed by its tarball URL keeps the + // descriptors it replaced only as `resolutions` selectors routed there. + let pkg_rel = format!("{dir_prefix}package.json"); + let pkg_text = view.read(&pkg_rel).await.ok().flatten(); + let mut pkg: Option = pkg_text + .as_deref() + .and_then(|t| serde_json::from_str(t.strip_prefix('\u{feff}').unwrap_or(t)).ok()) + .filter(serde_json::Value::is_object); + let mut pkg_changed = false; + + // `(block index, uuid, name, version, restored key or None to keep it, + // selectors to drop)`. + struct Hit { + idx: usize, + uuid: String, + name: String, + version: String, + key: Option, + selectors: Vec, + } + let mut hits: Vec = Vec::new(); for (i, block) in blocks.iter().enumerate() { let Some(resolution) = resolution_re.captures(block).map(|c| c[1].to_string()) else { continue; }; - let Some((_, archive)) = resolution.split_once("::__archiveUrl=") else { + // The hosted pin is the tarball-URL locator `name@`; locks + // pinned by releases up to 5.0 spell it as an `npm:` locator's + // percent-encoded `::__archiveUrl=` binding (#404). + let Some((_, reference)) = split_pattern(&resolution) else { + continue; + }; + let url_pin = reference.starts_with("https://") || reference.starts_with("http://"); + let archive = if url_pin { + reference + } else if let Some((_, binding)) = reference.split_once("::__archiveUrl=") { + binding.split('&').next().unwrap_or(binding) + } else { continue; }; - let archive = archive.split('&').next().unwrap_or(archive); let Some(uuid) = ctx.hosted_uuid(archive) else { continue; }; @@ -425,16 +460,80 @@ async fn restore_berry( let version = version_re .captures(block) .map(|c| c[1].trim().trim_matches('"').to_string()); - match (names.len(), names.into_iter().next(), version) { - (1, Some(name), Some(version)) => hits.push((i, uuid, name.to_string(), version)), - _ => result.refuse( + let (Some(name), Some(version), 1) = (names.iter().next(), version, names.len()) else { + result.refuse( &uuid, format!("a {rel} entry wiring it names no single package and version"), - ), - } + ); + continue; + }; + // An entry keyed by the tarball descriptor itself takes back the + // descriptors its `resolutions` selectors route to that URL. + let keyed_by_url = matches!( + patterns.as_slice(), + [only] if split_pattern(only).is_some_and(|(_, r)| r == reference) + ); + let (restored_key, selectors) = if keyed_by_url { + let selectors: Vec = pkg + .as_ref() + .and_then(|p| p.get("resolutions")) + .and_then(serde_json::Value::as_object) + .map(|table| { + table + .iter() + .filter(|(sel, value)| { + resolution_selector_target(sel) == Some(name.as_str()) + && value.as_str() == Some(reference) + }) + .map(|(sel, _)| sel.clone()) + .collect() + }) + .unwrap_or_default(); + let mut descriptors: Vec = selectors + .iter() + .filter(|sel| { + split_pattern(sel).is_some_and(|(n, r)| n == name && r.starts_with("npm:")) + }) + .cloned() + .collect(); + if descriptors.is_empty() { + result.refuse( + &uuid, + format!( + "{pkg_rel} has no resolutions entry routing a {name} descriptor to the \ + hosted tarball, so the {rel} entry's original key cannot be rebuilt — \ + restore {rel} and {pkg_rel} from version control (or delete the entry \ + and run `yarn install`)" + ), + ); + continue; + } + descriptors.sort(); + descriptors.dedup(); + (Some(format!("\"{}\"", descriptors.join(", "))), selectors) + } else { + (None, Vec::new()) + }; + hits.push(Hit { + idx: i, + uuid, + name: name.clone(), + version, + key: restored_key, + selectors, + }); } let mut changed = false; - for (i, uuid, name, version) in hits { + let mut moved: Vec = Vec::new(); + for Hit { + idx, + uuid, + name, + version, + key, + selectors, + } in hits + { if result.refused.contains_key(&uuid) { continue; } @@ -459,20 +558,69 @@ async fn restore_berry( }; let resolution = format!("\n resolution: \"{name}@npm:{version}\"").replace('$', "$$"); let mut block = resolution_re - .replace(&blocks[i], resolution.as_str()) + .replace(&blocks[idx], resolution.as_str()) .into_owned(); if checksum_re.is_match(&block) { block = checksum_re .replace(&block, format!("\n checksum: {checksum}").as_str()) .into_owned(); } - blocks[i] = block; + if let Some(key) = key { + let body_lines = block + .split_once('\n') + .map(|(_, r)| r.to_string()) + .unwrap_or_default(); + block = format!("{key}:\n{body_lines}"); + moved.push(key); + } + blocks[idx] = block; + if !selectors.is_empty() { + if let Some(table) = pkg + .as_mut() + .and_then(serde_json::Value::as_object_mut) + .and_then(|obj| obj.get_mut("resolutions")) + .and_then(serde_json::Value::as_object_mut) + { + for selector in &selectors { + table.shift_remove(selector); + } + pkg_changed = true; + } + } result.handled.insert(uuid); changed = true; } - if changed { - view.write(rel, format!("{bom}{}", eol.restore(&blocks.join("\n\n")))); + if !changed { + return; + } + if pkg_changed { + if let (Some(text), Some(value)) = (pkg_text.as_deref(), pkg.as_mut()) { + if let Some(obj) = value.as_object_mut() { + if obj + .get("resolutions") + .and_then(serde_json::Value::as_object) + .is_some_and(serde_json::Map::is_empty) + { + obj.shift_remove("resolutions"); + } + } + match crate::vendor::common::JsonLayout::of(text) + .render(value) + .map(String::from_utf8) + { + Ok(Ok(rendered)) => view.write(&pkg_rel, rendered), + _ => { + for uuid in pins.keys() { + result.refuse(uuid, format!("{pkg_rel} could not be re-serialized")); + } + return; + } + } + } } + super::super::berry_reposition_blocks(&mut blocks, &moved, was_sorted); + let out = format!("{}{trailing_newlines}", blocks.join("\n\n")); + view.write(rel, format!("{bom}{}", eol.restore(&out))); } // ── pnpm-lock.yaml ─────────────────────────────────────────────────────────── diff --git a/crates/socket-patch-core/src/vendor/lock_inventory/tests.rs b/crates/socket-patch-core/src/vendor/lock_inventory/tests.rs index e43f5d85a..e6392c30a 100644 --- a/crates/socket-patch-core/src/vendor/lock_inventory/tests.rs +++ b/crates/socket-patch-core/src/vendor/lock_inventory/tests.rs @@ -998,6 +998,43 @@ async fn yarn_berry_registry_resolutions_inventory_with_checksums() { assert!(!entries.iter().any(|e| e.name == "fixture"), "{entries:?}"); } +/// #404: a hosted berry pin — keyed by its tarball descriptor (the +/// `resolutions` pin) or, from an earlier release, under the untouched +/// `npm:` key — is still the registry package, so lock-only discovery keeps +/// inventorying it (and a later hosted scan can re-pin it). A user's own URL +/// dependency whose tarball does not name a package version stays out. +#[tokio::test] +async fn yarn_berry_hosted_tarball_pin_stays_in_the_inventory() { + let tmp = tempfile::tempdir().unwrap(); + let hosted = YARN_BERRY + .replace( + "resolution: \"left-pad@npm:1.3.0\"", + "resolution: \"left-pad@https://patch.socket.dev/patch/npm/left-pad/1.3.0/t/u/left-pad-1.3.0.tgz\"", + ) + .replace( + "\"@scope/pkg@npm:^2.0.0\":\n version: 2.0.0\n resolution: \"@scope/pkg@npm:2.0.0\"", + "\"@scope/pkg@https://patch.socket.dev/patch/npm/@scope/pkg/2.0.0/t/u/pkg-2.0.0.tgz\":\n \ + version: 2.0.0\n \ + resolution: \"@scope/pkg@https://patch.socket.dev/patch/npm/@scope/pkg/2.0.0/t/u/pkg-2.0.0.tgz\"", + ) + + "\n\"own@https://example.test/own-latest.tgz\":\n version: 1.0.0\n \ + resolution: \"own@https://example.test/own-latest.tgz\"\n \ + checksum: 10c0/own==\n languageName: node\n linkType: hard\n"; + assert!(hosted.contains("\"@scope/pkg@https://"), "{hosted}"); + write(tmp.path(), "yarn.lock", &hosted).await; + + let (flavor, entries) = inventory_npm_lock(tmp.path()).await.unwrap().unwrap(); + assert_eq!(flavor, NpmLockFlavor::YarnBerry); + let lp = entry(&entries, "left-pad"); + assert_eq!(lp.version, "1.3.0"); + assert_eq!( + lp.integrity, + LockIntegrity::BerryChecksum("10c0/deadbeefcafe==".into()) + ); + assert_eq!(entry(&entries, "@scope/pkg").version, "2.0.0"); + assert!(!entries.iter().any(|e| e.name == "own"), "{entries:?}"); +} + /// yarn berry writes a CRLF `yarn.lock` on Windows (a new lockfile gets /// `os.EOL`), and editors add a BOM: the Windows spellings — header-less /// too — inventory exactly like the LF lock, with no stray `\r` riding into diff --git a/crates/socket-patch-core/src/vendor/lock_inventory/yarn.rs b/crates/socket-patch-core/src/vendor/lock_inventory/yarn.rs index 7f2ccc2b1..065214aa1 100644 --- a/crates/socket-patch-core/src/vendor/lock_inventory/yarn.rs +++ b/crates/socket-patch-core/src/vendor/lock_inventory/yarn.rs @@ -187,6 +187,29 @@ pub(super) async fn inventory_yarn_berry_in(view: &ProjectView<'_>) -> Option bool { + use crate::vendor::yarn_classic_lock::split_pattern; + crate::patch::redirect::hosted_url::hosted_url_names(reference, name, version) + && !entry.patterns.is_empty() + && entry.patterns.iter().all(|p| { + split_pattern(p) + .is_some_and(|(_, range)| range.starts_with("npm:") || range == reference) + }) +} + fn berry_registry_view(text: &str) -> Vec { let mut out = Vec::new(); for entry in berry_entries(text).entries { @@ -194,16 +217,29 @@ fn berry_registry_view(text: &str) -> Vec { continue; } // Registry resolutions are `name@npm:` (a `::binding` - // suffix may follow). Anything else (workspace:/patch:/file:/link:) - // is skipped — including our own vendored file: resolutions. + // suffix may follow). A Socket hosted pin is a tarball-URL locator + // (`name@https://…/-.tgz`, #404) — still the registry + // package, just fetched from the patch host (see + // [`berry_hosted_tarball_entry`]). Anything else (workspace:/patch:/ + // file:/link:, a user's own URL dependency) is skipped — including + // our own vendored file: resolutions. let Some(locator) = entry.locator() else { continue; }; - let Some((version_from_res, _)) = locator.npm() else { - continue; - }; let lines = &entry.block.lines; - let version = berry_field(lines, "version").unwrap_or(version_from_res); + let version = match locator.npm() { + Some((version_from_res, _)) => { + berry_field(lines, "version").unwrap_or(version_from_res) + } + None => match berry_field(lines, "version") { + Some(v) + if berry_hosted_tarball_entry(&entry, locator.name, v, locator.reference) => + { + v + } + _ => continue, + }, + }; let integrity = berry_field(lines, "checksum") .map(|c| LockIntegrity::BerryChecksum(c.to_string())) .unwrap_or(LockIntegrity::None); diff --git a/crates/socket-patch-core/src/vendor/yarn_berry_lock.rs b/crates/socket-patch-core/src/vendor/yarn_berry_lock.rs index c051e38cc..39c12a814 100644 --- a/crates/socket-patch-core/src/vendor/yarn_berry_lock.rs +++ b/crates/socket-patch-core/src/vendor/yarn_berry_lock.rs @@ -1281,7 +1281,7 @@ fn carried_sections(lines: &[String]) -> Vec { /// cache zip, but a `--immutable` install treats a respelled checksum as a /// lockfile modification (YN0028: "The lockfile would have been modified by /// this install") — so an entry Socket writes (the vendored `file:` entry, -/// the hosted `__archiveUrl` rewrite) must follow the lock's own spelling or +/// the hosted tarball-locator rewrite) must follow the lock's own spelling or /// every CI install of a yarn 4.0.x project fails. A lock with no checksum /// at all keeps the prefixed form (every yarn since 4.1). pub(crate) fn lock_spells_bare_checksums(lock_text: &str) -> bool { @@ -1392,7 +1392,8 @@ impl<'a> BerryLocator<'a> { } /// The `__archiveUrl=` binding of a registry locator (bindings are - /// `&`-joined), still percent-encoded — what the hosted redirect writes. + /// `&`-joined), still percent-encoded — what hosted redirects up to 5.0 + /// wrote (and what yarn itself writes for a custom registry). pub(crate) fn archive_url(&self) -> Option<&'a str> { self.npm()? .1 diff --git a/crates/socket-patch-core/src/vendor/yarn_layering_tests.rs b/crates/socket-patch-core/src/vendor/yarn_layering_tests.rs index f2f7edf5f..dd11192c7 100644 --- a/crates/socket-patch-core/src/vendor/yarn_layering_tests.rs +++ b/crates/socket-patch-core/src/vendor/yarn_layering_tests.rs @@ -39,7 +39,6 @@ use crate::hash::git_sha256::compute_git_sha256_from_bytes; use crate::manifest::schema::{PatchFileInfo, PatchRecord}; use crate::patch::apply::PatchSources; use crate::patch::redirect::{rewrite_registry_redirect, DepOverride, Integrity}; -use crate::utils::uri::encode_uri_component; use crate::vendor::lock_inventory::{inventory_npm_lock, LockIntegrity}; use crate::vendor::npm_flavor::NpmLockFlavor; use crate::vendor::yarn_berry_lock::revert_yarn_berry; @@ -907,11 +906,11 @@ async fn berry_vendoring_a_builtin_patched_package_refuses_fail_closed() { } /// Incident guard 4c: the hosted redirect rewriter on a berry lock rewrites -/// ONLY the targeted npm: entry (gaining yarn's own `::__archiveUrl=` -/// binding) and leaves the builtin patch: entries byte-identical. +/// ONLY the targeted npm: entry (its resolution becoming the hosted +/// tarball-URL locator) and leaves the builtin patch: entries byte-identical. /// /// RED-verified: asserting BERRY_PATCH_COUNT+1 fails; asserting the fsevents -/// entry gained an __archiveUrl fails. +/// entry was redirected fails. #[tokio::test] async fn berry_hosted_redirect_leaves_builtin_patch_entries_untouched() { let hosted_url = format!( @@ -937,6 +936,7 @@ async fn berry_hosted_redirect_leaves_builtin_patch_entries_untouched() { let mut files = BTreeMap::new(); files.insert("yarn.lock".to_string(), BERRY_BEFORE_LOCK.to_string()); files.insert(".yarnrc.yml".to_string(), BERRY_YARNRC.to_string()); + files.insert("package.json".to_string(), BERRY_BEFORE_PKG.to_string()); let result = rewrite_registry_redirect(&files, &[dep]); let text = result.files.get("yarn.lock").expect("yarn.lock rewritten"); @@ -948,11 +948,11 @@ async fn berry_hosted_redirect_leaves_builtin_patch_entries_untouched() { assert_eq!(berry_edits.len(), 1, "{:?}", result.edits); assert_eq!(berry_edits[0].key.as_deref(), Some("left-pad@1.3.0")); - // The target gained yarn's own archive binding… + // The target is re-keyed to the hosted tarball (its `resolutions` + // selector rides package.json)… assert!( text.contains(&format!( - " resolution: \"left-pad@npm:1.3.0::__archiveUrl={}\"", - encode_uri_component(&hosted_url) + "\"left-pad@{hosted_url}\":\n version: 1.3.0\n resolution: \"left-pad@{hosted_url}\"" )), "target entry redirected: {text}" ); @@ -965,7 +965,7 @@ async fn berry_hosted_redirect_leaves_builtin_patch_entries_untouched() { "redirect must not introduce or remove patch: strings" ); assert!( - !text.contains("fsevents@npm:2.3.2::__archiveUrl"), + !text.contains("fsevents@https://"), "untargeted packages must not be redirected" ); // left-pad has no builtin patch: entry, so the protocol gate (and every @@ -984,16 +984,12 @@ async fn berry_hosted_redirect_leaves_builtin_patch_entries_untouched() { /// Incident guard 4d: redirecting a package yarn ITSELF builtin-patches /// (resolve — the fixture holds both the plain `resolve@npm:1.20.0` entry /// and the builtin `resolve@patch:...#builtin` entry at the -/// same version) rewrites ONLY the npm: entry and leaves the builtin -/// `patch:` block byte-identical, warning about the block it refused to -/// touch. Without the protocol gate the rewriter would splice an -/// `npm:...::__archiveUrl=` resolution under the still-`patch:` key — a -/// corrupted key/resolution protocol mismatch in the incident's exact error -/// family, emitted as a silent second edit. -/// -/// RED-verified: with the protocol gate removed from `rewrite_yarn_berry`, -/// TWO `resolve@1.20.0` edits are emitted and the RESOLVE_PATCH_ENTRY -/// verbatim assert fails (resolution rewritten under the patch: key). +/// same version) leaves BOTH entries byte-identical. The builtin `patch:` +/// descriptor wraps the npm one, so the `resolutions` pin the hosted +/// redirect writes would move it too (#404's option C); the redirect skips +/// the package, naming the patch: entry and why the npm entry is left too. +/// The older incident — splicing a hosted resolution under the still-`patch:` +/// key, a corrupted key/resolution protocol pairing — stays impossible. #[tokio::test] async fn berry_hosted_redirect_of_builtin_patched_package_skips_patch_entry() { let hosted_url = format!( @@ -1017,45 +1013,35 @@ async fn berry_hosted_redirect_of_builtin_patched_package_skips_patch_entry() { let mut files = BTreeMap::new(); files.insert("yarn.lock".to_string(), BERRY_BEFORE_LOCK.to_string()); files.insert(".yarnrc.yml".to_string(), BERRY_YARNRC.to_string()); + files.insert("package.json".to_string(), BERRY_BEFORE_PKG.to_string()); let result = rewrite_registry_redirect(&files, &[dep]); - let text = result.files.get("yarn.lock").expect("yarn.lock rewritten"); - - // Exactly ONE edit — the plain npm: entry. (The corruption shape was - // two edits both keyed resolve@1.20.0, the second under the patch: key.) - let berry_edits: Vec<_> = result - .edits - .iter() - .filter(|e| e.kind == "redirect_yarn_berry_entry") - .collect(); - assert_eq!(berry_edits.len(), 1, "{:?}", result.edits); - assert_eq!(berry_edits[0].key.as_deref(), Some("resolve@1.20.0")); - - // The plain npm: entry gained yarn's archive binding… + // Nothing is written: the builtin `patch:` entry wraps the same npm + // descriptor a `resolutions` pin would move, so the whole package is + // left alone (both entries byte-identical) with both reasons named. assert!( - text.contains(&format!( - " resolution: \"resolve@npm:1.20.0::__archiveUrl={}\"", - encode_uri_component(&hosted_url) - )), - "plain npm: entry redirected: {text}" + result.files.get("yarn.lock").is_none(), + "{:?}", + result.files ); - // …the builtin patch: entries survive byte-identically (key AND - // resolution — the corruption kept the key but rewrote the resolution), - // with the patch: count unchanged… - assert!(text.contains(RESOLVE_PATCH_ENTRY), "{text}"); - assert!(text.contains(FSEVENTS_PATCH_ENTRY), "{text}"); - assert_eq!( - text.matches("patch:").count(), - BERRY_PATCH_COUNT, - "redirect must not introduce or remove patch: strings" + assert!( + result.files.get("package.json").is_none(), + "{:?}", + result.files ); - // …and the skip is loud, naming the un-ownable entry. assert!( - result.warnings.iter().any(|w| { - w.code == "redirect_yarn_berry_unsupported_protocol" - && w.detail.contains("builtin") - }), - "warning must name the skipped builtin patch: entry: {:?}", - result.warnings + !result + .edits + .iter() + .any(|e| e.kind.starts_with("redirect_yarn_berry")), + "{:?}", + result.edits + ); + let codes: Vec<&str> = result.warnings.iter().map(|w| w.code.as_str()).collect(); + assert!( + codes.contains(&"redirect_yarn_berry_unsupported_protocol") + && codes.contains(&"redirect_yarn_berry_shared_descriptor"), + "{codes:?}" ); + assert!(BERRY_BEFORE_LOCK.contains(RESOLVE_PATCH_ENTRY)); } diff --git a/crates/socket-patch-core/src/vex/discover/yarn.rs b/crates/socket-patch-core/src/vex/discover/yarn.rs index 1a73013ba..ea8fcef83 100644 --- a/crates/socket-patch-core/src/vex/discover/yarn.rs +++ b/crates/socket-patch-core/src/vex/discover/yarn.rs @@ -44,15 +44,17 @@ //! `version:` line. `workspace:`, `patch:`, `portal:`, `link:`, `exec:`, //! `git` and plain registry locators are not ours and are skipped silently. //! -//! * **Hosted** (`patch::redirect::rewrite_yarn_berry`): -//! `resolution: "name@npm:X::__archiveUrl="` — -//! the `__archiveUrl` binding value (bindings are `&`-joined after `::`) is -//! handed to [`DiscoverCtx::hosted_uuid`], which decodes a wholly -//! percent-encoded url; a custom-registry `__archiveUrl` is not Socket's and -//! is skipped. The locator's `npm:` version must equal `version:` (the -//! rewriter writes both from the same coordinate). Hand-edit tolerance: a -//! direct url locator `name@https://patch.socket.dev/…` (what a -//! url-range dependency locks to) is accepted too. Pin: `checksum:` +//! * **Hosted** (`patch::redirect::rewrite_yarn_berry`): the direct +//! tarball-URL locator `resolution: "name@https://patch.socket.dev/…"`, +//! whose reference is handed to [`DiscoverCtx::hosted_uuid`]. Locks pinned +//! by releases up to 5.0 spell it +//! `resolution: "name@npm:X::__archiveUrl="` (an +//! `npm:` locator, whose fetcher sent registry auth to the patch host — +//! #404); that `__archiveUrl` binding value (bindings are `&`-joined after +//! `::`) is still recognized, decoded by the same helper; a +//! custom-registry `__archiveUrl` is not Socket's and is skipped. That +//! locator's `npm:` version must equal `version:` (the rewriter wrote both +//! from the same coordinate). Pin: `checksum:` //! (`10c0/` — the cache-zip checksum, [`LockIntegrity::BerryChecksum`]; //! yarn 4.0.x spells it as bare hex under `cacheKey: 10c0`, read as the //! same pin); the rewriter always writes it, so `integrity_required = true`. @@ -216,19 +218,107 @@ struct BerryVendored { key: String, } +/// A berry hosted entry keyed by its own tarball descriptor (the +/// `resolutions` pin, #404), awaiting its `package.json` confirmation. +struct BerryHostedKeyed { + name: String, + version: String, + url: String, + wiring: Wired, + integrity: Option, + key: String, +} + async fn extract_berry(ctx: &DiscoverCtx<'_>, lock: BerryLock, out: &mut Discovery) { let mut vendored: Vec = Vec::new(); + let mut hosted_keyed: Vec = Vec::new(); for entry in lock.entries.iter().filter(|e| e.live) { - berry_block(ctx, entry, lock.cache_key.as_deref(), &mut vendored, out); + berry_block( + ctx, + entry, + lock.cache_key.as_deref(), + &mut vendored, + &mut hosted_keyed, + out, + ); } + confirm_berry_hosted_keyed(ctx, hosted_keyed, out).await; confirm_berry_vendored(ctx, vendored, out).await; } +/// Emit each berry hosted entry keyed by its tarball descriptor only when +/// the root `package.json` `resolutions` routes a descriptor of the package +/// to that same URL: yarn reaches the entry through that selector alone, so +/// without it the entry is orphaned (an `--immutable` install fails, a +/// plain install re-resolves the registry package) and is diagnosed +/// instead. +async fn confirm_berry_hosted_keyed( + ctx: &DiscoverCtx<'_>, + hosted: Vec, + out: &mut Discovery, +) { + if hosted.is_empty() { + return; + } + let routes: Vec<(String, String)> = match ctx.read_bytes(PACKAGE_JSON, out).await { + None => Vec::new(), + Some(bytes) => { + let bytes = bytes.strip_prefix(b"\xef\xbb\xbf").unwrap_or(&bytes); + match parse_json(PACKAGE_JSON, bytes) { + Ok(doc) => doc + .get("resolutions") + .and_then(Value::as_object) + .map(|res| { + res.iter() + .filter_map(|(selector, value)| { + let target = resolution_selector_target(selector)?; + Some((target.to_string(), value.as_str()?.to_string())) + }) + .collect() + }) + .unwrap_or_default(), + Err(detail) => { + out.diag(DIAG_LOCKFILE_UNPARSEABLE, PACKAGE_JSON, detail); + Vec::new() + } + } + } + }; + for entry in hosted { + if routes + .iter() + .any(|(target, url)| *target == entry.name && *url == entry.url) + { + emit( + &entry.name, + &entry.version, + &entry.url, + entry.wiring, + entry.integrity, + &entry.key, + out, + ); + } else { + out.diag( + DIAG_REF_INVALID, + YARN_LOCK, + format!( + "{YARN_LOCK}: hosted entry `{}` is orphaned: no {PACKAGE_JSON} \ + `resolutions` entry routes a {} descriptor to {}, so yarn does not \ + install it", + entry.key, entry.name, entry.url + ), + ); + } + } +} + fn berry_block( ctx: &DiscoverCtx<'_>, entry: &YarnEntry, cache_key: Option<&str>, vendored: &mut Vec, + hosted_keyed: &mut Vec, out: &mut Discovery, ) { let block = &entry.block; @@ -312,6 +402,18 @@ fn berry_block( key: block.key.clone(), }); } + // Keyed by its own tarball descriptor: the `resolutions` pin, live + // only through its package.json selector. + hosted if entry.patterns.len() == 1 && entry.patterns[0] == format!("{name}@{spec}") => { + hosted_keyed.push(BerryHostedKeyed { + name: name.to_string(), + version: version.to_string(), + url: spec.to_string(), + wiring: hosted, + integrity, + key: block.key.clone(), + }); + } hosted => emit(name, version, spec, hosted, integrity, &block.key, out), } } @@ -1114,27 +1216,54 @@ mod tests { ); } - /// Hand-edit tolerance: a direct url locator on the patch server (what a - /// url-range dependency locks to) and CRLF line endings (a - /// `core.autocrlf` checkout of an LF lock). + /// The `resolutions` pin (#404): an entry keyed by its own tarball + /// descriptor on the patch server, with CRLF line endings (a + /// `core.autocrlf` checkout of an LF lock), attests only while the root + /// `package.json` routes a descriptor of the package to that URL — yarn + /// reaches the entry through that selector alone. Without it (no + /// manifest, no selector, a selector to another URL) the entry is an + /// orphan: diagnosed, never attested. #[tokio::test] async fn berry_hosted_direct_url_locator_and_crlf() { let url = hosted_url("npm", "left-pad", "1.3.0", UUID_A, "left-pad-1.3.0.tgz"); + let lock = berry(&[berry_block( + &format!("left-pad@{url}"), + "1.3.0", + &format!("left-pad@{url}"), + Some(CHECKSUM), + )]) + .replace('\n', "\r\n"); let p = Project::new(); + p.write("yarn.lock", &lock); p.write( - "yarn.lock", - berry(&[berry_block( - &format!("left-pad@{url}"), - "1.3.0", - &format!("left-pad@{url}"), - Some(CHECKSUM), - )]) - .replace('\n', "\r\n"), + "package.json", + serde_json::json!({"resolutions": {"left-pad@npm:^1.3.0": url}}).to_string(), ); assert_refs( &run(&p).await, &[("pkg:npm/left-pad@1.3.0", UUID_A, WiringMode::Hosted)], ); + let other = hosted_url("npm", "left-pad", "1.3.0", UUID_B, "left-pad-1.3.0.tgz"); + for pkg in [ + None, + Some(serde_json::json!({"name": "app"}).to_string()), + Some(serde_json::json!({"resolutions": {"left-pad@npm:^1.3.0": other}}).to_string()), + ] { + let p = Project::new(); + p.write("yarn.lock", &lock); + if let Some(pkg) = &pkg { + p.write("package.json", pkg); + } + let out = run(&p).await; + assert!(out.refs.is_empty(), "{pkg:?}: {:#?}", out.refs); + assert!( + out.diagnostics + .iter() + .any(|d| d.code == DIAG_REF_INVALID && d.detail.contains("orphaned")), + "{pkg:?}: {:#?}", + out.diagnostics + ); + } } /// Negative berry shapes: an `__archiveUrl` on a foreign host carrying diff --git a/crates/socket-patch-core/tests/equivalence/pdm_rewrite_shared_parse.golden b/crates/socket-patch-core/tests/equivalence/pdm_rewrite_shared_parse.golden index 7b431b1fe..54dc20d26 100644 --- a/crates/socket-patch-core/tests/equivalence/pdm_rewrite_shared_parse.golden +++ b/crates/socket-patch-core/tests/equivalence/pdm_rewrite_shared_parse.golden @@ -1,122 +1,122 @@ # One grown pdm.lock and its url deps, rewritten, then re-run over the result. # -0.12.3-extras.lock/extra=0/crlf=false 21655a1176272fba 809d5173d9648756 -0.12.3-extras.lock/extra=0/crlf=false/re-run 9991f30c983a9730 17b1bbcbe61eec4f -0.12.3-extras.lock/extra=0/crlf=true 5900533590840a46 901c8140211b7cd7 -0.12.3-extras.lock/extra=0/crlf=true/re-run 881424448f72e65f 59d8b4c620dc0c4b -0.12.3-extras.lock/extra=3/crlf=false ce9fcf172c1ebe53 70c0d988d0dfd4b4 -0.12.3-extras.lock/extra=3/crlf=false/re-run 3255b7f165a3f1d3 e78ed1647a155407 -0.12.3-extras.lock/extra=3/crlf=true edf41cc43e312ff7 87722b79e7e93b1d -0.12.3-extras.lock/extra=3/crlf=true/re-run 2c3c4e5915d2acb2 fe8f47e0cbea1655 -0.12.3.lock/extra=0/crlf=false bb6882fa6e308227 90f59644f98311f9 -0.12.3.lock/extra=0/crlf=false/re-run 4968389f75327559 5e56b325c096f8c7 -0.12.3.lock/extra=0/crlf=true 28fa02d135110df5 dbc710cb1b884b72 -0.12.3.lock/extra=0/crlf=true/re-run d4862ba4b6fc43c9 818d3e949a2b93b9 -0.12.3.lock/extra=3/crlf=false 875655d84a273615 7cb801d1c56dbabd -0.12.3.lock/extra=3/crlf=false/re-run 3c2b2fe1fa3f2817 a5490dc0b0550433 -0.12.3.lock/extra=3/crlf=true 5b7bdf5d23ba4480 7dd95e61ad426389 -0.12.3.lock/extra=3/crlf=true/re-run b6e20136704fd19e 5e022258733eaa76 -1.15.5.lock/extra=0/crlf=false 4d4f13130b2f5ca6 0b2205f8191bb19c -1.15.5.lock/extra=0/crlf=false/re-run 4d4f13130b2f5ca6 0b2205f8191bb19c -1.15.5.lock/extra=0/crlf=true 738ea560b4bac5e9 0b2205f8191bb19c -1.15.5.lock/extra=0/crlf=true/re-run 738ea560b4bac5e9 0b2205f8191bb19c -1.15.5.lock/extra=3/crlf=false 05076616697b9f90 07c0c411f4ba1e71 -1.15.5.lock/extra=3/crlf=false/re-run 05076616697b9f90 07c0c411f4ba1e71 -1.15.5.lock/extra=3/crlf=true 61a0a8be9a863e05 07c0c411f4ba1e71 -1.15.5.lock/extra=3/crlf=true/re-run 61a0a8be9a863e05 07c0c411f4ba1e71 -2.0.3.lock/extra=0/crlf=false f85fd6eef847aaaf 131b66b9be3e0a45 -2.0.3.lock/extra=0/crlf=false/re-run f85fd6eef847aaaf 131b66b9be3e0a45 -2.0.3.lock/extra=0/crlf=true 5d41e681e269208b 131b66b9be3e0a45 -2.0.3.lock/extra=0/crlf=true/re-run 5d41e681e269208b 131b66b9be3e0a45 -2.0.3.lock/extra=3/crlf=false 4d3cfc88ac5ffece 4d21e78d4a6c9672 -2.0.3.lock/extra=3/crlf=false/re-run 4d3cfc88ac5ffece 4d21e78d4a6c9672 -2.0.3.lock/extra=3/crlf=true 337957cda423fdbf 4d21e78d4a6c9672 -2.0.3.lock/extra=3/crlf=true/re-run 337957cda423fdbf 4d21e78d4a6c9672 -2.1.5.lock/extra=0/crlf=false f85fd6eef847aaaf 131b66b9be3e0a45 -2.1.5.lock/extra=0/crlf=false/re-run f85fd6eef847aaaf 131b66b9be3e0a45 -2.1.5.lock/extra=0/crlf=true 5d41e681e269208b 131b66b9be3e0a45 -2.1.5.lock/extra=0/crlf=true/re-run 5d41e681e269208b 131b66b9be3e0a45 -2.1.5.lock/extra=3/crlf=false 4d3cfc88ac5ffece 4d21e78d4a6c9672 -2.1.5.lock/extra=3/crlf=false/re-run 4d3cfc88ac5ffece 4d21e78d4a6c9672 -2.1.5.lock/extra=3/crlf=true 337957cda423fdbf 4d21e78d4a6c9672 -2.1.5.lock/extra=3/crlf=true/re-run 337957cda423fdbf 4d21e78d4a6c9672 -2.10.4.lock/extra=0/crlf=false 172e58243f676d64 f2a0a81b4bed3b88 -2.10.4.lock/extra=0/crlf=false/re-run 52c4734e1adb2848 eb9bf215fd16a529 -2.10.4.lock/extra=0/crlf=true 84b3284978be45b7 1f654836e7b33fa1 -2.10.4.lock/extra=0/crlf=true/re-run 7d030b203da8acd0 0ead2aa1fcbfad52 -2.10.4.lock/extra=3/crlf=false 9c9557ecc3467229 c68f7d1f248c3ecc -2.10.4.lock/extra=3/crlf=false/re-run cc827f83fff579f2 0c49bd373922f97f -2.10.4.lock/extra=3/crlf=true 001be1702fcbb10b 93389a811c9ed52a -2.10.4.lock/extra=3/crlf=true/re-run e1337b373345aad8 2ff55a05b9927475 -2.11.2.lock/extra=0/crlf=false 6b4f6ea534403ce1 cc61854f70d4fba2 -2.11.2.lock/extra=0/crlf=false/re-run 02e7df4fcb62c310 0a8eaad6d2cd06b8 -2.11.2.lock/extra=0/crlf=true c626206c4024fe46 529985fb6a6cae00 -2.11.2.lock/extra=0/crlf=true/re-run 87f02f8297dce1ec e8782ebba7126463 -2.11.2.lock/extra=3/crlf=false 71024d43303c242f 3fa8ff9c1fa8a829 -2.11.2.lock/extra=3/crlf=false/re-run 4c92b86de4ae77eb b640065677eec095 -2.11.2.lock/extra=3/crlf=true 986a91c5cd4511b7 7931110cfdd96743 -2.11.2.lock/extra=3/crlf=true/re-run 601377db18458ff3 72e432f7f7264e24 -2.17.3.lock/extra=0/crlf=false 98c5c38de2f9a8e1 40814a1796758eb4 -2.17.3.lock/extra=0/crlf=false/re-run bea465fa3cc73663 0a8eaad6d2cd06b8 -2.17.3.lock/extra=0/crlf=true a0c9bb9ed37191ef 3c6319ad4300e6ab -2.17.3.lock/extra=0/crlf=true/re-run ea711090534018d5 e8782ebba7126463 -2.17.3.lock/extra=3/crlf=false 093ae70e2482288c 582669a24bbce5d6 -2.17.3.lock/extra=3/crlf=false/re-run b73d20dc2ea08fa5 b640065677eec095 -2.17.3.lock/extra=3/crlf=true d21eb6a0c09c524c b70232b890c8e760 -2.17.3.lock/extra=3/crlf=true/re-run 15971e9f41f1f52e 72e432f7f7264e24 -2.29.2-extras.lock/extra=0/crlf=false eb78aecb54bd098d 89c3baad99afcc2f -2.29.2-extras.lock/extra=0/crlf=false/re-run 42d93b5c405df78d b3f36f2cdfa5dc33 -2.29.2-extras.lock/extra=0/crlf=true a74183ac5e064afc 6ea3469b9815b1e7 -2.29.2-extras.lock/extra=0/crlf=true/re-run 9c29d9c8a18cf39b 69bd7ba0f9c7d38e -2.29.2-extras.lock/extra=3/crlf=false 6485ead297972881 68223a7210a4053b -2.29.2-extras.lock/extra=3/crlf=false/re-run a522a0262db8df40 effd968d9f51e16e -2.29.2-extras.lock/extra=3/crlf=true 53718730bd1cda51 303bb052130da472 -2.29.2-extras.lock/extra=3/crlf=true/re-run 8b5ab3312a7b27a3 b10d2bcc65b0fc1d -2.29.2.lock/extra=0/crlf=false 0afc5c9cc3e3929d 2806e6920b5375b6 -2.29.2.lock/extra=0/crlf=false/re-run 95943761aa62a765 0a8eaad6d2cd06b8 -2.29.2.lock/extra=0/crlf=true 36eebecfb1bfccd1 bcb3dc38ad08627d -2.29.2.lock/extra=0/crlf=true/re-run 02f20956ed8b4483 e8782ebba7126463 -2.29.2.lock/extra=3/crlf=false ef194e4c3f23be69 978acdf6b1dae5ae -2.29.2.lock/extra=3/crlf=false/re-run 771462e9c51554c5 b640065677eec095 -2.29.2.lock/extra=3/crlf=true cbaa4943320bfe44 a5e1953363d49eb7 -2.29.2.lock/extra=3/crlf=true/re-run 35b9ad6159d5458d 72e432f7f7264e24 -2.3.4.lock/extra=0/crlf=false 1edd8acc82a5ddcc 29736fbdb094f94b -2.3.4.lock/extra=0/crlf=false/re-run 1edd8acc82a5ddcc 29736fbdb094f94b -2.3.4.lock/extra=0/crlf=true 817bd86e9a710e96 29736fbdb094f94b -2.3.4.lock/extra=0/crlf=true/re-run 817bd86e9a710e96 29736fbdb094f94b -2.3.4.lock/extra=3/crlf=false 26d0cc1acb4d1e66 d7ba2987ab7dd67e -2.3.4.lock/extra=3/crlf=false/re-run 26d0cc1acb4d1e66 d7ba2987ab7dd67e -2.3.4.lock/extra=3/crlf=true 9e6c6abc2b3b36da d7ba2987ab7dd67e -2.3.4.lock/extra=3/crlf=true/re-run 9e6c6abc2b3b36da d7ba2987ab7dd67e -2.6.1.lock/extra=0/crlf=false a1cf90463aec548e f6f253029810f912 -2.6.1.lock/extra=0/crlf=false/re-run a1cf90463aec548e f6f253029810f912 -2.6.1.lock/extra=0/crlf=true b4f2fc1d2ad04aff f6f253029810f912 -2.6.1.lock/extra=0/crlf=true/re-run b4f2fc1d2ad04aff f6f253029810f912 -2.6.1.lock/extra=3/crlf=false 0a39d27410f0a359 c6b867d9dfec6773 -2.6.1.lock/extra=3/crlf=false/re-run 0a39d27410f0a359 c6b867d9dfec6773 -2.6.1.lock/extra=3/crlf=true 028a8e1f1bf050a5 c6b867d9dfec6773 -2.6.1.lock/extra=3/crlf=true/re-run 028a8e1f1bf050a5 c6b867d9dfec6773 -2.7.4.lock/extra=0/crlf=false a1cf90463aec548e f6f253029810f912 -2.7.4.lock/extra=0/crlf=false/re-run a1cf90463aec548e f6f253029810f912 -2.7.4.lock/extra=0/crlf=true b4f2fc1d2ad04aff f6f253029810f912 -2.7.4.lock/extra=0/crlf=true/re-run b4f2fc1d2ad04aff f6f253029810f912 -2.7.4.lock/extra=3/crlf=false 0a39d27410f0a359 c6b867d9dfec6773 -2.7.4.lock/extra=3/crlf=false/re-run 0a39d27410f0a359 c6b867d9dfec6773 -2.7.4.lock/extra=3/crlf=true 028a8e1f1bf050a5 c6b867d9dfec6773 -2.7.4.lock/extra=3/crlf=true/re-run 028a8e1f1bf050a5 c6b867d9dfec6773 -2.8.2.lock/extra=0/crlf=false 55e31f21f6b597b6 4220c32ed0b57c66 -2.8.2.lock/extra=0/crlf=false/re-run bdf788b9e0b21711 33f0667d1cf222b9 -2.8.2.lock/extra=0/crlf=true 8860e7f81b315e97 227e1e4fbde049fd -2.8.2.lock/extra=0/crlf=true/re-run 152ed911da843927 9a38f87f3025407d -2.8.2.lock/extra=3/crlf=false aa2044473027360e b0e271b710d41fc2 -2.8.2.lock/extra=3/crlf=false/re-run a0c3c1cf672c0825 1c6cd92a69a414e5 -2.8.2.lock/extra=3/crlf=true 59da76de16052042 a194ed789693634a -2.8.2.lock/extra=3/crlf=true/re-run 37089aac3445a20c de9b5b7544cde7bc -2.9.3.lock/extra=0/crlf=false 55e31f21f6b597b6 4220c32ed0b57c66 -2.9.3.lock/extra=0/crlf=false/re-run bdf788b9e0b21711 33f0667d1cf222b9 -2.9.3.lock/extra=0/crlf=true 8860e7f81b315e97 227e1e4fbde049fd -2.9.3.lock/extra=0/crlf=true/re-run 152ed911da843927 9a38f87f3025407d -2.9.3.lock/extra=3/crlf=false aa2044473027360e b0e271b710d41fc2 -2.9.3.lock/extra=3/crlf=false/re-run a0c3c1cf672c0825 1c6cd92a69a414e5 -2.9.3.lock/extra=3/crlf=true 59da76de16052042 a194ed789693634a -2.9.3.lock/extra=3/crlf=true/re-run 37089aac3445a20c de9b5b7544cde7bc +0.12.3-extras.lock/extra=0/crlf=false 21655a1176272fba 9cbade162505fee0 +0.12.3-extras.lock/extra=0/crlf=false/re-run 9991f30c983a9730 793457405e333ba8 +0.12.3-extras.lock/extra=0/crlf=true 5900533590840a46 ce544a091f71af00 +0.12.3-extras.lock/extra=0/crlf=true/re-run 881424448f72e65f df78790d27b111a1 +0.12.3-extras.lock/extra=3/crlf=false ce9fcf172c1ebe53 dd8e834970a168ec +0.12.3-extras.lock/extra=3/crlf=false/re-run 3255b7f165a3f1d3 66ca3eba7984e3bb +0.12.3-extras.lock/extra=3/crlf=true edf41cc43e312ff7 43108a254ca1e83f +0.12.3-extras.lock/extra=3/crlf=true/re-run 2c3c4e5915d2acb2 d61c62092a306efb +0.12.3.lock/extra=0/crlf=false bb6882fa6e308227 907d1d1a73b7170e +0.12.3.lock/extra=0/crlf=false/re-run 4968389f75327559 bd1c3d37def8de8b +0.12.3.lock/extra=0/crlf=true 28fa02d135110df5 f0e499e7ac078918 +0.12.3.lock/extra=0/crlf=true/re-run d4862ba4b6fc43c9 ba07ae39513e0d51 +0.12.3.lock/extra=3/crlf=false 875655d84a273615 813ebc475d3f19c3 +0.12.3.lock/extra=3/crlf=false/re-run 3c2b2fe1fa3f2817 ad97f3348557350e +0.12.3.lock/extra=3/crlf=true 5b7bdf5d23ba4480 30833127c2300a6d +0.12.3.lock/extra=3/crlf=true/re-run b6e20136704fd19e 05591671f368bfdf +1.15.5.lock/extra=0/crlf=false 4d4f13130b2f5ca6 e864f4448fd41f03 +1.15.5.lock/extra=0/crlf=false/re-run 4d4f13130b2f5ca6 e864f4448fd41f03 +1.15.5.lock/extra=0/crlf=true 738ea560b4bac5e9 e864f4448fd41f03 +1.15.5.lock/extra=0/crlf=true/re-run 738ea560b4bac5e9 e864f4448fd41f03 +1.15.5.lock/extra=3/crlf=false 05076616697b9f90 95c7decb7f8a952c +1.15.5.lock/extra=3/crlf=false/re-run 05076616697b9f90 95c7decb7f8a952c +1.15.5.lock/extra=3/crlf=true 61a0a8be9a863e05 95c7decb7f8a952c +1.15.5.lock/extra=3/crlf=true/re-run 61a0a8be9a863e05 95c7decb7f8a952c +2.0.3.lock/extra=0/crlf=false f85fd6eef847aaaf 600061fab0474509 +2.0.3.lock/extra=0/crlf=false/re-run f85fd6eef847aaaf 600061fab0474509 +2.0.3.lock/extra=0/crlf=true 5d41e681e269208b 600061fab0474509 +2.0.3.lock/extra=0/crlf=true/re-run 5d41e681e269208b 600061fab0474509 +2.0.3.lock/extra=3/crlf=false 4d3cfc88ac5ffece 0108056ab6bb4a43 +2.0.3.lock/extra=3/crlf=false/re-run 4d3cfc88ac5ffece 0108056ab6bb4a43 +2.0.3.lock/extra=3/crlf=true 337957cda423fdbf 0108056ab6bb4a43 +2.0.3.lock/extra=3/crlf=true/re-run 337957cda423fdbf 0108056ab6bb4a43 +2.1.5.lock/extra=0/crlf=false f85fd6eef847aaaf 600061fab0474509 +2.1.5.lock/extra=0/crlf=false/re-run f85fd6eef847aaaf 600061fab0474509 +2.1.5.lock/extra=0/crlf=true 5d41e681e269208b 600061fab0474509 +2.1.5.lock/extra=0/crlf=true/re-run 5d41e681e269208b 600061fab0474509 +2.1.5.lock/extra=3/crlf=false 4d3cfc88ac5ffece 0108056ab6bb4a43 +2.1.5.lock/extra=3/crlf=false/re-run 4d3cfc88ac5ffece 0108056ab6bb4a43 +2.1.5.lock/extra=3/crlf=true 337957cda423fdbf 0108056ab6bb4a43 +2.1.5.lock/extra=3/crlf=true/re-run 337957cda423fdbf 0108056ab6bb4a43 +2.10.4.lock/extra=0/crlf=false 172e58243f676d64 cf4fe3ef38b2f0f8 +2.10.4.lock/extra=0/crlf=false/re-run 52c4734e1adb2848 ba04212889eba2a3 +2.10.4.lock/extra=0/crlf=true 84b3284978be45b7 cd0d59d5019a1114 +2.10.4.lock/extra=0/crlf=true/re-run 7d030b203da8acd0 02655f74431f4865 +2.10.4.lock/extra=3/crlf=false 9c9557ecc3467229 3a5527da132b8ad4 +2.10.4.lock/extra=3/crlf=false/re-run cc827f83fff579f2 31c0381ce0317801 +2.10.4.lock/extra=3/crlf=true 001be1702fcbb10b 281f50565f77e7d1 +2.10.4.lock/extra=3/crlf=true/re-run e1337b373345aad8 3a5d80c8a0c46bd8 +2.11.2.lock/extra=0/crlf=false 6b4f6ea534403ce1 9c28babcbeafbc8e +2.11.2.lock/extra=0/crlf=false/re-run 02e7df4fcb62c310 f65da9dde35c9f79 +2.11.2.lock/extra=0/crlf=true c626206c4024fe46 334732622c8a9273 +2.11.2.lock/extra=0/crlf=true/re-run 87f02f8297dce1ec 6fc0547d806983e7 +2.11.2.lock/extra=3/crlf=false 71024d43303c242f 738f1863c5b58e79 +2.11.2.lock/extra=3/crlf=false/re-run 4c92b86de4ae77eb 1a1c19b5e80789dd +2.11.2.lock/extra=3/crlf=true 986a91c5cd4511b7 7d8dc7df8a5aa93b +2.11.2.lock/extra=3/crlf=true/re-run 601377db18458ff3 4be4fac4e84c75b9 +2.17.3.lock/extra=0/crlf=false 98c5c38de2f9a8e1 230860f727a697bd +2.17.3.lock/extra=0/crlf=false/re-run bea465fa3cc73663 f65da9dde35c9f79 +2.17.3.lock/extra=0/crlf=true a0c9bb9ed37191ef 863b4d9a0bc81e57 +2.17.3.lock/extra=0/crlf=true/re-run ea711090534018d5 6fc0547d806983e7 +2.17.3.lock/extra=3/crlf=false 093ae70e2482288c b558315264903729 +2.17.3.lock/extra=3/crlf=false/re-run b73d20dc2ea08fa5 1a1c19b5e80789dd +2.17.3.lock/extra=3/crlf=true d21eb6a0c09c524c 2ec3501dcb2859c4 +2.17.3.lock/extra=3/crlf=true/re-run 15971e9f41f1f52e 4be4fac4e84c75b9 +2.29.2-extras.lock/extra=0/crlf=false eb78aecb54bd098d bd5d41ea51501dc4 +2.29.2-extras.lock/extra=0/crlf=false/re-run 42d93b5c405df78d 3e84ffdf57183ce7 +2.29.2-extras.lock/extra=0/crlf=true a74183ac5e064afc cc0ba95a1a5f823f +2.29.2-extras.lock/extra=0/crlf=true/re-run 9c29d9c8a18cf39b fbbd77f8b3c5d5c3 +2.29.2-extras.lock/extra=3/crlf=false 6485ead297972881 1cdf42341d9ffa61 +2.29.2-extras.lock/extra=3/crlf=false/re-run a522a0262db8df40 de8ce75171e02cd0 +2.29.2-extras.lock/extra=3/crlf=true 53718730bd1cda51 b70c52ac1129c2a2 +2.29.2-extras.lock/extra=3/crlf=true/re-run 8b5ab3312a7b27a3 9c153f7662382567 +2.29.2.lock/extra=0/crlf=false 0afc5c9cc3e3929d cdcfdda0098db93c +2.29.2.lock/extra=0/crlf=false/re-run 95943761aa62a765 f65da9dde35c9f79 +2.29.2.lock/extra=0/crlf=true 36eebecfb1bfccd1 d88cc108b1d7abff +2.29.2.lock/extra=0/crlf=true/re-run 02f20956ed8b4483 6fc0547d806983e7 +2.29.2.lock/extra=3/crlf=false ef194e4c3f23be69 53524501c474be0e +2.29.2.lock/extra=3/crlf=false/re-run 771462e9c51554c5 1a1c19b5e80789dd +2.29.2.lock/extra=3/crlf=true cbaa4943320bfe44 9d3349ed847e8d43 +2.29.2.lock/extra=3/crlf=true/re-run 35b9ad6159d5458d 4be4fac4e84c75b9 +2.3.4.lock/extra=0/crlf=false 1edd8acc82a5ddcc 8a412225fe7cfcbb +2.3.4.lock/extra=0/crlf=false/re-run 1edd8acc82a5ddcc 8a412225fe7cfcbb +2.3.4.lock/extra=0/crlf=true 817bd86e9a710e96 8a412225fe7cfcbb +2.3.4.lock/extra=0/crlf=true/re-run 817bd86e9a710e96 8a412225fe7cfcbb +2.3.4.lock/extra=3/crlf=false 26d0cc1acb4d1e66 5749237f48a6a1bf +2.3.4.lock/extra=3/crlf=false/re-run 26d0cc1acb4d1e66 5749237f48a6a1bf +2.3.4.lock/extra=3/crlf=true 9e6c6abc2b3b36da 5749237f48a6a1bf +2.3.4.lock/extra=3/crlf=true/re-run 9e6c6abc2b3b36da 5749237f48a6a1bf +2.6.1.lock/extra=0/crlf=false a1cf90463aec548e 28f73c0383eb31fe +2.6.1.lock/extra=0/crlf=false/re-run a1cf90463aec548e 28f73c0383eb31fe +2.6.1.lock/extra=0/crlf=true b4f2fc1d2ad04aff 28f73c0383eb31fe +2.6.1.lock/extra=0/crlf=true/re-run b4f2fc1d2ad04aff 28f73c0383eb31fe +2.6.1.lock/extra=3/crlf=false 0a39d27410f0a359 72ad6b5da9737ced +2.6.1.lock/extra=3/crlf=false/re-run 0a39d27410f0a359 72ad6b5da9737ced +2.6.1.lock/extra=3/crlf=true 028a8e1f1bf050a5 72ad6b5da9737ced +2.6.1.lock/extra=3/crlf=true/re-run 028a8e1f1bf050a5 72ad6b5da9737ced +2.7.4.lock/extra=0/crlf=false a1cf90463aec548e 28f73c0383eb31fe +2.7.4.lock/extra=0/crlf=false/re-run a1cf90463aec548e 28f73c0383eb31fe +2.7.4.lock/extra=0/crlf=true b4f2fc1d2ad04aff 28f73c0383eb31fe +2.7.4.lock/extra=0/crlf=true/re-run b4f2fc1d2ad04aff 28f73c0383eb31fe +2.7.4.lock/extra=3/crlf=false 0a39d27410f0a359 72ad6b5da9737ced +2.7.4.lock/extra=3/crlf=false/re-run 0a39d27410f0a359 72ad6b5da9737ced +2.7.4.lock/extra=3/crlf=true 028a8e1f1bf050a5 72ad6b5da9737ced +2.7.4.lock/extra=3/crlf=true/re-run 028a8e1f1bf050a5 72ad6b5da9737ced +2.8.2.lock/extra=0/crlf=false 55e31f21f6b597b6 91548502bf4e668e +2.8.2.lock/extra=0/crlf=false/re-run bdf788b9e0b21711 0a1fbbbab38f7296 +2.8.2.lock/extra=0/crlf=true 8860e7f81b315e97 b12ae756a79e4638 +2.8.2.lock/extra=0/crlf=true/re-run 152ed911da843927 eaa7e3d1f60e017e +2.8.2.lock/extra=3/crlf=false aa2044473027360e ee6ef491a5760fd3 +2.8.2.lock/extra=3/crlf=false/re-run a0c3c1cf672c0825 8ebc8eda8ae4dfe8 +2.8.2.lock/extra=3/crlf=true 59da76de16052042 627a70d61084d0ed +2.8.2.lock/extra=3/crlf=true/re-run 37089aac3445a20c 42e8dda85bf7f9a3 +2.9.3.lock/extra=0/crlf=false 55e31f21f6b597b6 91548502bf4e668e +2.9.3.lock/extra=0/crlf=false/re-run bdf788b9e0b21711 0a1fbbbab38f7296 +2.9.3.lock/extra=0/crlf=true 8860e7f81b315e97 b12ae756a79e4638 +2.9.3.lock/extra=0/crlf=true/re-run 152ed911da843927 eaa7e3d1f60e017e +2.9.3.lock/extra=3/crlf=false aa2044473027360e ee6ef491a5760fd3 +2.9.3.lock/extra=3/crlf=false/re-run a0c3c1cf672c0825 8ebc8eda8ae4dfe8 +2.9.3.lock/extra=3/crlf=true 59da76de16052042 627a70d61084d0ed +2.9.3.lock/extra=3/crlf=true/re-run 37089aac3445a20c 42e8dda85bf7f9a3 diff --git a/crates/socket-patch-core/tests/equivalence/poetry_rewrite.golden b/crates/socket-patch-core/tests/equivalence/poetry_rewrite.golden index f17694ddd..3bffb0f0d 100644 --- a/crates/socket-patch-core/tests/equivalence/poetry_rewrite.golden +++ b/crates/socket-patch-core/tests/equivalence/poetry_rewrite.golden @@ -1,122 +1,122 @@ # One grown poetry.lock pair and its deps, rewritten, then re-run over the result. # -0.12.17_extra=0_crlf=false d2b80f8d058298a0 9e1b8be6c76c1595 -0.12.17_extra=0_crlf=false/re-run d2b80f8d058298a0 9e1b8be6c76c1595 -0.12.17_extra=0_crlf=true 4b3a0e23f3264df8 9e1b8be6c76c1595 -0.12.17_extra=0_crlf=true/re-run 4b3a0e23f3264df8 9e1b8be6c76c1595 -0.12.17_extra=3_crlf=false a8bee4606ba5b760 a6e7d0f278fdc06d -0.12.17_extra=3_crlf=false/re-run a8bee4606ba5b760 a6e7d0f278fdc06d -0.12.17_extra=3_crlf=true 5ae487fb99db0745 a6e7d0f278fdc06d -0.12.17_extra=3_crlf=true/re-run 5ae487fb99db0745 a6e7d0f278fdc06d -1.0.10_extra=0_crlf=false 06d7816556307b33 73cc56ad4e9d9930 -1.0.10_extra=0_crlf=false/re-run 613ac93e03589fac 30b0266a94d592b0 -1.0.10_extra=0_crlf=true 38755c44d8811d7b 78c86c70937ed439 -1.0.10_extra=0_crlf=true/re-run a039bf51c09a676d 30b0266a94d592b0 -1.0.10_extra=3_crlf=false cd980d8f38e065ee b4b3c43dbea8a492 -1.0.10_extra=3_crlf=false/re-run 885d4754947c0ec7 32234b814326d8c9 -1.0.10_extra=3_crlf=true 684566844b37fcd0 eac73debe4d8bd01 -1.0.10_extra=3_crlf=true/re-run 4fd1416655a91e78 32234b814326d8c9 -1.1.15_extra=0_crlf=false d0f26c272489ebe3 e6e2d176c11a9658 -1.1.15_extra=0_crlf=false/re-run bc82912108fd25ce 30b0266a94d592b0 -1.1.15_extra=0_crlf=true 6c566dc6e3aeba50 1e5d9d15d8746324 -1.1.15_extra=0_crlf=true/re-run dea1745a49ef026f 30b0266a94d592b0 -1.1.15_extra=3_crlf=false 8870f129976dee0d d0f4d984f4acb4fe -1.1.15_extra=3_crlf=false/re-run 8551e339b8b54ddf 32234b814326d8c9 -1.1.15_extra=3_crlf=true 4d055246679dee4c 710b587ea21a08a3 -1.1.15_extra=3_crlf=true/re-run 53fe14f551e02333 32234b814326d8c9 -1.2.2_extra=0_crlf=false 4b75722f3771f21c 6d4974422d916050 -1.2.2_extra=0_crlf=false/re-run 133b23c3dfc4cce1 30b0266a94d592b0 -1.2.2_extra=0_crlf=true 1813b308432ea034 0038e3d1b5bae3b2 -1.2.2_extra=0_crlf=true/re-run 004001fb1c25e747 30b0266a94d592b0 -1.2.2_extra=3_crlf=false 812817b968306e99 51d726f48e26fd66 -1.2.2_extra=3_crlf=false/re-run f81a1c4a49505bce 1d80852e685ef00a -1.2.2_extra=3_crlf=true 6051efc48c2d3992 d950b16a45185f00 -1.2.2_extra=3_crlf=true/re-run e3ec0a6371139d8b 1d80852e685ef00a -1.3.2_extra=0_crlf=false 86cda4c82eee7e8b 2100b1880f82fed5 -1.3.2_extra=0_crlf=false/re-run a9df9acaedd7db22 30b0266a94d592b0 -1.3.2_extra=0_crlf=true e92e1cd1beeb0002 8f4c0ee8749dd386 -1.3.2_extra=0_crlf=true/re-run f13a599ff9511fe7 30b0266a94d592b0 -1.3.2_extra=3_crlf=false fe9f2817767e5612 8910fbc19bc708bc -1.3.2_extra=3_crlf=false/re-run d9728f0057845cbb 32234b814326d8c9 -1.3.2_extra=3_crlf=true 253448255457216e e6b4e4b0a73361e6 -1.3.2_extra=3_crlf=true/re-run 71d0a8d8ab2ecc77 32234b814326d8c9 -1.4.2_extra=0_crlf=false 00a7bc0a3c89c49b edd2d911d6d732cc -1.4.2_extra=0_crlf=false/re-run 4aa5eedcf2dd0b02 30b0266a94d592b0 -1.4.2_extra=0_crlf=true 44de4691acb61dba a61f31935e15372d -1.4.2_extra=0_crlf=true/re-run 47bf0330a4e93507 30b0266a94d592b0 -1.4.2_extra=3_crlf=false a3fb1d70a1207ea7 d07317e895cdd67b -1.4.2_extra=3_crlf=false/re-run 60023b576de9ad30 32234b814326d8c9 -1.4.2_extra=3_crlf=true bd8911bca06c1262 2f4fb7292045af26 -1.4.2_extra=3_crlf=true/re-run a5f5292e17ecb5f4 32234b814326d8c9 -1.5.1_extra=0_crlf=false a1795ca286fa80ce 51496e9bd28f8b5e -1.5.1_extra=0_crlf=false/re-run f19230652767bb30 30b0266a94d592b0 -1.5.1_extra=0_crlf=true 5871c60c9d63aeee 3d72fcde158d25b0 -1.5.1_extra=0_crlf=true/re-run 6dab15e1eab2f462 30b0266a94d592b0 -1.5.1_extra=3_crlf=false 0b33aee716ad5a55 795e549cefa6ae95 -1.5.1_extra=3_crlf=false/re-run e26d96dd76e1b368 32234b814326d8c9 -1.5.1_extra=3_crlf=true 161846bd33f1d508 6ccb3dea8f889715 -1.5.1_extra=3_crlf=true/re-run 3e4cf8de3fdcb6ea 32234b814326d8c9 -1.6.1_extra=0_crlf=false d1e0442566c6ce49 9ec3457339bb0cf8 -1.6.1_extra=0_crlf=false/re-run ea7966d58c9f2aab 30b0266a94d592b0 -1.6.1_extra=0_crlf=true db20a2c4edcafbf0 8422662929ee4c5e -1.6.1_extra=0_crlf=true/re-run 8f7c22713b6f70b6 30b0266a94d592b0 -1.6.1_extra=3_crlf=false afd5d875780cdf40 03bb443e813da9a3 -1.6.1_extra=3_crlf=false/re-run 647ef4eb652f1ba6 32234b814326d8c9 -1.6.1_extra=3_crlf=true b37fcda617275f4b 0c14502a9117c3b9 -1.6.1_extra=3_crlf=true/re-run 1a4219f03efad32a 32234b814326d8c9 -1.7.1_extra=0_crlf=false 6600136814fb4134 a784d951b1977a03 -1.7.1_extra=0_crlf=false/re-run abdbbfe42c8b4d7e 30b0266a94d592b0 -1.7.1_extra=0_crlf=true 6a85a6a27c99cd2b 8444713d90406a10 -1.7.1_extra=0_crlf=true/re-run ac9d84b451f9c128 30b0266a94d592b0 -1.7.1_extra=3_crlf=false 020fa829b6a870bc dbd900dfc98d6ec9 -1.7.1_extra=3_crlf=false/re-run 31133977b68135e7 32234b814326d8c9 -1.7.1_extra=3_crlf=true bcb1fd00c2d4d5bf 91e9b8a125e37564 -1.7.1_extra=3_crlf=true/re-run ac38704d471666ab 32234b814326d8c9 -1.8.5_extra=0_crlf=false 7b0c488a965c4f7b d052c3135a7fb420 -1.8.5_extra=0_crlf=false/re-run 60ce065c6b04b5b1 30b0266a94d592b0 -1.8.5_extra=0_crlf=true 2094d208abbeabc2 40944bc260591f07 -1.8.5_extra=0_crlf=true/re-run 5903de75cc2030e8 30b0266a94d592b0 -1.8.5_extra=3_crlf=false e22381008c314e3d e3434b2985a4676c -1.8.5_extra=3_crlf=false/re-run 147043d9e02c7a08 32234b814326d8c9 -1.8.5_extra=3_crlf=true e42675d2895de9fb a7260f9af327d8ff -1.8.5_extra=3_crlf=true/re-run 9e97459a7ac1ffb1 32234b814326d8c9 -2.0.1_extra=0_crlf=false 2775b8f1411fbadb 63f3c3a3268d2c56 -2.0.1_extra=0_crlf=false/re-run 5a19ccf0764fac47 30b0266a94d592b0 -2.0.1_extra=0_crlf=true 17ebf61a05b76816 b1eeee8b5d00f898 -2.0.1_extra=0_crlf=true/re-run 97eff076ab7188a2 30b0266a94d592b0 -2.0.1_extra=3_crlf=false bc38767ba9dcc6e6 f595a747eadfa172 -2.0.1_extra=3_crlf=false/re-run 44cce38f20f8f16f 32234b814326d8c9 -2.0.1_extra=3_crlf=true b68b1e02f151ef48 0da34045415ea712 -2.0.1_extra=3_crlf=true/re-run 054b4ddcb4dcfafa 32234b814326d8c9 -2.1.4_extra=0_crlf=false 6f62ddaa4adc50a4 afb51a354c1fd0df -2.1.4_extra=0_crlf=false/re-run 5b7e62f966b8d271 30b0266a94d592b0 -2.1.4_extra=0_crlf=true 8a8d93a2b2127129 41275a6bd08fcca6 -2.1.4_extra=0_crlf=true/re-run d1cd3eeea0692da9 30b0266a94d592b0 -2.1.4_extra=3_crlf=false 65364c0f4e7aa0e7 344f645289622a8d -2.1.4_extra=3_crlf=false/re-run a3676e7eddbccf4a 32234b814326d8c9 -2.1.4_extra=3_crlf=true 821fdbf37b56026b 3e3f32dd9cc9347d -2.1.4_extra=3_crlf=true/re-run 25f65d16b322be1c 32234b814326d8c9 -2.2.1_extra=0_crlf=false 7a500b5022ac388c 042cdfaba6348b6c -2.2.1_extra=0_crlf=false/re-run b484d2ecc12a5edb 30b0266a94d592b0 -2.2.1_extra=0_crlf=true 11b39909d694a4f5 3d57b740dd98fbc8 -2.2.1_extra=0_crlf=true/re-run f73c0184185b55c8 30b0266a94d592b0 -2.2.1_extra=3_crlf=false 10b0bcde3632669b 5b977e8ce996681a -2.2.1_extra=3_crlf=false/re-run 679acf31e13b71a0 32234b814326d8c9 -2.2.1_extra=3_crlf=true feaa6416a4b168a3 6e813d20100f490a -2.2.1_extra=3_crlf=true/re-run 4b8aa165ed772485 32234b814326d8c9 -2.3.4_extra=0_crlf=false 1ad17cac9704b1ce 2090fe73272aa385 -2.3.4_extra=0_crlf=false/re-run 398fea1a1ac5d77b 30b0266a94d592b0 -2.3.4_extra=0_crlf=true d4578a811e547b2c 4fe8bd420710ba7d -2.3.4_extra=0_crlf=true/re-run 5f79a7161605a083 30b0266a94d592b0 -2.3.4_extra=3_crlf=false d38430f6b06c87f3 1abe319fe8ab8822 -2.3.4_extra=3_crlf=false/re-run ee7114aaad11f4f0 32234b814326d8c9 -2.3.4_extra=3_crlf=true 89e72b13eccda257 7c5648bd37100db0 -2.3.4_extra=3_crlf=true/re-run 04c0dd27743659d0 32234b814326d8c9 -2.4.3_extra=0_crlf=false 70d3006a3e404efc 72d4b0d17de4824a -2.4.3_extra=0_crlf=false/re-run cb0289ac6716caab 30b0266a94d592b0 -2.4.3_extra=0_crlf=true ebe555cb2a5fb1f2 0964ccd968ad9987 -2.4.3_extra=0_crlf=true/re-run 3f6277fd5f2ef21b 30b0266a94d592b0 -2.4.3_extra=3_crlf=false 7bcb0eb7f4f1150a d48160322a01d395 -2.4.3_extra=3_crlf=false/re-run c7b4e742d1f0f79b 32234b814326d8c9 -2.4.3_extra=3_crlf=true f1ba10a6e2549703 743c50e17db2b102 -2.4.3_extra=3_crlf=true/re-run 6c2fb99f914a5767 32234b814326d8c9 +0.12.17_extra=0_crlf=false d2b80f8d058298a0 0b2a725e611a39ba +0.12.17_extra=0_crlf=false/re-run d2b80f8d058298a0 0b2a725e611a39ba +0.12.17_extra=0_crlf=true 4b3a0e23f3264df8 0b2a725e611a39ba +0.12.17_extra=0_crlf=true/re-run 4b3a0e23f3264df8 0b2a725e611a39ba +0.12.17_extra=3_crlf=false a8bee4606ba5b760 bc28e4842040ec43 +0.12.17_extra=3_crlf=false/re-run a8bee4606ba5b760 bc28e4842040ec43 +0.12.17_extra=3_crlf=true 5ae487fb99db0745 bc28e4842040ec43 +0.12.17_extra=3_crlf=true/re-run 5ae487fb99db0745 bc28e4842040ec43 +1.0.10_extra=0_crlf=false 06d7816556307b33 09c1cc4c24fd1e8a +1.0.10_extra=0_crlf=false/re-run 613ac93e03589fac 1f3c40d9eb7c76cc +1.0.10_extra=0_crlf=true 38755c44d8811d7b 1de45eb80f579354 +1.0.10_extra=0_crlf=true/re-run a039bf51c09a676d 1f3c40d9eb7c76cc +1.0.10_extra=3_crlf=false cd980d8f38e065ee e644c20d2bf249e6 +1.0.10_extra=3_crlf=false/re-run 885d4754947c0ec7 1a8438ef5c1e4029 +1.0.10_extra=3_crlf=true 684566844b37fcd0 b79d6c7901b3712e +1.0.10_extra=3_crlf=true/re-run 4fd1416655a91e78 1a8438ef5c1e4029 +1.1.15_extra=0_crlf=false d0f26c272489ebe3 5346ad345893f81c +1.1.15_extra=0_crlf=false/re-run bc82912108fd25ce 1f3c40d9eb7c76cc +1.1.15_extra=0_crlf=true 6c566dc6e3aeba50 58a4fd6324ef3646 +1.1.15_extra=0_crlf=true/re-run dea1745a49ef026f 1f3c40d9eb7c76cc +1.1.15_extra=3_crlf=false 8870f129976dee0d 2dc32d148015726c +1.1.15_extra=3_crlf=false/re-run 8551e339b8b54ddf 1a8438ef5c1e4029 +1.1.15_extra=3_crlf=true 4d055246679dee4c c612da5b78287e16 +1.1.15_extra=3_crlf=true/re-run 53fe14f551e02333 1a8438ef5c1e4029 +1.2.2_extra=0_crlf=false 4b75722f3771f21c 9888cc4e2dfbdcf0 +1.2.2_extra=0_crlf=false/re-run 133b23c3dfc4cce1 1f3c40d9eb7c76cc +1.2.2_extra=0_crlf=true 1813b308432ea034 fe1747c65c708940 +1.2.2_extra=0_crlf=true/re-run 004001fb1c25e747 1f3c40d9eb7c76cc +1.2.2_extra=3_crlf=false 812817b968306e99 92205289c2cde979 +1.2.2_extra=3_crlf=false/re-run f81a1c4a49505bce ba58a7a5dce59269 +1.2.2_extra=3_crlf=true 6051efc48c2d3992 1837eb4033639a52 +1.2.2_extra=3_crlf=true/re-run e3ec0a6371139d8b ba58a7a5dce59269 +1.3.2_extra=0_crlf=false 86cda4c82eee7e8b 6bba7a02d52c265f +1.3.2_extra=0_crlf=false/re-run a9df9acaedd7db22 1f3c40d9eb7c76cc +1.3.2_extra=0_crlf=true e92e1cd1beeb0002 dcd05b3afe27840d +1.3.2_extra=0_crlf=true/re-run f13a599ff9511fe7 1f3c40d9eb7c76cc +1.3.2_extra=3_crlf=false fe9f2817767e5612 5f7d2dba58b9fc19 +1.3.2_extra=3_crlf=false/re-run d9728f0057845cbb 1a8438ef5c1e4029 +1.3.2_extra=3_crlf=true 253448255457216e 0e2a78c377278b8a +1.3.2_extra=3_crlf=true/re-run 71d0a8d8ab2ecc77 1a8438ef5c1e4029 +1.4.2_extra=0_crlf=false 00a7bc0a3c89c49b 35bf18ca91fd9d61 +1.4.2_extra=0_crlf=false/re-run 4aa5eedcf2dd0b02 1f3c40d9eb7c76cc +1.4.2_extra=0_crlf=true 44de4691acb61dba 62e231066f59791e +1.4.2_extra=0_crlf=true/re-run 47bf0330a4e93507 1f3c40d9eb7c76cc +1.4.2_extra=3_crlf=false a3fb1d70a1207ea7 952dcca3cb6d0255 +1.4.2_extra=3_crlf=false/re-run 60023b576de9ad30 1a8438ef5c1e4029 +1.4.2_extra=3_crlf=true bd8911bca06c1262 e820c0056a0e124c +1.4.2_extra=3_crlf=true/re-run a5f5292e17ecb5f4 1a8438ef5c1e4029 +1.5.1_extra=0_crlf=false a1795ca286fa80ce e93753c3978de13b +1.5.1_extra=0_crlf=false/re-run f19230652767bb30 1f3c40d9eb7c76cc +1.5.1_extra=0_crlf=true 5871c60c9d63aeee fbd73874c2c2808e +1.5.1_extra=0_crlf=true/re-run 6dab15e1eab2f462 1f3c40d9eb7c76cc +1.5.1_extra=3_crlf=false 0b33aee716ad5a55 d3c843f055aec036 +1.5.1_extra=3_crlf=false/re-run e26d96dd76e1b368 1a8438ef5c1e4029 +1.5.1_extra=3_crlf=true 161846bd33f1d508 9cbde1afb60c8400 +1.5.1_extra=3_crlf=true/re-run 3e4cf8de3fdcb6ea 1a8438ef5c1e4029 +1.6.1_extra=0_crlf=false d1e0442566c6ce49 cf501da566656c93 +1.6.1_extra=0_crlf=false/re-run ea7966d58c9f2aab 1f3c40d9eb7c76cc +1.6.1_extra=0_crlf=true db20a2c4edcafbf0 335c351e01000a64 +1.6.1_extra=0_crlf=true/re-run 8f7c22713b6f70b6 1f3c40d9eb7c76cc +1.6.1_extra=3_crlf=false afd5d875780cdf40 30e8ad1edb556a8d +1.6.1_extra=3_crlf=false/re-run 647ef4eb652f1ba6 1a8438ef5c1e4029 +1.6.1_extra=3_crlf=true b37fcda617275f4b 4264d7e80059c266 +1.6.1_extra=3_crlf=true/re-run 1a4219f03efad32a 1a8438ef5c1e4029 +1.7.1_extra=0_crlf=false 6600136814fb4134 f6b16e92d3bea560 +1.7.1_extra=0_crlf=false/re-run abdbbfe42c8b4d7e 1f3c40d9eb7c76cc +1.7.1_extra=0_crlf=true 6a85a6a27c99cd2b fc7fce47c65d34ff +1.7.1_extra=0_crlf=true/re-run ac9d84b451f9c128 1f3c40d9eb7c76cc +1.7.1_extra=3_crlf=false 020fa829b6a870bc a3cd491e00fe8d3a +1.7.1_extra=3_crlf=false/re-run 31133977b68135e7 1a8438ef5c1e4029 +1.7.1_extra=3_crlf=true bcb1fd00c2d4d5bf 33a9240a9f83dfd9 +1.7.1_extra=3_crlf=true/re-run ac38704d471666ab 1a8438ef5c1e4029 +1.8.5_extra=0_crlf=false 7b0c488a965c4f7b 5708bfd6bb9ec464 +1.8.5_extra=0_crlf=false/re-run 60ce065c6b04b5b1 1f3c40d9eb7c76cc +1.8.5_extra=0_crlf=true 2094d208abbeabc2 1d8b437489b30c3e +1.8.5_extra=0_crlf=true/re-run 5903de75cc2030e8 1f3c40d9eb7c76cc +1.8.5_extra=3_crlf=false e22381008c314e3d 0a425da029e73f6b +1.8.5_extra=3_crlf=false/re-run 147043d9e02c7a08 1a8438ef5c1e4029 +1.8.5_extra=3_crlf=true e42675d2895de9fb 4d83e93a09ad746b +1.8.5_extra=3_crlf=true/re-run 9e97459a7ac1ffb1 1a8438ef5c1e4029 +2.0.1_extra=0_crlf=false 2775b8f1411fbadb cce75b3045871657 +2.0.1_extra=0_crlf=false/re-run 5a19ccf0764fac47 1f3c40d9eb7c76cc +2.0.1_extra=0_crlf=true 17ebf61a05b76816 24db92ebcada6076 +2.0.1_extra=0_crlf=true/re-run 97eff076ab7188a2 1f3c40d9eb7c76cc +2.0.1_extra=3_crlf=false bc38767ba9dcc6e6 074baa7851946243 +2.0.1_extra=3_crlf=false/re-run 44cce38f20f8f16f 1a8438ef5c1e4029 +2.0.1_extra=3_crlf=true b68b1e02f151ef48 edcbe489b55d980a +2.0.1_extra=3_crlf=true/re-run 054b4ddcb4dcfafa 1a8438ef5c1e4029 +2.1.4_extra=0_crlf=false 6f62ddaa4adc50a4 64a60db83d599042 +2.1.4_extra=0_crlf=false/re-run 5b7e62f966b8d271 1f3c40d9eb7c76cc +2.1.4_extra=0_crlf=true 8a8d93a2b2127129 edfc2a44ac796cb1 +2.1.4_extra=0_crlf=true/re-run d1cd3eeea0692da9 1f3c40d9eb7c76cc +2.1.4_extra=3_crlf=false 65364c0f4e7aa0e7 ee85256b26e73975 +2.1.4_extra=3_crlf=false/re-run a3676e7eddbccf4a 1a8438ef5c1e4029 +2.1.4_extra=3_crlf=true 821fdbf37b56026b d36ee61ccaed1901 +2.1.4_extra=3_crlf=true/re-run 25f65d16b322be1c 1a8438ef5c1e4029 +2.2.1_extra=0_crlf=false 7a500b5022ac388c 9638a8b0c560fbcf +2.2.1_extra=0_crlf=false/re-run b484d2ecc12a5edb 1f3c40d9eb7c76cc +2.2.1_extra=0_crlf=true 11b39909d694a4f5 7a698e83c907cf50 +2.2.1_extra=0_crlf=true/re-run f73c0184185b55c8 1f3c40d9eb7c76cc +2.2.1_extra=3_crlf=false 10b0bcde3632669b 09c2370eeb4bd3d1 +2.2.1_extra=3_crlf=false/re-run 679acf31e13b71a0 1a8438ef5c1e4029 +2.2.1_extra=3_crlf=true feaa6416a4b168a3 cee2e6b74cec8768 +2.2.1_extra=3_crlf=true/re-run 4b8aa165ed772485 1a8438ef5c1e4029 +2.3.4_extra=0_crlf=false 1ad17cac9704b1ce 4bfcd86cc885c460 +2.3.4_extra=0_crlf=false/re-run 398fea1a1ac5d77b 1f3c40d9eb7c76cc +2.3.4_extra=0_crlf=true d4578a811e547b2c 904f17bb4d9785c4 +2.3.4_extra=0_crlf=true/re-run 5f79a7161605a083 1f3c40d9eb7c76cc +2.3.4_extra=3_crlf=false d38430f6b06c87f3 3175a0bc70960152 +2.3.4_extra=3_crlf=false/re-run ee7114aaad11f4f0 1a8438ef5c1e4029 +2.3.4_extra=3_crlf=true 89e72b13eccda257 e2edba8cfb04309d +2.3.4_extra=3_crlf=true/re-run 04c0dd27743659d0 1a8438ef5c1e4029 +2.4.3_extra=0_crlf=false 70d3006a3e404efc 04e301df1078ebaa +2.4.3_extra=0_crlf=false/re-run cb0289ac6716caab 1f3c40d9eb7c76cc +2.4.3_extra=0_crlf=true ebe555cb2a5fb1f2 bff9310a0d7fe5c2 +2.4.3_extra=0_crlf=true/re-run 3f6277fd5f2ef21b 1f3c40d9eb7c76cc +2.4.3_extra=3_crlf=false 7bcb0eb7f4f1150a 26a72136bfe7b634 +2.4.3_extra=3_crlf=false/re-run c7b4e742d1f0f79b 1a8438ef5c1e4029 +2.4.3_extra=3_crlf=true f1ba10a6e2549703 80b2e48af24a373c +2.4.3_extra=3_crlf=true/re-run 6c2fb99f914a5767 1a8438ef5c1e4029 diff --git a/crates/socket-patch-core/tests/fixtures/redirect/npm/yarn-berry/basic/expected-edits.json b/crates/socket-patch-core/tests/fixtures/redirect/npm/yarn-berry/basic/expected-edits.json index fbda0655c..fbb1d5546 100644 --- a/crates/socket-patch-core/tests/fixtures/redirect/npm/yarn-berry/basic/expected-edits.json +++ b/crates/socket-patch-core/tests/fixtures/redirect/npm/yarn-berry/basic/expected-edits.json @@ -1,10 +1,17 @@ [ + { + "path": "package.json", + "kind": "redirect_yarn_berry_resolution", + "action": "added", + "key": "left-pad@npm:^1.3.0", + "new": "https://patch.socket.dev/patch/npm/11111111-1111-1111-1111-111111111111/77777777-7777-7777-7777-777777777777/left-pad-1.3.0.tgz" + }, { "path": "yarn.lock", "kind": "redirect_yarn_berry_entry", "action": "rewritten", "key": "left-pad@1.3.0", "original": "\"left-pad@npm:^1.3.0\":\n version: 1.3.0\n resolution: \"left-pad@npm:1.3.0\"\n checksum: 10c0/3fb59c76e281a2f5c810ad71dbbb8eba8b10c6cf94733dc7f27b8c516a5376cacea53543e76f6ae477d866c8954b27f1e15ca349424c2542474eb5bb1d2b6955\n languageName: node\n linkType: hard", - "new": "\"left-pad@npm:^1.3.0\":\n version: 1.3.0\n resolution: \"left-pad@npm:1.3.0::__archiveUrl=https%3A%2F%2Fpatch.socket.dev%2Fpatch%2Fnpm%2F11111111-1111-1111-1111-111111111111%2F77777777-7777-7777-7777-777777777777%2Fleft-pad-1.3.0.tgz\"\n checksum: 10c0/7785879d9a7dc9bee6730ec55926a0ab9ed6bfe0eaee0cbcbcf00841d42488fddda51265c73eeddd54c5deca87d131e846ff66d27d890ef73f12720b458d7ca3\n languageName: node\n linkType: hard" + "new": "\"left-pad@https://patch.socket.dev/patch/npm/11111111-1111-1111-1111-111111111111/77777777-7777-7777-7777-777777777777/left-pad-1.3.0.tgz\":\n version: 1.3.0\n resolution: \"left-pad@https://patch.socket.dev/patch/npm/11111111-1111-1111-1111-111111111111/77777777-7777-7777-7777-777777777777/left-pad-1.3.0.tgz\"\n checksum: 10c0/7785879d9a7dc9bee6730ec55926a0ab9ed6bfe0eaee0cbcbcf00841d42488fddda51265c73eeddd54c5deca87d131e846ff66d27d890ef73f12720b458d7ca3\n languageName: node\n linkType: hard" } ] diff --git a/crates/socket-patch-core/tests/fixtures/redirect/npm/yarn-berry/basic/expected/package.json b/crates/socket-patch-core/tests/fixtures/redirect/npm/yarn-berry/basic/expected/package.json new file mode 100644 index 000000000..4e9c10438 --- /dev/null +++ b/crates/socket-patch-core/tests/fixtures/redirect/npm/yarn-berry/basic/expected/package.json @@ -0,0 +1,11 @@ +{ + "name": "consumer", + "version": "0.0.0", + "private": true, + "dependencies": { + "left-pad": "^1.3.0" + }, + "resolutions": { + "left-pad@npm:^1.3.0": "https://patch.socket.dev/patch/npm/11111111-1111-1111-1111-111111111111/77777777-7777-7777-7777-777777777777/left-pad-1.3.0.tgz" + } +} diff --git a/crates/socket-patch-core/tests/fixtures/redirect/npm/yarn-berry/basic/expected/yarn.lock b/crates/socket-patch-core/tests/fixtures/redirect/npm/yarn-berry/basic/expected/yarn.lock index 755d86f29..ed6ea3260 100644 --- a/crates/socket-patch-core/tests/fixtures/redirect/npm/yarn-berry/basic/expected/yarn.lock +++ b/crates/socket-patch-core/tests/fixtures/redirect/npm/yarn-berry/basic/expected/yarn.lock @@ -5,9 +5,9 @@ __metadata: version: 8 cacheKey: 10c0 -"left-pad@npm:^1.3.0": +"left-pad@https://patch.socket.dev/patch/npm/11111111-1111-1111-1111-111111111111/77777777-7777-7777-7777-777777777777/left-pad-1.3.0.tgz": version: 1.3.0 - resolution: "left-pad@npm:1.3.0::__archiveUrl=https%3A%2F%2Fpatch.socket.dev%2Fpatch%2Fnpm%2F11111111-1111-1111-1111-111111111111%2F77777777-7777-7777-7777-777777777777%2Fleft-pad-1.3.0.tgz" + resolution: "left-pad@https://patch.socket.dev/patch/npm/11111111-1111-1111-1111-111111111111/77777777-7777-7777-7777-777777777777/left-pad-1.3.0.tgz" checksum: 10c0/7785879d9a7dc9bee6730ec55926a0ab9ed6bfe0eaee0cbcbcf00841d42488fddda51265c73eeddd54c5deca87d131e846ff66d27d890ef73f12720b458d7ca3 languageName: node linkType: hard diff --git a/crates/socket-patch-core/tests/fixtures/redirect/npm/yarn-berry/basic/input/package.json b/crates/socket-patch-core/tests/fixtures/redirect/npm/yarn-berry/basic/input/package.json new file mode 100644 index 000000000..8761f8284 --- /dev/null +++ b/crates/socket-patch-core/tests/fixtures/redirect/npm/yarn-berry/basic/input/package.json @@ -0,0 +1,8 @@ +{ + "name": "consumer", + "version": "0.0.0", + "private": true, + "dependencies": { + "left-pad": "^1.3.0" + } +} diff --git a/crates/socket-patch-core/tests/fixtures/redirect/npm/yarn-berry/cachekey-mismatch-refusal/input/package.json b/crates/socket-patch-core/tests/fixtures/redirect/npm/yarn-berry/cachekey-mismatch-refusal/input/package.json new file mode 100644 index 000000000..8761f8284 --- /dev/null +++ b/crates/socket-patch-core/tests/fixtures/redirect/npm/yarn-berry/cachekey-mismatch-refusal/input/package.json @@ -0,0 +1,8 @@ +{ + "name": "consumer", + "version": "0.0.0", + "private": true, + "dependencies": { + "left-pad": "^1.3.0" + } +} diff --git a/crates/socket-patch-core/tests/fixtures/redirect/npm/yarn-berry/existing-archive-url/expected-edits.json b/crates/socket-patch-core/tests/fixtures/redirect/npm/yarn-berry/existing-archive-url/expected-edits.json index b27c51d32..d6961ca0b 100644 --- a/crates/socket-patch-core/tests/fixtures/redirect/npm/yarn-berry/existing-archive-url/expected-edits.json +++ b/crates/socket-patch-core/tests/fixtures/redirect/npm/yarn-berry/existing-archive-url/expected-edits.json @@ -1,10 +1,17 @@ [ + { + "path": "package.json", + "kind": "redirect_yarn_berry_resolution", + "action": "added", + "key": "left-pad@npm:^1.3.0", + "new": "https://patch.socket.dev/patch/npm/11111111-1111-1111-1111-111111111111/77777777-7777-7777-7777-777777777777/left-pad-1.3.0.tgz" + }, { "path": "yarn.lock", "kind": "redirect_yarn_berry_entry", "action": "rewritten", "key": "left-pad@1.3.0", "original": "\"left-pad@npm:^1.3.0\":\n version: 1.3.0\n resolution: \"left-pad@npm:1.3.0::__archiveUrl=https%3A%2F%2Fregistry.corp.example%2Fleft-pad-1.3.0.tgz\"\n checksum: 10c0/3fb59c76e281a2f5c810ad71dbbb8eba8b10c6cf94733dc7f27b8c516a5376cacea53543e76f6ae477d866c8954b27f1e15ca349424c2542474eb5bb1d2b6955\n languageName: node\n linkType: hard", - "new": "\"left-pad@npm:^1.3.0\":\n version: 1.3.0\n resolution: \"left-pad@npm:1.3.0::__archiveUrl=https%3A%2F%2Fpatch.socket.dev%2Fpatch%2Fnpm%2F11111111-1111-1111-1111-111111111111%2F77777777-7777-7777-7777-777777777777%2Fleft-pad-1.3.0.tgz\"\n checksum: 10c0/7785879d9a7dc9bee6730ec55926a0ab9ed6bfe0eaee0cbcbcf00841d42488fddda51265c73eeddd54c5deca87d131e846ff66d27d890ef73f12720b458d7ca3\n languageName: node\n linkType: hard" + "new": "\"left-pad@https://patch.socket.dev/patch/npm/11111111-1111-1111-1111-111111111111/77777777-7777-7777-7777-777777777777/left-pad-1.3.0.tgz\":\n version: 1.3.0\n resolution: \"left-pad@https://patch.socket.dev/patch/npm/11111111-1111-1111-1111-111111111111/77777777-7777-7777-7777-777777777777/left-pad-1.3.0.tgz\"\n checksum: 10c0/7785879d9a7dc9bee6730ec55926a0ab9ed6bfe0eaee0cbcbcf00841d42488fddda51265c73eeddd54c5deca87d131e846ff66d27d890ef73f12720b458d7ca3\n languageName: node\n linkType: hard" } ] diff --git a/crates/socket-patch-core/tests/fixtures/redirect/npm/yarn-berry/existing-archive-url/expected/package.json b/crates/socket-patch-core/tests/fixtures/redirect/npm/yarn-berry/existing-archive-url/expected/package.json new file mode 100644 index 000000000..4e9c10438 --- /dev/null +++ b/crates/socket-patch-core/tests/fixtures/redirect/npm/yarn-berry/existing-archive-url/expected/package.json @@ -0,0 +1,11 @@ +{ + "name": "consumer", + "version": "0.0.0", + "private": true, + "dependencies": { + "left-pad": "^1.3.0" + }, + "resolutions": { + "left-pad@npm:^1.3.0": "https://patch.socket.dev/patch/npm/11111111-1111-1111-1111-111111111111/77777777-7777-7777-7777-777777777777/left-pad-1.3.0.tgz" + } +} diff --git a/crates/socket-patch-core/tests/fixtures/redirect/npm/yarn-berry/existing-archive-url/expected/yarn.lock b/crates/socket-patch-core/tests/fixtures/redirect/npm/yarn-berry/existing-archive-url/expected/yarn.lock index 755d86f29..ed6ea3260 100644 --- a/crates/socket-patch-core/tests/fixtures/redirect/npm/yarn-berry/existing-archive-url/expected/yarn.lock +++ b/crates/socket-patch-core/tests/fixtures/redirect/npm/yarn-berry/existing-archive-url/expected/yarn.lock @@ -5,9 +5,9 @@ __metadata: version: 8 cacheKey: 10c0 -"left-pad@npm:^1.3.0": +"left-pad@https://patch.socket.dev/patch/npm/11111111-1111-1111-1111-111111111111/77777777-7777-7777-7777-777777777777/left-pad-1.3.0.tgz": version: 1.3.0 - resolution: "left-pad@npm:1.3.0::__archiveUrl=https%3A%2F%2Fpatch.socket.dev%2Fpatch%2Fnpm%2F11111111-1111-1111-1111-111111111111%2F77777777-7777-7777-7777-777777777777%2Fleft-pad-1.3.0.tgz" + resolution: "left-pad@https://patch.socket.dev/patch/npm/11111111-1111-1111-1111-111111111111/77777777-7777-7777-7777-777777777777/left-pad-1.3.0.tgz" checksum: 10c0/7785879d9a7dc9bee6730ec55926a0ab9ed6bfe0eaee0cbcbcf00841d42488fddda51265c73eeddd54c5deca87d131e846ff66d27d890ef73f12720b458d7ca3 languageName: node linkType: hard diff --git a/crates/socket-patch-core/tests/fixtures/redirect/npm/yarn-berry/existing-archive-url/input/package.json b/crates/socket-patch-core/tests/fixtures/redirect/npm/yarn-berry/existing-archive-url/input/package.json new file mode 100644 index 000000000..8761f8284 --- /dev/null +++ b/crates/socket-patch-core/tests/fixtures/redirect/npm/yarn-berry/existing-archive-url/input/package.json @@ -0,0 +1,8 @@ +{ + "name": "consumer", + "version": "0.0.0", + "private": true, + "dependencies": { + "left-pad": "^1.3.0" + } +} diff --git a/crates/socket-patch-core/tests/fixtures/redirect/npm/yarn-berry/missing-berry-checksum/input/package.json b/crates/socket-patch-core/tests/fixtures/redirect/npm/yarn-berry/missing-berry-checksum/input/package.json new file mode 100644 index 000000000..8761f8284 --- /dev/null +++ b/crates/socket-patch-core/tests/fixtures/redirect/npm/yarn-berry/missing-berry-checksum/input/package.json @@ -0,0 +1,8 @@ +{ + "name": "consumer", + "version": "0.0.0", + "private": true, + "dependencies": { + "left-pad": "^1.3.0" + } +} diff --git a/crates/socket-patch-core/tests/fixtures/redirect/npm/yarn-berry/multi-descriptor-key/expected-edits.json b/crates/socket-patch-core/tests/fixtures/redirect/npm/yarn-berry/multi-descriptor-key/expected-edits.json index e89ed4d26..2407eb8a0 100644 --- a/crates/socket-patch-core/tests/fixtures/redirect/npm/yarn-berry/multi-descriptor-key/expected-edits.json +++ b/crates/socket-patch-core/tests/fixtures/redirect/npm/yarn-berry/multi-descriptor-key/expected-edits.json @@ -1,10 +1,24 @@ [ + { + "path": "package.json", + "kind": "redirect_yarn_berry_resolution", + "action": "added", + "key": "left-pad@npm:^1.0.0", + "new": "https://patch.socket.dev/patch/npm/11111111-1111-1111-1111-111111111111/77777777-7777-7777-7777-777777777777/left-pad-1.3.0.tgz" + }, + { + "path": "package.json", + "kind": "redirect_yarn_berry_resolution", + "action": "added", + "key": "left-pad@npm:^1.3.0", + "new": "https://patch.socket.dev/patch/npm/11111111-1111-1111-1111-111111111111/77777777-7777-7777-7777-777777777777/left-pad-1.3.0.tgz" + }, { "path": "yarn.lock", "kind": "redirect_yarn_berry_entry", "action": "rewritten", "key": "left-pad@1.3.0", "original": "\"left-pad@npm:^1.0.0, left-pad@npm:^1.3.0\":\n version: 1.3.0\n resolution: \"left-pad@npm:1.3.0\"\n checksum: 10c0/3fb59c76e281a2f5c810ad71dbbb8eba8b10c6cf94733dc7f27b8c516a5376cacea53543e76f6ae477d866c8954b27f1e15ca349424c2542474eb5bb1d2b6955\n languageName: node\n linkType: hard", - "new": "\"left-pad@npm:^1.0.0, left-pad@npm:^1.3.0\":\n version: 1.3.0\n resolution: \"left-pad@npm:1.3.0::__archiveUrl=https%3A%2F%2Fpatch.socket.dev%2Fpatch%2Fnpm%2F11111111-1111-1111-1111-111111111111%2F77777777-7777-7777-7777-777777777777%2Fleft-pad-1.3.0.tgz\"\n checksum: 10c0/7785879d9a7dc9bee6730ec55926a0ab9ed6bfe0eaee0cbcbcf00841d42488fddda51265c73eeddd54c5deca87d131e846ff66d27d890ef73f12720b458d7ca3\n languageName: node\n linkType: hard" + "new": "\"left-pad@https://patch.socket.dev/patch/npm/11111111-1111-1111-1111-111111111111/77777777-7777-7777-7777-777777777777/left-pad-1.3.0.tgz\":\n version: 1.3.0\n resolution: \"left-pad@https://patch.socket.dev/patch/npm/11111111-1111-1111-1111-111111111111/77777777-7777-7777-7777-777777777777/left-pad-1.3.0.tgz\"\n checksum: 10c0/7785879d9a7dc9bee6730ec55926a0ab9ed6bfe0eaee0cbcbcf00841d42488fddda51265c73eeddd54c5deca87d131e846ff66d27d890ef73f12720b458d7ca3\n languageName: node\n linkType: hard" } ] diff --git a/crates/socket-patch-core/tests/fixtures/redirect/npm/yarn-berry/multi-descriptor-key/expected/package.json b/crates/socket-patch-core/tests/fixtures/redirect/npm/yarn-berry/multi-descriptor-key/expected/package.json new file mode 100644 index 000000000..6835dfba5 --- /dev/null +++ b/crates/socket-patch-core/tests/fixtures/redirect/npm/yarn-berry/multi-descriptor-key/expected/package.json @@ -0,0 +1,12 @@ +{ + "name": "consumer", + "version": "0.0.0", + "private": true, + "dependencies": { + "left-pad": "^1.3.0" + }, + "resolutions": { + "left-pad@npm:^1.0.0": "https://patch.socket.dev/patch/npm/11111111-1111-1111-1111-111111111111/77777777-7777-7777-7777-777777777777/left-pad-1.3.0.tgz", + "left-pad@npm:^1.3.0": "https://patch.socket.dev/patch/npm/11111111-1111-1111-1111-111111111111/77777777-7777-7777-7777-777777777777/left-pad-1.3.0.tgz" + } +} diff --git a/crates/socket-patch-core/tests/fixtures/redirect/npm/yarn-berry/multi-descriptor-key/expected/yarn.lock b/crates/socket-patch-core/tests/fixtures/redirect/npm/yarn-berry/multi-descriptor-key/expected/yarn.lock index 8ab2423d9..ed6ea3260 100644 --- a/crates/socket-patch-core/tests/fixtures/redirect/npm/yarn-berry/multi-descriptor-key/expected/yarn.lock +++ b/crates/socket-patch-core/tests/fixtures/redirect/npm/yarn-berry/multi-descriptor-key/expected/yarn.lock @@ -5,9 +5,9 @@ __metadata: version: 8 cacheKey: 10c0 -"left-pad@npm:^1.0.0, left-pad@npm:^1.3.0": +"left-pad@https://patch.socket.dev/patch/npm/11111111-1111-1111-1111-111111111111/77777777-7777-7777-7777-777777777777/left-pad-1.3.0.tgz": version: 1.3.0 - resolution: "left-pad@npm:1.3.0::__archiveUrl=https%3A%2F%2Fpatch.socket.dev%2Fpatch%2Fnpm%2F11111111-1111-1111-1111-111111111111%2F77777777-7777-7777-7777-777777777777%2Fleft-pad-1.3.0.tgz" + resolution: "left-pad@https://patch.socket.dev/patch/npm/11111111-1111-1111-1111-111111111111/77777777-7777-7777-7777-777777777777/left-pad-1.3.0.tgz" checksum: 10c0/7785879d9a7dc9bee6730ec55926a0ab9ed6bfe0eaee0cbcbcf00841d42488fddda51265c73eeddd54c5deca87d131e846ff66d27d890ef73f12720b458d7ca3 languageName: node linkType: hard diff --git a/crates/socket-patch-core/tests/fixtures/redirect/npm/yarn-berry/multi-descriptor-key/input/package.json b/crates/socket-patch-core/tests/fixtures/redirect/npm/yarn-berry/multi-descriptor-key/input/package.json new file mode 100644 index 000000000..8761f8284 --- /dev/null +++ b/crates/socket-patch-core/tests/fixtures/redirect/npm/yarn-berry/multi-descriptor-key/input/package.json @@ -0,0 +1,8 @@ +{ + "name": "consumer", + "version": "0.0.0", + "private": true, + "dependencies": { + "left-pad": "^1.3.0" + } +} diff --git a/crates/socket-patch-core/tests/fixtures/redirect/npm/yarn-berry/multiple-versions/expected-edits.json b/crates/socket-patch-core/tests/fixtures/redirect/npm/yarn-berry/multiple-versions/expected-edits.json index fbda0655c..fbb1d5546 100644 --- a/crates/socket-patch-core/tests/fixtures/redirect/npm/yarn-berry/multiple-versions/expected-edits.json +++ b/crates/socket-patch-core/tests/fixtures/redirect/npm/yarn-berry/multiple-versions/expected-edits.json @@ -1,10 +1,17 @@ [ + { + "path": "package.json", + "kind": "redirect_yarn_berry_resolution", + "action": "added", + "key": "left-pad@npm:^1.3.0", + "new": "https://patch.socket.dev/patch/npm/11111111-1111-1111-1111-111111111111/77777777-7777-7777-7777-777777777777/left-pad-1.3.0.tgz" + }, { "path": "yarn.lock", "kind": "redirect_yarn_berry_entry", "action": "rewritten", "key": "left-pad@1.3.0", "original": "\"left-pad@npm:^1.3.0\":\n version: 1.3.0\n resolution: \"left-pad@npm:1.3.0\"\n checksum: 10c0/3fb59c76e281a2f5c810ad71dbbb8eba8b10c6cf94733dc7f27b8c516a5376cacea53543e76f6ae477d866c8954b27f1e15ca349424c2542474eb5bb1d2b6955\n languageName: node\n linkType: hard", - "new": "\"left-pad@npm:^1.3.0\":\n version: 1.3.0\n resolution: \"left-pad@npm:1.3.0::__archiveUrl=https%3A%2F%2Fpatch.socket.dev%2Fpatch%2Fnpm%2F11111111-1111-1111-1111-111111111111%2F77777777-7777-7777-7777-777777777777%2Fleft-pad-1.3.0.tgz\"\n checksum: 10c0/7785879d9a7dc9bee6730ec55926a0ab9ed6bfe0eaee0cbcbcf00841d42488fddda51265c73eeddd54c5deca87d131e846ff66d27d890ef73f12720b458d7ca3\n languageName: node\n linkType: hard" + "new": "\"left-pad@https://patch.socket.dev/patch/npm/11111111-1111-1111-1111-111111111111/77777777-7777-7777-7777-777777777777/left-pad-1.3.0.tgz\":\n version: 1.3.0\n resolution: \"left-pad@https://patch.socket.dev/patch/npm/11111111-1111-1111-1111-111111111111/77777777-7777-7777-7777-777777777777/left-pad-1.3.0.tgz\"\n checksum: 10c0/7785879d9a7dc9bee6730ec55926a0ab9ed6bfe0eaee0cbcbcf00841d42488fddda51265c73eeddd54c5deca87d131e846ff66d27d890ef73f12720b458d7ca3\n languageName: node\n linkType: hard" } ] diff --git a/crates/socket-patch-core/tests/fixtures/redirect/npm/yarn-berry/multiple-versions/expected/package.json b/crates/socket-patch-core/tests/fixtures/redirect/npm/yarn-berry/multiple-versions/expected/package.json new file mode 100644 index 000000000..4e9c10438 --- /dev/null +++ b/crates/socket-patch-core/tests/fixtures/redirect/npm/yarn-berry/multiple-versions/expected/package.json @@ -0,0 +1,11 @@ +{ + "name": "consumer", + "version": "0.0.0", + "private": true, + "dependencies": { + "left-pad": "^1.3.0" + }, + "resolutions": { + "left-pad@npm:^1.3.0": "https://patch.socket.dev/patch/npm/11111111-1111-1111-1111-111111111111/77777777-7777-7777-7777-777777777777/left-pad-1.3.0.tgz" + } +} diff --git a/crates/socket-patch-core/tests/fixtures/redirect/npm/yarn-berry/multiple-versions/expected/yarn.lock b/crates/socket-patch-core/tests/fixtures/redirect/npm/yarn-berry/multiple-versions/expected/yarn.lock index 473795b55..585da446a 100644 --- a/crates/socket-patch-core/tests/fixtures/redirect/npm/yarn-berry/multiple-versions/expected/yarn.lock +++ b/crates/socket-patch-core/tests/fixtures/redirect/npm/yarn-berry/multiple-versions/expected/yarn.lock @@ -12,9 +12,9 @@ __metadata: languageName: node linkType: hard -"left-pad@npm:^1.3.0": +"left-pad@https://patch.socket.dev/patch/npm/11111111-1111-1111-1111-111111111111/77777777-7777-7777-7777-777777777777/left-pad-1.3.0.tgz": version: 1.3.0 - resolution: "left-pad@npm:1.3.0::__archiveUrl=https%3A%2F%2Fpatch.socket.dev%2Fpatch%2Fnpm%2F11111111-1111-1111-1111-111111111111%2F77777777-7777-7777-7777-777777777777%2Fleft-pad-1.3.0.tgz" + resolution: "left-pad@https://patch.socket.dev/patch/npm/11111111-1111-1111-1111-111111111111/77777777-7777-7777-7777-777777777777/left-pad-1.3.0.tgz" checksum: 10c0/7785879d9a7dc9bee6730ec55926a0ab9ed6bfe0eaee0cbcbcf00841d42488fddda51265c73eeddd54c5deca87d131e846ff66d27d890ef73f12720b458d7ca3 languageName: node linkType: hard diff --git a/crates/socket-patch-core/tests/fixtures/redirect/npm/yarn-berry/multiple-versions/input/package.json b/crates/socket-patch-core/tests/fixtures/redirect/npm/yarn-berry/multiple-versions/input/package.json new file mode 100644 index 000000000..8761f8284 --- /dev/null +++ b/crates/socket-patch-core/tests/fixtures/redirect/npm/yarn-berry/multiple-versions/input/package.json @@ -0,0 +1,8 @@ +{ + "name": "consumer", + "version": "0.0.0", + "private": true, + "dependencies": { + "left-pad": "^1.3.0" + } +} diff --git a/crates/socket-patch-core/tests/fixtures/redirect/npm/yarn-berry/rerun-noop/input/package.json b/crates/socket-patch-core/tests/fixtures/redirect/npm/yarn-berry/rerun-noop/input/package.json new file mode 100644 index 000000000..4e9c10438 --- /dev/null +++ b/crates/socket-patch-core/tests/fixtures/redirect/npm/yarn-berry/rerun-noop/input/package.json @@ -0,0 +1,11 @@ +{ + "name": "consumer", + "version": "0.0.0", + "private": true, + "dependencies": { + "left-pad": "^1.3.0" + }, + "resolutions": { + "left-pad@npm:^1.3.0": "https://patch.socket.dev/patch/npm/11111111-1111-1111-1111-111111111111/77777777-7777-7777-7777-777777777777/left-pad-1.3.0.tgz" + } +} diff --git a/crates/socket-patch-core/tests/fixtures/redirect/npm/yarn-berry/rerun-noop/input/yarn.lock b/crates/socket-patch-core/tests/fixtures/redirect/npm/yarn-berry/rerun-noop/input/yarn.lock index 755d86f29..ed6ea3260 100644 --- a/crates/socket-patch-core/tests/fixtures/redirect/npm/yarn-berry/rerun-noop/input/yarn.lock +++ b/crates/socket-patch-core/tests/fixtures/redirect/npm/yarn-berry/rerun-noop/input/yarn.lock @@ -5,9 +5,9 @@ __metadata: version: 8 cacheKey: 10c0 -"left-pad@npm:^1.3.0": +"left-pad@https://patch.socket.dev/patch/npm/11111111-1111-1111-1111-111111111111/77777777-7777-7777-7777-777777777777/left-pad-1.3.0.tgz": version: 1.3.0 - resolution: "left-pad@npm:1.3.0::__archiveUrl=https%3A%2F%2Fpatch.socket.dev%2Fpatch%2Fnpm%2F11111111-1111-1111-1111-111111111111%2F77777777-7777-7777-7777-777777777777%2Fleft-pad-1.3.0.tgz" + resolution: "left-pad@https://patch.socket.dev/patch/npm/11111111-1111-1111-1111-111111111111/77777777-7777-7777-7777-777777777777/left-pad-1.3.0.tgz" checksum: 10c0/7785879d9a7dc9bee6730ec55926a0ab9ed6bfe0eaee0cbcbcf00841d42488fddda51265c73eeddd54c5deca87d131e846ff66d27d890ef73f12720b458d7ca3 languageName: node linkType: hard diff --git a/crates/socket-patch-core/tests/fixtures/redirect/npm/yarn-berry/scoped-package/expected-edits.json b/crates/socket-patch-core/tests/fixtures/redirect/npm/yarn-berry/scoped-package/expected-edits.json index 825155ac4..20665c51d 100644 --- a/crates/socket-patch-core/tests/fixtures/redirect/npm/yarn-berry/scoped-package/expected-edits.json +++ b/crates/socket-patch-core/tests/fixtures/redirect/npm/yarn-berry/scoped-package/expected-edits.json @@ -1,10 +1,17 @@ [ + { + "path": "package.json", + "kind": "redirect_yarn_berry_resolution", + "action": "added", + "key": "@babel/core@npm:^7.0.0", + "new": "https://patch.socket.dev/patch/npm/11111111-1111-1111-1111-111111111111/77777777-7777-7777-7777-777777777777/left-pad-1.3.0.tgz" + }, { "path": "yarn.lock", "kind": "redirect_yarn_berry_entry", "action": "rewritten", "key": "@babel/core@7.0.0", "original": "\"@babel/core@npm:^7.0.0\":\n version: 7.0.0\n resolution: \"@babel/core@npm:7.0.0\"\n checksum: 10c0/3fb59c76e281a2f5c810ad71dbbb8eba8b10c6cf94733dc7f27b8c516a5376cacea53543e76f6ae477d866c8954b27f1e15ca349424c2542474eb5bb1d2b6955\n languageName: node\n linkType: hard", - "new": "\"@babel/core@npm:^7.0.0\":\n version: 7.0.0\n resolution: \"@babel/core@npm:7.0.0::__archiveUrl=https%3A%2F%2Fpatch.socket.dev%2Fpatch%2Fnpm%2F11111111-1111-1111-1111-111111111111%2F77777777-7777-7777-7777-777777777777%2Fleft-pad-1.3.0.tgz\"\n checksum: 10c0/7785879d9a7dc9bee6730ec55926a0ab9ed6bfe0eaee0cbcbcf00841d42488fddda51265c73eeddd54c5deca87d131e846ff66d27d890ef73f12720b458d7ca3\n languageName: node\n linkType: hard" + "new": "\"@babel/core@https://patch.socket.dev/patch/npm/11111111-1111-1111-1111-111111111111/77777777-7777-7777-7777-777777777777/left-pad-1.3.0.tgz\":\n version: 7.0.0\n resolution: \"@babel/core@https://patch.socket.dev/patch/npm/11111111-1111-1111-1111-111111111111/77777777-7777-7777-7777-777777777777/left-pad-1.3.0.tgz\"\n checksum: 10c0/7785879d9a7dc9bee6730ec55926a0ab9ed6bfe0eaee0cbcbcf00841d42488fddda51265c73eeddd54c5deca87d131e846ff66d27d890ef73f12720b458d7ca3\n languageName: node\n linkType: hard" } ] diff --git a/crates/socket-patch-core/tests/fixtures/redirect/npm/yarn-berry/scoped-package/expected/package.json b/crates/socket-patch-core/tests/fixtures/redirect/npm/yarn-berry/scoped-package/expected/package.json new file mode 100644 index 000000000..509abe827 --- /dev/null +++ b/crates/socket-patch-core/tests/fixtures/redirect/npm/yarn-berry/scoped-package/expected/package.json @@ -0,0 +1,11 @@ +{ + "name": "consumer", + "version": "0.0.0", + "private": true, + "dependencies": { + "left-pad": "^1.3.0" + }, + "resolutions": { + "@babel/core@npm:^7.0.0": "https://patch.socket.dev/patch/npm/11111111-1111-1111-1111-111111111111/77777777-7777-7777-7777-777777777777/left-pad-1.3.0.tgz" + } +} diff --git a/crates/socket-patch-core/tests/fixtures/redirect/npm/yarn-berry/scoped-package/expected/yarn.lock b/crates/socket-patch-core/tests/fixtures/redirect/npm/yarn-berry/scoped-package/expected/yarn.lock index 77cf3b868..e93309e14 100644 --- a/crates/socket-patch-core/tests/fixtures/redirect/npm/yarn-berry/scoped-package/expected/yarn.lock +++ b/crates/socket-patch-core/tests/fixtures/redirect/npm/yarn-berry/scoped-package/expected/yarn.lock @@ -5,9 +5,9 @@ __metadata: version: 8 cacheKey: 10c0 -"@babel/core@npm:^7.0.0": +"@babel/core@https://patch.socket.dev/patch/npm/11111111-1111-1111-1111-111111111111/77777777-7777-7777-7777-777777777777/left-pad-1.3.0.tgz": version: 7.0.0 - resolution: "@babel/core@npm:7.0.0::__archiveUrl=https%3A%2F%2Fpatch.socket.dev%2Fpatch%2Fnpm%2F11111111-1111-1111-1111-111111111111%2F77777777-7777-7777-7777-777777777777%2Fleft-pad-1.3.0.tgz" + resolution: "@babel/core@https://patch.socket.dev/patch/npm/11111111-1111-1111-1111-111111111111/77777777-7777-7777-7777-777777777777/left-pad-1.3.0.tgz" checksum: 10c0/7785879d9a7dc9bee6730ec55926a0ab9ed6bfe0eaee0cbcbcf00841d42488fddda51265c73eeddd54c5deca87d131e846ff66d27d890ef73f12720b458d7ca3 languageName: node linkType: hard diff --git a/crates/socket-patch-core/tests/fixtures/redirect/npm/yarn-berry/scoped-package/input/package.json b/crates/socket-patch-core/tests/fixtures/redirect/npm/yarn-berry/scoped-package/input/package.json new file mode 100644 index 000000000..8761f8284 --- /dev/null +++ b/crates/socket-patch-core/tests/fixtures/redirect/npm/yarn-berry/scoped-package/input/package.json @@ -0,0 +1,8 @@ +{ + "name": "consumer", + "version": "0.0.0", + "private": true, + "dependencies": { + "left-pad": "^1.3.0" + } +} diff --git a/crates/socket-patch-core/tests/fixtures/redirect/npm/yarn-berry/yarnrc-compression-refusal/input/package.json b/crates/socket-patch-core/tests/fixtures/redirect/npm/yarn-berry/yarnrc-compression-refusal/input/package.json new file mode 100644 index 000000000..8761f8284 --- /dev/null +++ b/crates/socket-patch-core/tests/fixtures/redirect/npm/yarn-berry/yarnrc-compression-refusal/input/package.json @@ -0,0 +1,8 @@ +{ + "name": "consumer", + "version": "0.0.0", + "private": true, + "dependencies": { + "left-pad": "^1.3.0" + } +} diff --git a/crates/socket-patch-core/tests/fixtures/vex-discover-golden/redirect-npm.json b/crates/socket-patch-core/tests/fixtures/vex-discover-golden/redirect-npm.json index 4b37774b7..2130028cb 100644 --- a/crates/socket-patch-core/tests/fixtures/vex-discover-golden/redirect-npm.json +++ b/crates/socket-patch-core/tests/fixtures/vex-discover-golden/redirect-npm.json @@ -6409,17 +6409,27 @@ "artifact_rel": null, "locked_integrity": "BerryChecksum(\"10c0/7785879d9a7dc9bee6730ec55926a0ab9ed6bfe0eaee0cbcbcf00841d42488fddda51265c73eeddd54c5deca87d131e846ff66d27d890ef73f12720b458d7ca3\")", "integrity_required": true, - "url": "https%3A%2F%2Fpatch.socket.dev%2Fpatch%2Fnpm%2F11111111-1111-1111-1111-111111111111%2F77777777-7777-7777-7777-777777777777%2Fleft-pad-1.3.0.tgz", + "url": "https://patch.socket.dev/patch/npm/11111111-1111-1111-1111-111111111111/77777777-7777-7777-7777-777777777777/left-pad-1.3.0.tgz", "lockfile_basis_ok": true } ], "diagnostics": [], "recognized": [ + { + "uuid": "11111111-1111-1111-1111-111111111111", + "mode": "hosted", + "file": "package.json" + }, { "uuid": "11111111-1111-1111-1111-111111111111", "mode": "hosted", "file": "yarn.lock" }, + { + "uuid": "77777777-7777-7777-7777-777777777777", + "mode": "hosted", + "file": "package.json" + }, { "uuid": "77777777-7777-7777-7777-777777777777", "mode": "hosted", @@ -6472,17 +6482,27 @@ "artifact_rel": null, "locked_integrity": "BerryChecksum(\"10c0/7785879d9a7dc9bee6730ec55926a0ab9ed6bfe0eaee0cbcbcf00841d42488fddda51265c73eeddd54c5deca87d131e846ff66d27d890ef73f12720b458d7ca3\")", "integrity_required": true, - "url": "https%3A%2F%2Fpatch.socket.dev%2Fpatch%2Fnpm%2F11111111-1111-1111-1111-111111111111%2F77777777-7777-7777-7777-777777777777%2Fleft-pad-1.3.0.tgz", + "url": "https://patch.socket.dev/patch/npm/11111111-1111-1111-1111-111111111111/77777777-7777-7777-7777-777777777777/left-pad-1.3.0.tgz", "lockfile_basis_ok": true } ], "diagnostics": [], "recognized": [ + { + "uuid": "11111111-1111-1111-1111-111111111111", + "mode": "hosted", + "file": "package.json" + }, { "uuid": "11111111-1111-1111-1111-111111111111", "mode": "hosted", "file": "yarn.lock" }, + { + "uuid": "77777777-7777-7777-7777-777777777777", + "mode": "hosted", + "file": "package.json" + }, { "uuid": "77777777-7777-7777-7777-777777777777", "mode": "hosted", @@ -6535,17 +6555,27 @@ "artifact_rel": null, "locked_integrity": "BerryChecksum(\"10c0/7785879d9a7dc9bee6730ec55926a0ab9ed6bfe0eaee0cbcbcf00841d42488fddda51265c73eeddd54c5deca87d131e846ff66d27d890ef73f12720b458d7ca3\")", "integrity_required": true, - "url": "https%3A%2F%2Fpatch.socket.dev%2Fpatch%2Fnpm%2F11111111-1111-1111-1111-111111111111%2F77777777-7777-7777-7777-777777777777%2Fleft-pad-1.3.0.tgz", + "url": "https://patch.socket.dev/patch/npm/11111111-1111-1111-1111-111111111111/77777777-7777-7777-7777-777777777777/left-pad-1.3.0.tgz", "lockfile_basis_ok": true } ], "diagnostics": [], "recognized": [ + { + "uuid": "11111111-1111-1111-1111-111111111111", + "mode": "hosted", + "file": "package.json" + }, { "uuid": "11111111-1111-1111-1111-111111111111", "mode": "hosted", "file": "yarn.lock" }, + { + "uuid": "77777777-7777-7777-7777-777777777777", + "mode": "hosted", + "file": "package.json" + }, { "uuid": "77777777-7777-7777-7777-777777777777", "mode": "hosted", @@ -6585,17 +6615,27 @@ "artifact_rel": null, "locked_integrity": "BerryChecksum(\"10c0/7785879d9a7dc9bee6730ec55926a0ab9ed6bfe0eaee0cbcbcf00841d42488fddda51265c73eeddd54c5deca87d131e846ff66d27d890ef73f12720b458d7ca3\")", "integrity_required": true, - "url": "https%3A%2F%2Fpatch.socket.dev%2Fpatch%2Fnpm%2F11111111-1111-1111-1111-111111111111%2F77777777-7777-7777-7777-777777777777%2Fleft-pad-1.3.0.tgz", + "url": "https://patch.socket.dev/patch/npm/11111111-1111-1111-1111-111111111111/77777777-7777-7777-7777-777777777777/left-pad-1.3.0.tgz", "lockfile_basis_ok": true } ], "diagnostics": [], "recognized": [ + { + "uuid": "11111111-1111-1111-1111-111111111111", + "mode": "hosted", + "file": "package.json" + }, { "uuid": "11111111-1111-1111-1111-111111111111", "mode": "hosted", "file": "yarn.lock" }, + { + "uuid": "77777777-7777-7777-7777-777777777777", + "mode": "hosted", + "file": "package.json" + }, { "uuid": "77777777-7777-7777-7777-777777777777", "mode": "hosted", @@ -6644,17 +6684,27 @@ "artifact_rel": null, "locked_integrity": "BerryChecksum(\"10c0/7785879d9a7dc9bee6730ec55926a0ab9ed6bfe0eaee0cbcbcf00841d42488fddda51265c73eeddd54c5deca87d131e846ff66d27d890ef73f12720b458d7ca3\")", "integrity_required": true, - "url": "https%3A%2F%2Fpatch.socket.dev%2Fpatch%2Fnpm%2F11111111-1111-1111-1111-111111111111%2F77777777-7777-7777-7777-777777777777%2Fleft-pad-1.3.0.tgz", + "url": "https://patch.socket.dev/patch/npm/11111111-1111-1111-1111-111111111111/77777777-7777-7777-7777-777777777777/left-pad-1.3.0.tgz", "lockfile_basis_ok": true } ], "diagnostics": [], "recognized": [ + { + "uuid": "11111111-1111-1111-1111-111111111111", + "mode": "hosted", + "file": "package.json" + }, { "uuid": "11111111-1111-1111-1111-111111111111", "mode": "hosted", "file": "yarn.lock" }, + { + "uuid": "77777777-7777-7777-7777-777777777777", + "mode": "hosted", + "file": "package.json" + }, { "uuid": "77777777-7777-7777-7777-777777777777", "mode": "hosted", @@ -6681,17 +6731,27 @@ "artifact_rel": null, "locked_integrity": "BerryChecksum(\"10c0/7785879d9a7dc9bee6730ec55926a0ab9ed6bfe0eaee0cbcbcf00841d42488fddda51265c73eeddd54c5deca87d131e846ff66d27d890ef73f12720b458d7ca3\")", "integrity_required": true, - "url": "https%3A%2F%2Fpatch.socket.dev%2Fpatch%2Fnpm%2F11111111-1111-1111-1111-111111111111%2F77777777-7777-7777-7777-777777777777%2Fleft-pad-1.3.0.tgz", + "url": "https://patch.socket.dev/patch/npm/11111111-1111-1111-1111-111111111111/77777777-7777-7777-7777-777777777777/left-pad-1.3.0.tgz", "lockfile_basis_ok": true } ], "diagnostics": [], "recognized": [ + { + "uuid": "11111111-1111-1111-1111-111111111111", + "mode": "hosted", + "file": "package.json" + }, { "uuid": "11111111-1111-1111-1111-111111111111", "mode": "hosted", "file": "yarn.lock" }, + { + "uuid": "77777777-7777-7777-7777-777777777777", + "mode": "hosted", + "file": "package.json" + }, { "uuid": "77777777-7777-7777-7777-777777777777", "mode": "hosted", diff --git a/docs/ecosystems.md b/docs/ecosystems.md index c6ab78391..ebb07765c 100644 --- a/docs/ecosystems.md +++ b/docs/ecosystems.md @@ -75,8 +75,13 @@ The backticked slug in each row is the value `-e`/`--ecosystems` accepts (e.g. upstream bytes. Use a clean install tree and an empty store; `--force` is not a reliable substitute. Run `socket-patch vex` after installation to verify the patched files. See the [compatibility matrix and workflow](testing/pnpm-compatibility.md). -- **yarn berry** — the redirect edits the `yarn.lock` entry only (cacheKey `10c0` / - yarn 4), and `.yarnrc.yml`'s `compressionLevel` must stay 0. The node-modules linker +- **yarn berry** — the redirect pins the way yarn does for a root `resolutions` + entry (cacheKey `10c0` / yarn 4): `package.json` routes the locked descriptor + (`"left-pad@npm:^1.3.0"`) to the hosted tarball and only that `yarn.lock` entry + is re-keyed by it. Yarn then fetches it without npm registry credentials and + hardened mode accepts it. A user-authored `resolutions` entry for the package + is never overwritten (`redirect_yarn_berry_resolutions_conflict`), and + `.yarnrc.yml`'s `compressionLevel` must stay 0. The node-modules linker is e2e-covered; PnP is untested for hosted — the lock rewrite fires, but PnP's `.yarn/cache` resolution isn't exercised. CRLF locks — what yarn writes on Windows, and what a `core.autocrlf` checkout produces anywhere — are rewritten in their own diff --git a/docs/testing/yarn-berry-compatibility.md b/docs/testing/yarn-berry-compatibility.md index 5bd45eee3..4ea792d22 100644 --- a/docs/testing/yarn-berry-compatibility.md +++ b/docs/testing/yarn-berry-compatibility.md @@ -2,8 +2,10 @@ socket-patch supports yarn berry at cacheKey `10c0` — yarn 4 with the default `compressionLevel: 0`, the one cache-zip checksum recipe it can reproduce -offline — in both modes: hosted (`scan --mode hosted` rewrites the lock entry -to the hosted `::__archiveUrl=`) and vendored (`vendor` wires the root +offline — in both modes: hosted (`scan --mode hosted` routes the locked +descriptors to the hosted tarball through root `package.json` `resolutions` and +re-keys the lock entry `@https://…/-.tgz`, see below) and +vendored (`vendor` wires the root `package.json` `resolutions` plus the lock's `file:` entry). yarn 2 and 3 (cacheKeys `7` / `8`) are refused by both modes. The node-modules and pnpm linkers are covered end to end; Plug'n'Play keeps packages inside @@ -11,6 +13,61 @@ linkers are covered end to end; Plug'n'Play keeps packages inside and so does `apply` (`yarn_pnp_unsupported`), while standalone `vex` still attests a hosted lock's `checksum:` pin. +## Hosted pin shape and registry credentials + +The hosted pin is what yarn itself writes for a root `resolutions` entry: + +- `package.json` gains one descriptor-specific selector per range the lock + entry carries, routed to the hosted tarball: + `"resolutions": {"left-pad@npm:^1.3.0": "https://patch.socket.dev/…/left-pad-1.3.0.tgz"}`; +- `yarn.lock` re-keys only that entry, `"left-pad@https://…/left-pad-1.3.0.tgz":`, + with the same URL as its `resolution:` and the patched `10c0` checksum. Its + `version:`, `dependencies:` and every dependent's descriptors stay + byte-identical, and the entry moves to where yarn sorts it. + +Why this shape (#404): + +- An `npm:` locator — including the `npm:::__archiveUrl=` pin releases + up to 5.0 wrote — is fetched with yarn's npm fetcher, which attaches the + configured registry auth (`npmAuthToken`, `YARN_NPM_AUTH_TOKEN`, + `npmScopes..npmAuthToken`) to every scoped package's request, and to + every request under `npmAlwaysAuth: true`, whatever host the URL names. The + tarball fetcher sends none. +- A tarball locator under the untouched `npm:` key is rejected by yarn's + hardened mode (`YN0078: Invalid resolution`), which yarn turns on by itself + for CI runs on public pull requests and `enableHardenedMode: true` turns on + anywhere. The `resolutions` pin passes it, so hosted mode assumes every + berry project may run hardened. + +Measured on yarn 4.12.0 (fresh checkout, cold cache, `YARN_NPM_AUTH_TOKEN` + +`npmAlwaysAuth`, hardened mode): `yarn install --immutable --check-cache` +passes for direct and transitive packages, leaves the lock untouched, and the +patch host receives no `Authorization` header; another locked version of the +same package keeps its registry entry. The real-yarn capstone +(`e2e_redirect_yarn_berry_build`) runs that fresh install in hardened mode with +a registry token configured and asserts the patch host received none. + +Only yarn berry projects are touched this way: `package.json` is read beside a +berry `yarn.lock` only, and yarn classic, npm, pnpm, bun and vlt pins are +unchanged. `rollback` / `remove` rebuild the original lock key from the +selectors and drop them (an emptied `resolutions` table is removed); a lock +pinned by an older release (`::__archiveUrl=`) is still recognized by `vex`, +rollback and the mode takeovers, and the next hosted `scan` re-pins it. + +Refusals (nothing written, the warning names the cause): + +- `redirect_yarn_berry_resolutions_conflict` — `package.json` already has a + user-authored `resolutions` entry for the package (bare, ranged or nested); + hosted mode never overwrites it. +- `redirect_yarn_berry_manifest_missing` — no root `package.json` object. +- `redirect_yarn_berry_shared_descriptor` — the package is also locked through + a non-npm entry (yarn's builtin `patch:` compatibility entries for + `resolve`, `typescript`, `fsevents`), which wraps the same descriptor a pin + would move. +- `redirect_yarn_berry_artifact_url_unsupported` — an artifact URL yarn could + not fetch as a tarball (not `http(s)`, not ending in `.tgz`/`.tar.gz`, or + carrying a query or fragment). + ## Test matrix The `yarn-berry-e2e` job in `.github/workflows/ci.yml` runs