Skip to content

fix: support scope-specific registry auth tokens#12392

Merged
zkochan merged 4 commits into
mainfrom
feat/12390
Jun 14, 2026
Merged

fix: support scope-specific registry auth tokens#12392
zkochan merged 4 commits into
mainfrom
feat/12390

Conversation

@zkochan

@zkochan zkochan commented Jun 13, 2026

Copy link
Copy Markdown
Member

Summary

  • Parse registry auth into configByUri[registryUrl][scope], using @ for registry-wide/default credentials and keeping tls beside the scope entries.
  • Resolve scoped auth headers before falling back to the registry-wide @ credentials.
  • Index scoped auth headers by package scope during lookup, so packages whose scope has no headers skip the scoped URI-prefix walk entirely.
  • Thread package names through npm metadata resolution, lockfile verification, tarball downloads, and package-specific registry-access/stage commands.
  • Preserve package-scoped publish credentials by selecting the package scope before collapsing the per-package registry config to libnpmpublish's default auth slot.
  • Port the same behavior to pacquet and send pnpr credentials as structured authHeaders[registryUrl][scope] instead of flattening scoped entries into URLs.

Tests

  • pnpm --filter @pnpm/config.reader test test/getNetworkConfigs.test.ts --runInBand --watchman=false
  • pnpm --filter @pnpm/config.reader test test/index.ts --runInBand --watchman=false -t "project \\.npmrc does not expand env variables in auth values|package manager bootstrap registries ignore project workspace registries|unscoped credentials are pinned"
  • pnpm --filter @pnpm/network.auth-header test test/getAuthHeadersFromConfig.test.ts test/getAuthHeaderByURI.ts --runInBand --watchman=false
  • pnpm --filter @pnpm/registry-access.commands test test/dist-tag.ts -t "dist-tag ls: should use package-scoped auth" --runInBand --watchman=false
  • pnpm --filter @pnpm/releasing.commands test test/stage.test.ts test/publish/publishConfigAccess.test.ts -t "stage list uses package-scoped auth|prefers package-scoped credentials" --runInBand --watchman=false
  • pnpm --filter @pnpm/types --filter @pnpm/config.reader --filter @pnpm/network.auth-header --filter @pnpm/installing.deps-installer --filter @pnpm/pnpr.client --filter @pnpm/releasing.commands run compile
  • pnpm --filter @pnpm/registry-access.commands --filter @pnpm/deps.inspection.commands --filter @pnpm/releasing.commands run compile
  • PNPM_REGISTRY_MOCK_PORT=7769 NODE_OPTIONS="$NODE_OPTIONS --experimental-vm-modules --disable-warning=ExperimentalWarning --disable-warning=DEP0169" pnpm --dir installing/deps-installer exec jest test/install/auth.ts --runInBand --watchman=false
  • pnpm --filter @pnpm/fetching.pick-fetcher test customFetch.ts --runInBand
  • pnpm --filter @pnpm/fetching.tarball-fetcher test fetch.ts --runInBand --watchman=false
  • cargo test -p pacquet-network auth
  • cargo test -p pacquet-config npmrc_auth::tests::
  • cargo test -p pacquet-pnpr-client credential --test integration
  • cargo check -p pacquet-cli
  • cargo clippy -p pacquet-network --all-targets -- -D warnings
  • cargo clippy -p pacquet-config --all-targets -- -D warnings
  • cargo clippy -p pacquet-config -p pacquet-network -p pacquet-pnpr-client -p pnpr --all-targets -- -D warnings
  • RUSTFLAGS="-D warnings" cargo dylint --all -- --all-targets --workspace
  • cargo fmt --all -- --check
  • pnpm exec cspell registry-access/commands/test/dist-tag.ts --no-progress
  • git diff --check
  • pre-push hook via git push --force-with-lease (includes pnpm bundle checks, workspace Rust clippy/docs/dylint, spellcheck, and metadata checks)

Fixes #12390


Written by an agent (Codex, GPT-5).

Summary by CodeRabbit

  • New Features
    • Package-scope registry auth is now prioritized over registry-wide auth on the same registry host, enabling different tokens/credentials per package scope.
  • Bug Fixes
    • Authorization header selection is now consistently package-aware across registry metadata, tarball downloads, and pnpr resolution/verification.
  • Breaking/Behavioral Changes
    • Registry auth config format now uses scope keys (@ for registry-wide; @<scope> for scoped) instead of the previous creds wrapper.
  • Tests
    • Updated/added coverage for scoped precedence, scope-aware header forwarding, and login --scope persistence.
  • Chores
    • Updated release metadata for patch publishing.

@coderabbitai

coderabbitai Bot commented Jun 13, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Registry authentication resolution is changed to prioritize package scope before falling back to registry URL, enabling different auth tokens for scopes sharing the same registry host. Both TypeScript and Rust stacks receive a new DEFAULT_REGISTRY_SCOPE (@) constant, a restructured credential model keyed by scope, an extended auth-header API accepting pkgName context, and updated call sites across config parsing, tarball fetching, metadata resolving, and the pnpr server protocol. The pnpm login --scope command now writes scope-specific _authToken entries and registry mappings, enabling end-to-end scope-aware authentication.

Changes

Scoped Registry Auth

Layer / File(s) Summary
Release notes and core scope contracts
.changeset/scoped-registry-auth.md, core/types/src/misc.ts, fetching/types/src/index.ts
Adds release metadata documenting scope-first registry auth resolution with fallback to registry-wide token. Introduces DEFAULT_REGISTRY_SCOPE = '@' constant, replaces RegistryConfig.creds? with [scope: \@${string}`]: Creds | undefinedindex signature, and extendsGetAuthHeaderto accept optionalGetAuthHeaderOptions { pkgName? }` parameter.
Login scoped token persistence
auth/commands/src/login.ts, auth/commands/test/login.test.ts
Updates help() text for --scope <scope> option to clarify associating login tokens with package scopes. Refactors auth.ini persistence to normalize scope early, compute scope-aware authConfigKey, write _authToken under the scoped key when scope is provided, and preserve unscoped writes when scope is omitted. Expands tests to validate scoped _authToken keys, scope→registry mappings, and path-registry handling.
TypeScript config reader scoped credential parsing
config/reader/src/getNetworkConfigs.ts, config/reader/test/getNetworkConfigs.test.ts, config/reader/test/index.ts, pnpm/src/switchCliVersion.test.ts, pnpm/src/syncEnvLockfile.test.ts
Extends tryParseCredsKey with splitScopeFromRegistry variants to extract package scopes from registry URIs (supporting :@ and path forms). Changes rawCredsMap from flat registry→RawCreds to nested registry→scope→RawCreds structure. Adds getScopedCreds helper to parse per-scope credentials and merges scoped credential objects into configByUri using Object.assign. Updates all test fixtures from creds: {...} to '@': {...} shape.
TypeScript auth-header scoped maps and lookup
network/auth-header/src/getAuthHeadersFromConfig.ts, network/auth-header/src/index.ts, network/auth-header/test/getAuthHeaderByURI.ts, network/auth-header/test/getAuthHeadersFromConfig.test.ts
Refactors getAuthHeadersFromCreds to return structured AuthHeaders with authHeaderValueByURI and scopedAuthHeaderValueByURI maps. Adds getAuthHeadersByScope converter function. Updates createGetAuthHeaderByURI to precompute per-scope lookup tables and route scoped vs. unscoped lookups based on extracted pkgName scope. Corrects port-stripping retry logic in getAuthHeaderByNerfedURI. Expands tests for package-scope precedence, pathname registry matching, and URL basic-auth override behavior.
TypeScript fetcher and resolver pkgName propagation
fetching/tarball-fetcher/src/remoteTarballFetcher.ts, fetching/tarball-fetcher/src/index.ts, fetching/tarball-fetcher/test/fetch.ts, resolving/npm-resolver/src/index.ts, resolving/npm-resolver/src/createNpmResolutionVerifier.ts, resolving/npm-resolver/test/index.ts, resolving/npm-resolver/test/createNpmResolutionVerifier.test.ts
Retypes DownloadOptions.getAuthHeaderByURI and resolver auth callbacks to GetAuthHeader signature. Explicitly declares pkg?: FetchOptions['pkg'] in DownloadOptions. Passes pkgName context through tarball downloads via opts.getAuthHeaderByURI(url, { pkgName: opts.pkg?.name }) and through full-metadata, abbreviated-metadata, attestation-timestamp, and tarball fetch chains. Adds test cases validating pkgName propagation for scoped packages.
TypeScript command flows with package-aware auth
deps/inspection/commands/src/fetchPackageInfo.ts, registry-access/commands/src/deprecation/common.ts, registry-access/commands/src/distTag.ts, registry-access/commands/src/owner.ts, registry-access/commands/src/star/common.ts, registry-access/commands/src/unpublish.ts, registry-access/commands/test/deprecate.ts, registry-access/commands/test/dist-tag.ts, registry-access/commands/test/star.ts, registry-access/commands/test/unpublish.ts, registry-access/commands/test/whoami.ts
Updates fetchPackageInfo metadata request to pass { pkgName: packageName }. Refactors getAuthHeaderForRegistry helper to accept optional packageName and forward to createGetAuthHeaderByURI. Applies package-aware auth throughout deprecate, dist-tag, owner, star, and unpublish command handlers. Updates all test fixtures to @-keyed config shape and adds new test for package-scoped dist-tag authentication.
TypeScript publish and stage scoped credential selection
releasing/commands/src/publish/publishPackedPkg.ts, releasing/commands/src/stage/context.ts, releasing/commands/test/publish/batchPublish.test.ts, releasing/commands/test/publish/publish.ts, releasing/commands/test/publish/publishConfigAccess.test.ts, releasing/commands/test/publish/recursivePublish.ts, releasing/commands/test/stage.test.ts
Updates publishPackedPkg to read tls from config?.tls and credentials from config?.[DEFAULT_REGISTRY_SCOPE] with scoped fallback logic. Enables createStageContext package-aware auth computation. Updates publish/stage tests for scoped token precedence and adds new test for dual-scope auth selection.
TypeScript pnpr authHeaders-by-scope wiring
pnpr/client/src/resolveViaPnprServer.ts, installing/deps-installer/src/install/index.ts, installing/deps-installer/test/install/auth.ts
Introduces AuthHeadersByScope type in pnpr client as nested record keyed by registry URI and scope. Updates ResolveViaPnprServerOptions.authHeaders to this structure. Converts forwarded auth headers via getAuthHeadersByScope in installViaPnprServer. Updates installer auth tests to @-keyed config shape.
Rust AuthHeaders scoped model and lookup behavior
pacquet/crates/network/src/auth.rs, pacquet/crates/network/src/lib.rs, pacquet/crates/network/src/auth/tests.rs
Extends AuthHeaders struct with scoped_by_scope and max_scoped_parts_by_scope fields for per-scope storage. Adds DEFAULT_REGISTRY_SCOPE constant and AuthHeadersByScope type. Implements from_parts (normalized construction), from_by_scope (wire conversion), and to_by_scope (serialization). Adds for_url_with_package(url, pkg_name) entry point with scope-first nerf-dart lookup and lookup_scope_by_nerf helper with max-depth bounding. Adds tests for scope precedence, path-prefix matching, round-trip serialization, and URL basic-auth override.
Rust npmrc auth nested scope storage
pacquet/crates/config/src/npmrc_auth.rs, pacquet/crates/config/src/lib.rs, pacquet/crates/config/src/npmrc_auth/tests.rs
Replaces flat creds_by_uri with nested creds_by_scope_by_uri HashMap structure. Routes all credential insertion (env and .npmrc) through creds_entry_mut helper that defaults missing scope to DEFAULT_REGISTRY_SCOPE. Rebuilds build_auth_headers to partition default-scope and scoped credentials and emit via AuthHeaders::from_parts. Updates rescope_unscoped and merge_under for nested traversal. Reworks config tests to assert tokens via per-URI-per-scope lookups, validates env-var substitution, and confirms ignored placeholders.
Rust package-aware auth at fetch call sites
pacquet/crates/registry/src/package.rs, pacquet/crates/registry/src/package_version.rs, pacquet/crates/resolving-npm-resolver/src/fetch_full_metadata.rs, pacquet/crates/resolving-npm-resolver/src/fetch_full_metadata_cached.rs, pacquet/crates/resolving-npm-resolver/src/fetch_attestation_published_at.rs, pacquet/crates/tarball/src/lib.rs, pacquet/crates/resolving-npm-resolver/src/fetch_full_metadata/tests.rs, pacquet/crates/tarball/src/tests.rs
Switches all HTTP fetch authorization lookups from auth_headers.for_url(&url) to auth_headers.for_url_with_package(&url, Some(package_name)). Provides package context for scope-aware header selection. Adds test cases validating scoped-token Authorization headers on registry metadata and tarball requests.
Rust pnpr protocol and client scope-map adoption
pnpr/crates/pnpr/src/resolver/protocol.rs, pnpr/crates/pnpr/src/resolver.rs, pacquet/crates/pnpr-client/src/lib.rs, pacquet/crates/pnpr-client/Cargo.toml, pacquet/crates/pnpr-client/tests/integration.rs, pacquet/crates/cli/src/cli_args/install.rs
Updates ResolveRequest.auth_headers from BTreeMap<String, String> to AuthHeadersByScope nested structure. Updates pnpr-client public ResolveOptions and VerifyLockfileOptions.auth_headers to AuthHeadersByScope. Switches resolver to construct request auth via AuthHeaders::from_by_scope. Adds pnpm-network dependency to pnpr-client. Adds test helper for scoped construction. Updates CLI to use auth_headers.to_by_scope().

Sequence Diagram(s)

sequenceDiagram
  participant npmrc as .npmrc / env
  participant ConfigReader as ConfigReader (TS)<br/>NpmrcAuth (Rust)
  participant AuthHeaders as AuthHeaders<br/>Auth Lookup
  participant Resolver as Package Resolver
  participant Registry as Registry HTTP

  npmrc->>ConfigReader: //npm.pkg.github.com/@orgA:_authToken=TOKEN_A<br/>//npm.pkg.github.com/@orgB:_authToken=TOKEN_B
  ConfigReader->>ConfigReader: parse scope from registry key<br/>store nested [registry][`@orgA`] = TOKEN_A<br/>store nested [registry][`@orgB`] = TOKEN_B
  ConfigReader->>AuthHeaders: build from nested creds<br/>via getAuthHeadersFromCreds
  Resolver->>AuthHeaders: lookup_for_package(`@orgA/pkg`, registry)
  AuthHeaders->>AuthHeaders: extract `@orgA` scope
  AuthHeaders->>AuthHeaders: nerf-dart match<br/>scoped_by_scope[`@orgA`]
  AuthHeaders-->>Resolver: Bearer TOKEN_A
  Resolver->>Registry: GET /@orgA%2Fpkg<br/>Authorization: Bearer TOKEN_A
  Registry-->>Resolver: 200 OK package metadata
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • pnpm/pnpm#11727: pnpm login --scope introduces writing @<scope>:registry=<registry> mappings, which the main PR's scope-aware config parsing (getNetworkConfigs, tryParseCredsKey scope splitting) consumes to route scoped credentials correctly.
  • pnpm/pnpm#12299: Both PRs modify releasing/commands/src/publish/publishPackedPkg.ts—main PR refactors for scope-keyed credentials (DEFAULT_REGISTRY_SCOPE), while retrieved PR adds OIDC/batch support—making changes adjacent in the publish auth-credential path.
  • pnpm/pnpm#11953: Both PRs touch network/auth-header and config/reader auth-credential plumbing for scope-aware default-registry vs. scoped lookup behavior across the stack.

Suggested reviewers

  • ranm8

🐇 Hop hop, a scope has its own key,
No more one token for all — now scopes run free!
@orgA gets its bearer, @orgB gets its own,
GitHub packages bloom where mixed auth was stone.
The rabbit cheers: '@' maps the way home! 🎉

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/12390

@github-actions

github-actions Bot commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Micro-Benchmark Results

Linux

group                          main                                   pr
-----                          ----                                   --
tarball/download_dependency    1.03      7.9±0.13ms   550.2 KB/sec    1.00      7.7±0.43ms   564.3 KB/sec

@codecov-commenter

codecov-commenter commented Jun 13, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.14563% with 10 lines in your changes missing coverage. Please review.
✅ Project coverage is 88.13%. Comparing base (94c13cc) to head (719439c).
⚠️ Report is 6 commits behind head on main.

Files with missing lines Patch % Lines
pacquet/crates/network/src/auth.rs 96.00% 5 Missing ⚠️
pacquet/crates/config/src/npmrc_auth.rs 94.28% 4 Missing ⚠️
pacquet/crates/tarball/src/lib.rs 50.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #12392      +/-   ##
==========================================
- Coverage   88.32%   88.13%   -0.19%     
==========================================
  Files         298      300       +2     
  Lines       39034    39714     +680     
==========================================
+ Hits        34477    35003     +526     
- Misses       4557     4711     +154     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@zkochan zkochan force-pushed the feat/12390 branch 3 times, most recently from 4d5d4a2 to dbfa165 Compare June 13, 2026 21:51
@github-actions

github-actions Bot commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Integrated-Benchmark Report (Linux)

Each scenario reports direct installs and pnpr installs. Bencher consumes pacquet@HEAD and pnpr@HEAD.

Scenario: Isolated linker: fresh restore, cold cache + cold store

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 3.787 ± 0.154 3.567 4.045 1.81 ± 0.11
pacquet@main 3.616 ± 0.111 3.497 3.869 1.73 ± 0.10
pnpr@HEAD 2.098 ± 0.100 1.982 2.288 1.00 ± 0.07
pnpr@main 2.096 ± 0.100 1.986 2.272 1.00
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 3.78666991188,
      "stddev": 0.15363599317102547,
      "median": 3.8242232127799998,
      "user": 3.4433506799999996,
      "system": 3.3088647799999995,
      "min": 3.56663452128,
      "max": 4.04545429628,
      "times": [
        4.04545429628,
        3.65704975628,
        3.9156075722800003,
        3.84104918228,
        3.80739724328,
        3.8853070682800004,
        3.59662595728,
        3.56663452128,
        3.69165792128,
        3.85991560028
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 3.61644489058,
      "stddev": 0.11142797488832354,
      "median": 3.5729575242800005,
      "user": 3.5324500800000003,
      "system": 3.2885356800000003,
      "min": 3.49698444728,
      "max": 3.86928715928,
      "times": [
        3.61767985628,
        3.54751014028,
        3.55233552828,
        3.5729168172800003,
        3.5465316392800004,
        3.49698444728,
        3.6496581592800004,
        3.86928715928,
        3.57299823128,
        3.7385469272800003
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 2.09759097258,
      "stddev": 0.10048444377201017,
      "median": 2.0869875752800002,
      "user": 2.59976698,
      "system": 2.8903264799999997,
      "min": 1.98196277328,
      "max": 2.28829510228,
      "times": [
        2.1794901442800003,
        2.01040292328,
        1.98196277328,
        1.98998790128,
        2.15824891428,
        2.09035018428,
        2.28829510228,
        2.17200742028,
        2.08362496628,
        2.02153939628
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 2.0955489773800005,
      "stddev": 0.10034489376852326,
      "median": 2.0611273592800003,
      "user": 2.6728827799999997,
      "system": 2.8982791800000003,
      "min": 1.98608657128,
      "max": 2.27229614728,
      "times": [
        2.0090541552800003,
        2.03064272128,
        2.2319143292800003,
        2.01588629328,
        2.0916119972800002,
        2.02852303528,
        2.11811472628,
        2.27229614728,
        1.98608657128,
        2.17135979728
      ]
    }
  ]
}

Scenario: Isolated linker: fresh restore, hot cache + hot store

Command Mean [ms] Min [ms] Max [ms] Relative
pacquet@HEAD 615.6 ± 11.0 596.3 626.3 1.00
pacquet@main 633.5 ± 93.2 586.7 894.8 1.03 ± 0.15
pnpr@HEAD 682.7 ± 59.3 641.8 846.4 1.11 ± 0.10
pnpr@main 645.5 ± 8.7 634.4 664.4 1.05 ± 0.02
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 0.6156014294200001,
      "stddev": 0.01099844172949568,
      "median": 0.61958417212,
      "user": 0.3549442399999999,
      "system": 1.2740240599999997,
      "min": 0.59626755762,
      "max": 0.62630504762,
      "times": [
        0.62150494462,
        0.61033497662,
        0.61872394762,
        0.59650396262,
        0.6191306656200001,
        0.62003767862,
        0.6248470996200001,
        0.59626755762,
        0.62235841362,
        0.62630504762
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 0.63351064642,
      "stddev": 0.09322916521462865,
      "median": 0.59790899962,
      "user": 0.35421623999999996,
      "system": 1.2821966599999997,
      "min": 0.58668091862,
      "max": 0.89481370762,
      "times": [
        0.59715787662,
        0.62424521062,
        0.58970090362,
        0.63483388862,
        0.61967346562,
        0.58668091862,
        0.5954369526200001,
        0.59866012262,
        0.59390341762,
        0.89481370762
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.68269245832,
      "stddev": 0.059323800548062015,
      "median": 0.66546146662,
      "user": 0.36420723999999993,
      "system": 1.3217705599999998,
      "min": 0.6417796746200001,
      "max": 0.84636624062,
      "times": [
        0.6965230036200001,
        0.67558952362,
        0.66378656762,
        0.66713636562,
        0.65846719062,
        0.6534154736200001,
        0.6417796746200001,
        0.66756739462,
        0.65629314862,
        0.84636624062
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 0.6455140236200001,
      "stddev": 0.008744751271653134,
      "median": 0.64529045362,
      "user": 0.3783311399999999,
      "system": 1.2844798599999998,
      "min": 0.63437075462,
      "max": 0.6644455286200001,
      "times": [
        0.6644455286200001,
        0.64008331362,
        0.63437075462,
        0.63890146162,
        0.6462331946200001,
        0.64827388462,
        0.63783173262,
        0.64434771262,
        0.64735860662,
        0.65329404662
      ]
    }
  ]
}

Scenario: Isolated linker: fresh install, cold cache + cold store

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 4.081 ± 0.055 3.986 4.154 1.93 ± 0.10
pacquet@main 4.112 ± 0.065 4.037 4.266 1.95 ± 0.10
pnpr@HEAD 2.110 ± 0.100 2.019 2.329 1.00
pnpr@main 2.119 ± 0.124 1.975 2.350 1.00 ± 0.08
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 4.08107633264,
      "stddev": 0.05469063275686689,
      "median": 4.09859773224,
      "user": 3.5957869799999997,
      "system": 3.2595162599999994,
      "min": 3.98553569924,
      "max": 4.15362772224,
      "times": [
        3.98553569924,
        4.12152436924,
        4.10923529024,
        4.04962596224,
        4.11256939424,
        4.15362772224,
        4.08660494424,
        4.09426962924,
        3.9948444802400003,
        4.10292583524
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 4.111612447640001,
      "stddev": 0.06517795709842235,
      "median": 4.09389866874,
      "user": 3.678760079999999,
      "system": 3.2572165600000007,
      "min": 4.0366410332400005,
      "max": 4.26606296724,
      "times": [
        4.09485676624,
        4.09244839024,
        4.05495290524,
        4.07628934524,
        4.26606296724,
        4.0366410332400005,
        4.16759926124,
        4.09294057124,
        4.10985499424,
        4.12447824224
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 2.11014812294,
      "stddev": 0.09980658773359877,
      "median": 2.06894321424,
      "user": 2.4236724799999996,
      "system": 2.83889026,
      "min": 2.01889459724,
      "max": 2.32880520024,
      "times": [
        2.11657109224,
        2.15290402324,
        2.06035510824,
        2.05385681024,
        2.0775313202400003,
        2.32880520024,
        2.04807189424,
        2.02029785524,
        2.22419332824,
        2.01889459724
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 2.1191709227400004,
      "stddev": 0.12376192482317472,
      "median": 2.0861327797399998,
      "user": 2.48426268,
      "system": 2.8284512600000005,
      "min": 1.97473581124,
      "max": 2.3500044942400002,
      "times": [
        2.09323574724,
        2.10267718624,
        2.0267392392400003,
        1.99706289024,
        2.27204601324,
        2.07902981224,
        2.3500044942400002,
        2.2276321132400003,
        1.97473581124,
        2.06854592024
      ]
    }
  ]
}

Scenario: Isolated linker: fresh install, hot cache + hot store

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 1.257 ± 0.014 1.241 1.279 1.94 ± 0.26
pacquet@main 1.297 ± 0.050 1.267 1.436 2.01 ± 0.28
pnpr@HEAD 0.649 ± 0.076 0.613 0.864 1.00 ± 0.18
pnpr@main 0.646 ± 0.086 0.604 0.890 1.00
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 1.2566899398999998,
      "stddev": 0.013981519372908134,
      "median": 1.2536957754999998,
      "user": 1.2394926599999998,
      "system": 1.6545815400000001,
      "min": 1.2412355075,
      "max": 1.2793715074999998,
      "times": [
        1.2518033935,
        1.2793715074999998,
        1.2644721805,
        1.2416216945,
        1.2420424294999999,
        1.2488381665,
        1.2555881574999999,
        1.2412355075,
        1.2731648494999999,
        1.2687615125
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 1.2970911215,
      "stddev": 0.05010995908356041,
      "median": 1.284402849,
      "user": 1.2578352599999998,
      "system": 1.6896536400000002,
      "min": 1.2665728085,
      "max": 1.4359280895,
      "times": [
        1.2808008645,
        1.3036375464999999,
        1.2901601605,
        1.2745745955,
        1.4359280895,
        1.2898074935,
        1.2715656455,
        1.2665728085,
        1.2698591775,
        1.2880048334999998
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.6489462253999999,
      "stddev": 0.07579815775181901,
      "median": 0.623438861,
      "user": 0.3097473600000001,
      "system": 1.24897004,
      "min": 0.6131982755,
      "max": 0.8637033615,
      "times": [
        0.6225641755,
        0.6131982755,
        0.6194470875,
        0.6228971045,
        0.6228892014999999,
        0.8637033615,
        0.6290733655,
        0.6239806175,
        0.6323211795,
        0.6393878855
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 0.6463695199000001,
      "stddev": 0.08589197430288771,
      "median": 0.620453649,
      "user": 0.31771856000000004,
      "system": 1.23373764,
      "min": 0.6041197835,
      "max": 0.8896080145,
      "times": [
        0.6041197835,
        0.6207896185,
        0.6205881515,
        0.6203191465,
        0.6085952845,
        0.6265719685,
        0.6197240555,
        0.6352285395,
        0.6181506365,
        0.8896080145
      ]
    }
  ]
}

Scenario: Isolated linker: fresh install, cold cache + hot store

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 2.979 ± 0.038 2.941 3.068 4.69 ± 0.12
pacquet@main 2.927 ± 0.029 2.888 2.972 4.61 ± 0.11
pnpr@HEAD 0.635 ± 0.012 0.621 0.653 1.00 ± 0.03
pnpr@main 0.635 ± 0.014 0.617 0.651 1.00
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 2.97937998802,
      "stddev": 0.03753497503455069,
      "median": 2.9799811171199995,
      "user": 1.69327972,
      "system": 2.01678536,
      "min": 2.94146395612,
      "max": 3.06792862412,
      "times": [
        2.96049124712,
        2.94146395612,
        2.9726550221199997,
        2.94327506012,
        2.9873072121199997,
        2.94553857812,
        2.9893983831199997,
        3.06792862412,
        2.98945419512,
        2.99628760212
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 2.92682137202,
      "stddev": 0.02874919690881723,
      "median": 2.9297712251199997,
      "user": 1.70567032,
      "system": 1.88861006,
      "min": 2.8877608741199996,
      "max": 2.97219604812,
      "times": [
        2.97219604812,
        2.8877608741199996,
        2.92892301812,
        2.9018596411199997,
        2.9034586551199997,
        2.9319008791199996,
        2.89907101312,
        2.96466208712,
        2.9477620721199997,
        2.93061943212
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.63539006012,
      "stddev": 0.01190385940233323,
      "median": 0.63545138062,
      "user": 0.31880981999999997,
      "system": 1.25183176,
      "min": 0.62144890612,
      "max": 0.65280939512,
      "times": [
        0.6404604181200001,
        0.62751832512,
        0.62215432512,
        0.63155269512,
        0.6467521581200001,
        0.62261195512,
        0.64924235712,
        0.63935006612,
        0.65280939512,
        0.62144890612
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 0.6352339171200001,
      "stddev": 0.014142816340391268,
      "median": 0.63491715362,
      "user": 0.31029582,
      "system": 1.24942006,
      "min": 0.61712934312,
      "max": 0.65093338712,
      "times": [
        0.6491256511200001,
        0.6504123941200001,
        0.6325996821200001,
        0.64898037212,
        0.61712934312,
        0.6372346251200001,
        0.6184412611200001,
        0.65093338712,
        0.6184264501200001,
        0.62905600512
      ]
    }
  ]
}

@github-actions

github-actions Bot commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

🐰 Bencher Report

Branchpr/12392
Testbedpacquet
Click to view all benchmark results
BenchmarkLatencyBenchmark Result
milliseconds (ms)
(Result Δ%)
Upper Boundary
milliseconds (ms)
(Limit %)
isolated-linker.fresh-install.cold-cache.cold-store📈 view plot
🚷 view threshold
4,081.08 ms
(-5.10%)Baseline: 4,300.33 ms
5,160.39 ms
(79.08%)
isolated-linker.fresh-install.cold-cache.hot-store📈 view plot
🚷 view threshold
2,979.38 ms
(+3.00%)Baseline: 2,892.52 ms
3,471.02 ms
(85.84%)
isolated-linker.fresh-install.hot-cache.hot-store📈 view plot
🚷 view threshold
1,256.69 ms
(+6.53%)Baseline: 1,179.63 ms
1,415.56 ms
(88.78%)
isolated-linker.fresh-restore.cold-cache.cold-store📈 view plot
🚷 view threshold
3,786.67 ms
(-16.58%)Baseline: 4,539.50 ms
5,447.40 ms
(69.51%)
isolated-linker.fresh-restore.hot-cache.hot-store📈 view plot
🚷 view threshold
615.60 ms
(-5.02%)Baseline: 648.15 ms
777.78 ms
(79.15%)
🐰 View full continuous benchmarking report in Bencher

@github-actions

github-actions Bot commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

🐰 Bencher Report

Branchpr/12392
Testbedpnpr

⚠️ WARNING: No Threshold found!

Without a Threshold, no Alerts will ever be generated.

Click here to create a new Threshold
For more information, see the Threshold documentation.
To only post results if a Threshold exists, set the --ci-only-thresholds flag.

Click to view all benchmark results
BenchmarkLatencymilliseconds (ms)
isolated-linker.fresh-install.cold-cache.cold-store📈 view plot
⚠️ NO THRESHOLD
2,110.15 ms
isolated-linker.fresh-install.cold-cache.hot-store📈 view plot
⚠️ NO THRESHOLD
635.39 ms
isolated-linker.fresh-install.hot-cache.hot-store📈 view plot
⚠️ NO THRESHOLD
648.95 ms
isolated-linker.fresh-restore.cold-cache.cold-store📈 view plot
⚠️ NO THRESHOLD
2,097.59 ms
isolated-linker.fresh-restore.hot-cache.hot-store📈 view plot
⚠️ NO THRESHOLD
682.69 ms
🐰 View full continuous benchmarking report in Bencher

@zkochan zkochan force-pushed the feat/12390 branch 2 times, most recently from d8e9998 to cc61e74 Compare June 13, 2026 22:47
@zkochan zkochan marked this pull request as ready for review June 13, 2026 23:46
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 13, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (6) 📘 Rule violations (0) 📎 Requirement gaps (0)

Context used

Grey Divider


Action required

1. Pnpr v1 authHeaders break 🐞 Bug ☼ Reliability
Description
The pnpr server now deserializes authHeaders as a nested {[registryUri]: {[scope]: header}} map,
so older clients sending the previous flat {[registryUri]: header} shape will fail JSON parsing
and receive a 400. This breaks installs/resolution whenever pnpr client/server versions are skewed.
Code

pnpr/crates/pnpr/src/resolver/protocol.rs[R56-61]

+    /// and fetches private content as the caller. Keyed as
+    /// `auth_headers[registry_uri][scope]`; the `@` scope stores
+    /// registry-wide auth. Distinct from the request's HTTP
+    /// `Authorization` header (pnpr identity).
#[serde(default)]
-    pub auth_headers: BTreeMap<String, String>,
+    pub auth_headers: AuthHeadersByScope,
Evidence
ResolveRequest.auth_headers is now typed as AuthHeadersByScope under camelCase
deserialization, so the server expects nested objects; the handler returns BAD_REQUEST on any JSON
shape mismatch during serde_json::from_slice.

pnpr/crates/pnpr/src/resolver/protocol.rs[37-62]
pnpr/crates/pnpr/src/resolver.rs[210-214]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The pnpr `/v1` request schema changed for `authHeaders` from a flat map to a nested `by-scope` map. Because the server directly deserializes into the new type and returns `400 Bad Request` on any parse error, older clients (or any third-party client) still sending the old shape will break.
### Issue Context
- The server is at `/v1/...` but the request schema changed without versioning or backwards-compat deserialization.
- The resolver immediately rejects bodies that fail `serde_json::from_slice`.
### Fix Focus Areas
- pnpr/crates/pnpr/src/resolver/protocol.rs[38-62]
- pnpr/crates/pnpr/src/resolver.rs[210-223]
### Suggested fix approach
1. Introduce a backward-compatible wire type for `authHeaders`, e.g. an untagged enum:
- `Flat(BTreeMap<String, String>)` (legacy)
- `ByScope(AuthHeadersByScope)` (new)
2. In deserialization (or immediately after), normalize into `AuthHeadersByScope` by mapping the flat value into `{ registryUri: { "@": header } }`.
3. Keep internal logic working on the normalized `AuthHeadersByScope`.
4. (Optional) If you want to keep strictness, consider moving the new schema to `/v2` and keep `/v1` accepting the old schema.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Scoped auth bypassed 🐞 Bug ≡ Correctness
Description
createGetAuthHeaderByURI only considers scope-specific credentials when opts.pkgName is
provided, but multiple package-specific commands still call it as getAuthHeader(registryUrl)
without passing the package name. This causes scope-specific tokens (e.g.
//npm.pkg.github.com/@org:_authToken) to be ignored and can break operations on private scoped
packages with 401/403 when no registry-wide @ credential is set.
Code

network/auth-header/src/index.ts[R41-53]

if (!uri.endsWith('/')) {
uri += '/'
}
const parsedUri = new URL(uri)
const basic = basicAuth(parsedUri)
if (basic) return basic
+  const scope = getScope(opts?.pkgName)
+  if (scope) {
+    const scopedAuth = getScopedAuthHeaderByNerfedURI(authHeaders.scopedAuthHeaderValueByURI, maxParts.maxScopedParts, uri, scope)
+    if (scopedAuth) return scopedAuth
+  }
+  return getAuthHeaderByNerfedURI(authHeaders.authHeaderValueByURI, maxParts.maxParts, uri)
+}
Evidence
The new auth-header implementation only checks scoped headers when opts.pkgName is provided;
otherwise it immediately falls back to registry-wide auth. The listed command paths all have a
concrete packageName in hand but still call getAuthHeader(registryUrl) without passing it, so
scope-specific auth entries cannot be selected on those requests.

network/auth-header/src/index.ts[41-53]
registry-access/commands/src/deprecation/common.ts[39-50]
registry-access/commands/src/distTag.ts[276-283]
registry-access/commands/src/owner.ts[157-187]
registry-access/commands/src/unpublish.ts[100-114]
registry-access/commands/src/star/common.ts[132-138]
deps/inspection/commands/src/fetchPackageInfo.ts[41-73]
releasing/commands/src/stage/context.ts[22-32]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`createGetAuthHeaderByURI()` now prefers scope-specific auth only when the caller supplies `opts.pkgName` (so it can derive `@scope`). Several package-specific commands still compute `authHeaderValue` by calling `getAuthHeader(registryUrl)` without the package name, so scoped tokens are never selected on those paths.
### Issue Context
This PR changes auth config shape to `configByUri[registryUrl][scope]` and updates the auth-header lookup to resolve scoped credentials before falling back to registry-wide `@` credentials.
### Fix
Update package-specific callsites to pass the known package name:
- Change `getAuthHeader(registryUrl)` to `getAuthHeader(registryUrl, { pkgName: packageName })` (or equivalent variable) wherever the operation targets a specific package.
- Where helpers exist (e.g. `getAuthHeaderForRegistry(configByUri, registryUrl)`), extend them to accept `pkgName?: string` and thread it through.
- For stage context, `createStageContext(opts, packageName?)` should call `getAuthHeaderByUri(registry, { pkgName: packageName })` when `packageName` is provided.
### Fix Focus Areas
- registry-access/commands/src/deprecation/common.ts[39-50]
- registry-access/commands/src/distTag.ts[276-283]
- registry-access/commands/src/owner.ts[157-187]
- registry-access/commands/src/unpublish.ts[100-114]
- registry-access/commands/src/star/common.ts[132-138]
- deps/inspection/commands/src/fetchPackageInfo.ts[41-73]
- releasing/commands/src/stage/context.ts[22-32]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

3. Nondeterministic scoped auth override 🐞 Bug ☼ Reliability ⭐ New
Description
In pacquet_network::AuthHeaders::from_map(), two different scoped-auth key syntaxes (":@scope" and
"/@scope") are collapsed into the same (registry_uri, scope) bucket, but because the input is a
HashMap, whichever token wins is nondeterministic. This can cause flaky or surprising authentication
when both syntaxes exist for the same registry+scope during migration.
Code

pacquet/crates/network/src/auth.rs[R98-109]

+    pub fn from_map(headers: HashMap<String, String>) -> Self {
+        let mut by_uri = HashMap::new();
+        let mut scoped_by_uri: HashMap<String, HashMap<String, String>> = HashMap::new();
+        for (uri, value) in headers {
+            let uri = normalize_auth_key(uri);
+            if let Some((registry_uri, scope)) = split_scoped_auth_key(&uri) {
+                scoped_by_uri.entry(registry_uri).or_default().insert(scope, value);
+            } else {
+                by_uri.insert(uri, value);
+            }
+        }
+        Self::from_parts(by_uri, scoped_by_uri)
Evidence
from_map() iterates a HashMap and uses split_scoped_auth_key() (which supports both ":@" and "/@"
scoped syntaxes) to derive a shared (registry_uri, scope) key; it then inserts into
scoped_by_uri[registry_uri][scope] without deterministic collision handling, so duplicate syntaxes
for the same scope overwrite in an order that depends on HashMap iteration.

pacquet/crates/network/src/auth.rs[94-110]
pacquet/crates/network/src/auth.rs[288-305]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`AuthHeaders::from_map` collapses multiple scoped-auth key syntaxes into the same `(registry_uri, scope)` slot. Because it iterates a `HashMap`, collision resolution depends on iteration order, which is nondeterministic, so the chosen token/header can vary between runs.

### Issue Context
Both `//host/:@scope` (colon syntax) and `//host/@scope` (path syntax) are accepted by `split_scoped_auth_key`. If both appear (common during transition when new tooling writes one form and existing config has the other), `insert(scope, value)` overwrites based on iteration order.

### Fix Focus Areas
- pacquet/crates/network/src/auth.rs[98-110]
- pacquet/crates/network/src/auth.rs[288-305]

### Implementation guidance
- Ensure deterministic precedence when two inputs map to the same `(registry_uri, scope)`:
 - Option A: Change `from_map` to accept/convert to an ordered map (e.g., iterate keys in sorted order) before inserting into `scoped_by_uri`.
 - Option B: Track if `(registry_uri, scope)` already exists and apply a documented precedence rule (e.g., prefer `:@` form, or prefer the lexicographically-longer/more-specific raw key), or return an explicit error/warning on duplicates.
 - Option C: If preserving caller precedence is desired, take an ordered sequence (Vec/BTreeMap) rather than a HashMap so “last wins” is stable.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. Scoped TLS keys ignored 🐞 Bug ☼ Reliability
Description
getNetworkConfigs() strips a trailing package scope segment from auth keys (e.g.
//host/@scope:_authToken) but does not apply the same scope split/normalization to TLS keys
(//host/@scope:ca|cert|key). This can store TLS material under configByUri["//host/@scope"] which
dispatcher TLS matching never selects for real requests to https://host/…, causing mTLS/custom-CA
config to be skipped.
Code

config/reader/src/getNetworkConfigs.ts[R90-121]

if (!credsField) {
 throw new Error(`Unexpected key: ${match.groups.key}`)
}
-  return { registry, credsField }
+  return { ...splitScopeFromRegistry(registry), credsField }
+}
+
+function getScopedCreds (rawCredsByScope: Record<string, RawCreds> = {}): Record<string, Creds> {
+  const scopedCreds: Record<string, Creds> = {}
+  for (const [scope, rawCreds] of Object.entries(rawCredsByScope)) {
+    const creds = parseCreds(rawCreds)
+    if (creds) {
+      scopedCreds[scope] = creds
+    }
+  }
+  return scopedCreds
+}
+
+function splitScopeFromRegistry (registry: string): { registry: string, scope?: string } {
+  if (!registry.startsWith('//')) return { registry }
+  const trimmed = registry.endsWith('/') ? registry.slice(0, -1) : registry
+  const lastSlashIndex = trimmed.lastIndexOf('/')
+  if (lastSlashIndex === -1) return { registry }
+  const scope = trimmed.slice(lastSlashIndex + 1)
+  if (!isPackageScope(scope)) return { registry }
+  return {
+    registry: trimmed.slice(0, lastSlashIndex + 1),
+    scope,
+  }
+}
+
+function isPackageScope (scope: string): boolean {
+  return scope.startsWith('@') && scope.length > 1
Evidence
TLS parsing stores entries under the raw registry prefix from the .npmrc key, while auth parsing
was changed to split a trailing @scope segment off the registry key. Dispatcher TLS selection
matches by the nerf-darted request URL and shorter prefixes, so a TLS key stored under
//host/@scope/ cannot be selected for requests to https://host/... (nerf-dart //host/).

config/reader/src/getNetworkConfigs.ts[23-41]
config/reader/src/getNetworkConfigs.ts[83-118]
network/fetch/src/dispatcher.ts[393-433]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`getNetworkConfigs()` now supports scope-suffixed auth keys by splitting `//host/@scope` into `{ registry: "//host/", scope: "@scope" }`, but TLS parsing (`:ca`, `:cert`, `:key`) still uses the raw prefix as the configByUri key. If a user writes TLS keys with a scope suffix (mirroring the new auth syntax), the TLS ends up stored under `configByUri['//host/@scope']` and is never matched for outgoing requests to `https://host/...`.
### Issue Context
`network/fetch` selects TLS settings by matching the *request URL* via nerf-dart prefix walk; it cannot match a longer key like `//host/@scope/` when the request nerf-dart is `//host/`.
### Fix Focus Areas
- config/reader/src/getNetworkConfigs.ts[32-42]
- config/reader/src/getNetworkConfigs.ts[83-143]
- network/fetch/src/dispatcher.ts[393-433]
### What to change
1. In `tryParseSslKey`, normalize/scope-split the `registry` prefix the same way as auth keys:
- If the registry key starts with `//` and ends with a package scope segment, strip that scope and store TLS under the base registry URI.
- Ensure a consistent trailing `/` for TLS keys as well (so downstream matching is stable).
2. Add/extend a unit test in `config/reader/test/getNetworkConfigs.test.ts` that includes a scoped TLS key (e.g. `//npm.pkg.github.com/@orgA:ca=...`) and asserts TLS lands under `//npm.pkg.github.com/` (not under the scoped key).
### Acceptance criteria
- TLS settings specified with a scope suffix are applied to requests to the base registry host/path.
- Existing non-scoped TLS behavior remains unchanged.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. @path treated as scope 🐞 Bug ≡ Correctness
Description
splitScopeFromRegistry (and the mirrored Rust logic) treats any registry key whose last path
segment starts with @ as a package scope and strips it from the registry URI. This breaks
registries whose actual URL prefix legitimately ends with an @… path segment, causing credentials
to be looked up under the wrong registry prefix (missing auth) or applied more broadly than
intended.
Code

config/reader/src/getNetworkConfigs.ts[R107-121]

+function splitScopeFromRegistry (registry: string): { registry: string, scope?: string } {
+  if (!registry.startsWith('//')) return { registry }
+  const trimmed = registry.endsWith('/') ? registry.slice(0, -1) : registry
+  const lastSlashIndex = trimmed.lastIndexOf('/')
+  if (lastSlashIndex === -1) return { registry }
+  const scope = trimmed.slice(lastSlashIndex + 1)
+  if (!isPackageScope(scope)) return { registry }
+  return {
+    registry: trimmed.slice(0, lastSlashIndex + 1),
+    scope,
+  }
+}
+
+function isPackageScope (scope: string): boolean {
+  return scope.startsWith('@') && scope.length > 1
Evidence
The new parsing logic strips the final @… segment whenever it matches `startsWith('@') && length >
1`, which changes the registry key used to store creds; the Rust implementation applies the same
heuristic when interpreting auth keys. The codebase defines registry config keys as including
arbitrary “rest of the URI”, so @ is a plausible (and valid) path-segment character, making this
inference ambiguous for some real registry prefixes.

config/reader/src/getNetworkConfigs.ts[83-122]
pacquet/crates/network/src/auth.rs[291-315]
releasing/commands/src/publish/registryConfigKeys.ts[36-41]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`splitScopeFromRegistry()` infers “scope-specific auth” solely from the last path segment starting with `@`. This makes `.npmrc` URL prefixes that *legitimately* end with an `@…` segment ambiguous and can reroute credentials to a different registry key.
Concrete example that will be misparsed:
- User intends a literal prefix token:
- `//reg.example.com/repos/@npm/:_authToken=TOKEN`
- Current behavior:
- parsed as `registry=//reg.example.com/repos/` and `scope=@npm`
- requests to `https://reg.example.com/repos/@npm/...` for **unscoped** packages won’t see the token (no `pkgName` scope match), leading to 401/403.
### Issue Context
This ambiguity exists in both the TS config parsing and the Rust auth lookup, so the behavior is systemic.
### Fix Focus Areas
- config/reader/src/getNetworkConfigs.ts[107-121]
- pacquet/crates/network/src/auth.rs[291-303]
### Suggested fix direction
Implement an ambiguity-safe strategy, e.g.:
1. **Preserve literal-path semantics in addition to scoped semantics** for keys whose last segment looks like a scope:
- When detecting a scope-like last segment, store creds in both:
- `configByUri[baseRegistry][scope]` (new behavior), and
- `configByUri[fullRegistryPrefix][DEFAULT_REGISTRY_SCOPE]` (literal prefix fallback)
- Optionally emit a warning when both interpretations are possible.
OR
2. Add a stricter disambiguation rule (and document it) so only truly intended scope keys are split, while leaving literal path prefixes untouched.
Choose a strategy that does not regress the new feature tests while avoiding silent auth breakage for path prefixes that end in `@…`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (1)
6. Legacy creds ignored 🐞 Bug ≡ Correctness
Description
Auth header construction now only reads registryConfig['@'] (DEFAULT_REGISTRY_SCOPE) and never
consults the former registryConfig.creds field, so JS integrations still supplying `{ creds: ...
} will silently drop auth headers. This can surface as unexpected 401/403 when configByUri` is
constructed programmatically outside the updated type system.
Code

network/auth-header/src/getAuthHeadersFromConfig.ts[R21-33]

+    const normalizedUri = normalizeAuthKey(uri)
+    const header = credsToHeader(registryConfig[DEFAULT_REGISTRY_SCOPE])
if (header) {
-      authHeaderValueByURI[uri] = header
+      authHeaders.authHeaderValueByURI[normalizedUri] = header
+    }
+    for (const scope of getRegistryScopes(registryConfig)) {
+      if (scope === DEFAULT_REGISTRY_SCOPE) continue
+      const scopedCreds = registryConfig[scope]
+      const scopedHeader = credsToHeader(scopedCreds)
+      if (scopedHeader) {
+        authHeaders.scopedAuthHeaderValueByURI[normalizedUri] ??= {}
+        authHeaders.scopedAuthHeaderValueByURI[normalizedUri][scope] = scopedHeader
+      }
Evidence
The default credential slot used to be a dedicated creds property, but the new code only reads the
@-scoped entry and the type no longer defines creds, so legacy objects won’t contribute any
headers.

network/auth-header/src/getAuthHeadersFromConfig.ts[13-33]
core/types/src/misc.ts[31-57]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`getAuthHeadersFromCreds()` (and thus `createGetAuthHeaderByURI`) only reads `registryConfig[DEFAULT_REGISTRY_SCOPE]` and ignores any legacy `registryConfig.creds`. At runtime, older compiled JS or external callers may still pass the old shape, causing authentication headers to be missing without an explicit error.
### Issue Context
The PR also changed the `RegistryConfig` type to remove `creds?: Creds` and replace it with scope-indexed creds; internal call sites were updated, but runtime compatibility for legacy callers is not preserved.
### Fix Focus Areas
- network/auth-header/src/getAuthHeadersFromConfig.ts[13-33]
- core/types/src/misc.ts[31-57]
### Suggested fix approach
- Add a runtime fallback when selecting creds:
- Prefer `registryConfig[DEFAULT_REGISTRY_SCOPE]`.
- If absent, read `(registryConfig as any).creds` and treat it as the default `@` scope.
- Consider reintroducing `creds?: Creds` into `RegistryConfig` as a **deprecated alias** for `['@']` to keep TS consumers compiling while migrating.
- Add a small regression test that passes `{ creds: { authToken: '...' } }` and asserts a header is produced.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Previous review results

Review updated until commit 719439c

Results up to commit a8f7949


🐞 Bugs (5) 📘 Rule violations (0) 📎 Requirement gaps (0)

Context used

Action required
1. Pnpr v1 authHeaders break 🐞 Bug ☼ Reliability
Description
The pnpr server now deserializes authHeaders as a nested {[registryUri]: {[scope]: header}} map,
so older clients sending the previous flat {[registryUri]: header} shape will fail JSON parsing
and receive a 400. This breaks installs/resolution whenever pnpr client/server versions are skewed.
Code

pnpr/crates/pnpr/src/resolver/protocol.rs[R56-61]

+    /// and fetches private content as the caller. Keyed as
+    /// `auth_headers[registry_uri][scope]`; the `@` scope stores
+    /// registry-wide auth. Distinct from the request's HTTP
+    /// `Authorization` header (pnpr identity).
#[serde(default)]
-    pub auth_headers: BTreeMap<String, String>,
+    pub auth_headers: AuthHeadersByScope,
Evidence
ResolveRequest.auth_headers is now typed as AuthHeadersByScope under camelCase
deserialization, so the server expects nested objects; the handler returns BAD_REQUEST on any JSON
shape mismatch during serde_json::from_slice.

pnpr/crates/pnpr/src/resolver/protocol.rs[37-62]
pnpr/crates/pnpr/src/resolver.rs[210-214]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The pnpr `/v1` request schema changed for `authHeaders` from a flat map to a nested `by-scope` map. Because the server directly deserializes into the new type and returns `400 Bad Request` on any parse error, older clients (or any third-party client) still sending the old shape will break.
### Issue Context
- The server is at `/v1/...` but the request schema changed without versioning or backwards-compat deserialization.
- The resolver immediately rejects bodies that fail `serde_json::from_slice`.
### Fix Focus Areas
- pnpr/crates/pnpr/src/resolver/protocol.rs[38-62]
- pnpr/crates/pnpr/src/resolver.rs[210-223]
### Suggested fix approach
1. Introduce a backward-compatible wire type for `authHeaders`, e.g. an untagged enum:
- `Flat(BTreeMap<String, String>)` (legacy)
- `ByScope(AuthHeadersByScope)` (new)
2. In deserialization (or immediately after), normalize into `AuthHeadersByScope` by mapping the flat value into `{ registryUri: { "@": header } }`.
3. Keep internal logic working on the normalized `AuthHeadersByScope`.
4. (Optional) If you want to keep strictness, consider moving the new schema to `/v2` and keep `/v1` accepting the old schema.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Scoped auth bypassed 🐞 Bug ≡ Correctness
Description
createGetAuthHeaderByURI only considers scope-specific credentials when opts.pkgName is
provided, but multiple package-specific commands still call it as getAuthHeader(registryUrl)
without passing the package name. This causes scope-specific tokens (e.g.
//npm.pkg.github.com/@org:_authToken) to be ignored and can break operations on private scoped
packages with 401/403 when no registry-wide @ credential is set.
Code

network/auth-header/src/index.ts[R41-53]

if (!uri.endsWith('/')) {
uri += '/'
}
const parsedUri = new URL(uri)
const basic = basicAuth(parsedUri)
if (basic) return basic
+  const scope = getScope(opts?.pkgName)
+  if (scope) {
+    const scopedAuth = getScopedAuthHeaderByNerfedURI(authHeaders.scopedAuthHeaderValueByURI, maxParts.maxScopedParts, uri, scope)
+    if (scopedAuth) return scopedAuth
+  }
+  return getAuthHeaderByNerfedURI(authHeaders.authHeaderValueByURI, maxParts.maxParts, uri)
+}
Evidence
The new auth-header implementation only checks scoped headers when opts.pkgName is provided;
otherwise it immediately falls back to registry-wide auth. The listed command paths all have a
concrete packageName in hand but still call getAuthHeader(registryUrl) without passing it, so
scope-specific auth entries cannot be selected on those requests.

network/auth-header/src/index.ts[41-53]
registry-access/commands/src/deprecation/common.ts[39-50]
registry-access/commands/src/distTag.ts[276-283]
registry-access/commands/src/owner.ts[157-187]
registry-access/commands/src/unpublish.ts[100-114]
registry-access/commands/src/star/common.ts[132-138]
deps/inspection/commands/src/fetchPackageInfo.ts[41-73]
releasing/commands/src/stage/context.ts[22-32]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`createGetAuthHeaderByURI()` now prefers scope-specific auth only when the caller supplies `opts.pkgName` (so it can derive `@scope`). Several package-specific commands still compute `authHeaderValue` by calling `getAuthHeader(registryUrl)` without the package name, so scoped tokens are never selected on those paths.
### Issue Context
This PR changes auth config shape to `configByUri[registryUrl][scope]` and updates the auth-header lookup to resolve scoped credentials before falling back to registry-wide `@` credentials.
### Fix
Update package-specific callsites to pass the known package name:
- Change `getAuthHeader(registryUrl)` to `getAuthHeader(registryUrl, { pkgName: packageName })` (or equivalent variable) wherever the operation targets a specific package.
- Where helpers exist (e.g. `getAuthHeaderForRegistry(configByUri, registryUrl)`), extend them to accept `pkgName?: string` and thread it through.
- For stage context, `createStageContext(opts, packageName?)` should call `getAuthHeaderByUri(registry, { pkgName: packageName })` when `packageName` is provided.
### Fix Focus Areas
- registry-access/commands/src/deprecation/common.ts[39-50]
- registry-access/commands/src/distTag.ts[276-283]
- registry-access/commands/src/owner.ts[157-187]
- registry-access/commands/src/unpublish.ts[100-114]
- registry-access/commands/src/star/common.ts[132-138]
- deps/inspection/commands/src/fetchPackageInfo.ts[41-73]
- releasing/commands/src/stage/context.ts[22-32]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended
3. Scoped TLS keys ignored 🐞 Bug ☼ Reliability
Description
getNetworkConfigs() strips a trailing package scope segment from auth keys (e.g.
//host/@scope:_authToken) but does not apply the same scope split/normalization to TLS keys
(//host/@scope:ca|cert|key). This can store TLS material under configByUri["//host/@scope"] which
dispatcher TLS matching never selects for real requests to https://host/…, causing mTLS/custom-CA
config to be skipped.
Code

config/reader/src/getNetworkConfigs.ts[R90-121]

if (!credsField) {
  throw new Error(`Unexpected key: ${match.groups.key}`)
}
-  return { registry, credsField }
+  return { ...splitScopeFromRegistry(registry), credsField }
+}
+
+function getScopedCreds (rawCredsByScope: Record<string, RawCreds> = {}): Record<string, Creds> {
+  const scopedCreds: Record<string, Creds> = {}
+  for (const [scope, rawCreds] of Object.entries(rawCredsByScope)) {
+    const creds = parseCreds(rawCreds)
+    if (creds) {
+      scopedCreds[scope] = creds
+    }
+  }
+  return scopedCreds
+}
+
+function splitScopeFromRegistry (registry: string): { registry: string, scope?: string } {
+  if (!registry.startsWith('//')) return { registry }
+  const trimmed = registry.endsWith('/') ? registry.slice(0, -1) : registry
+  const lastSlashIndex = trimmed.lastIndexOf('/')
+  if (lastSlashIndex === -1) return { registry }
+  const scope = trimmed.slice(lastSlashIndex + 1)
+  if (!isPackageScope(scope)) return { registry }
+  return {
+    registry: trimmed.slice(0, lastSlashIndex + 1),
+    scope,
+  }
+}
+
+function isPackageScope (scope: string): boolean {
+  return scope.startsWith('@') && scope.length > 1
Evidence
TLS parsing stores entries under the raw registry prefix from the .npmrc key, while auth parsing
was changed to split a trailing @scope segment off the registry key. Dispatcher TLS selection
matches by the nerf-darted request URL and shorter prefixes, so a TLS key stored under
//host/@scope/ cannot be selected for requests to https://host/... (nerf-dart //host/).

config/reader/src/getNetworkConfigs.ts[23-41]
config/reader/src/getNetworkConfigs.ts[83-118]
network/fetch/src/dispatcher.ts[393-433]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`getNetworkConfigs()` now supports scope-suffixed auth keys by splitting `//host/@scope` into `{ registry: "//host/", scope: "@scope" }`, but TLS parsing (`:ca`, `:cert`, `:key`) still uses the raw prefix as the configByUri key. If a user writes TLS keys with a scope suffix (mirroring the new auth syntax), the TLS ends up stored under `configByUri['//host/@scope']` and is never matched for outgoing requests to `https://host/...`.
### Issue Context
`network/fetch` selects TLS settings by matching the *request URL* via nerf-dart prefix walk; it cannot match a longer key like `//host/@scope/` when the request nerf-dart is `//host/`.
### Fix Focus Areas
- config/reader/src/getNetworkConfigs.ts[32-42]
- config/reader/src/getNetworkConfigs.ts[83-143]
- network/fetch/src/dispatcher.ts[393-433]
### What to change
1. In `tryParseSslKey`, normalize/scope-split the `registry` prefix the same way as auth keys:
- If the registry key starts with `//` and ends with a package scope segment, strip that scope and store TLS under the base registry URI.
- Ensure a consistent trailing `/` for TLS keys as well (so downstream matching is stable).
2. Add/extend a unit test in `config/reader/test/getNetworkConfigs.test.ts` that includes a scoped TLS key (e.g. `//npm.pkg.github.com/@orgA:ca=...`) and asserts TLS lands under `//npm.pkg.github.com/` (not under the scoped key).
### Acceptance criteria
- TLS settings specified with a scope suffix are applied to requests to the base registry host/path.
- Existing non-scoped TLS behavior remains unchanged.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. @path treated as scope 🐞 Bug ≡ Correctness
Description
splitScopeFromRegistry (and the mirrored Rust logic) treats any registry key whose last path
segment starts with @ as a package scope and strips it from the registry URI. This breaks
registries whose actual URL prefix legitimately ends with an @… path segment, causing credentials
to be looked up under the wrong registry prefix (missing auth) or applied more broadly than
intended.
Code

config/reader/src/getNetworkConfigs.ts[R107-121]

+function splitScopeFromRegistry (registry: string): { registry: string, scope?: string } {
+  if (!registry.startsWith('//')) return { registry }
+  const trimmed = registry.endsWith('/') ? registry.slice(0, -1) : registry
+  const lastSlashIndex = trimmed.lastIndexOf('/')
+  if (lastSlashIndex === -1) return { registry }
+  const scope = trimmed.slice(lastSlashIndex + 1)
+  if (!isPackageScope(scope)) return { registry }
+  return {
+    registry: trimmed.slice(0, lastSlashIndex + 1),
+    scope,
+  }
+}
+
+function isPackageScope (scope: string): boolean {
+  return scope.startsWith('@') && scope.length > 1
Evidence
The new parsing logic strips the final @… segment whenever it matches `startsWith('@') && length >
1`, which changes the registry key used to store creds; the Rust implementation applies the same
heuristic when interpreting auth keys. The codebase defines registry config keys as including
arbitrary “rest of the URI”, so @ is a plausible (and valid) path-segment character, making this
inference ambiguous for some real registry prefixes.

config/reader/src/getNetworkConfigs.ts[83-122]
pacquet/crates/network/src/auth.rs[291-315]
releasing/commands/src/publish/registryConfigKeys.ts[36-41]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`splitScopeFromRegistry()` infers “scope-specific auth” solely from the last path segment starting with `@`. This makes `.npmrc` URL prefixes that *legitimately* end with an `@…` segment ambiguous and can reroute credentials to a different registry key.
Concrete example that will be misparsed:
- User intends a literal prefix token:
- `//reg.example.com/repos/@npm/:_authToken=TOKEN`
- Current behavior:
- parsed as `registry=//reg.example.com/repos/` and `scope=@npm`
- requests to `https://reg.example.com/repos/@npm/...` for **unscoped** packages won’t see the token (no `pkgName` scope match), leading to 401/403.
### Issue Context
This ambiguity exists in both the TS config parsing and the Rust auth lookup, so the behavior is systemic.
### Fix Focus Areas
- config/reader/src/getNetworkConfigs.ts[107-121]
- pacquet/crates/network/src/auth.rs[291-303]
### Suggested fix direction
Implement an ambiguity-safe strategy, e.g.:
1. **Preserve literal-path semantics in addition to scoped semantics** for keys whose last segment looks like a scope:
- When detecting a scope-like last segment, store creds in both:
 - `configByUri[baseRegistry][scope]` (new behavior), and
 - `configByUri[fullRegistryPrefix][DEFAULT_REGISTRY_SCOPE]` (literal prefix fallback)
- Optionally emit a warning when both interpretations are possible.
OR
2. Add a stricter disambiguation rule (and document it) so only truly intended scope keys are split, while leaving literal path prefixes untouched.
Choose a strategy that does not regress the new feature tests while avoiding silent auth breakage for path prefixes that end in `@…`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Legacy creds ignored 🐞 Bug ≡ Correctness
Description
Auth header construction now only reads registryConfig['@'] (DEFAULT_REGISTRY_SCOPE) and never
consults the former registryConfig.creds field, so JS integrations still supplying `{ creds: ...
} will silently drop auth headers. This can surface as unexpected 401/403 when configByUri` is
constructed programmatically outside the updated type system.
Code

network/auth-header/src/getAuthHeadersFromConfig.ts[R21-33]

+    const normalizedUri = normalizeAuthKey(uri)
+    const header = credsToHeader(registryConfig[DEFAULT_REGISTRY_SCOPE])
if (header) {
-      authHeaderValueByURI[uri] = header
+      authHeaders.authHeaderValueByURI[normalizedUri] = header
+    }
+    for (const scope of getRegistryScopes(registryConfig)) {
+      if (scope === DEFAULT_REGISTRY_SCOPE) continue
+      const scopedCreds = registryConfig[scope]
+      const scopedHeader = credsToHeader(scopedCreds)
+      if (scopedHeader) {
+        authHeaders.scopedAuthHeaderValueByURI[normalizedUri] ??= {}
+        authHeaders.scopedAuthHeaderValueByURI[normalizedUri][scope] = scopedHeader
+      }
Evidence
The default credential slot used to be a dedicated creds property, but the new code only reads the
@-scoped entry and the type no longer defines creds, so legacy objects won’t contribute any
headers.

network/auth-header/src/getAuthHeadersFromConfig.ts[13-33]
core/types/src/misc.ts[31-57]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`getAuthHeadersFromCreds()` (and thus `createGetAuthHeaderByURI`) only reads `registryConfig[DEFAULT_REGISTRY_SCOPE]` and ignores any legacy `registryConfig.creds`. At runtime, older compiled JS or external callers may still pass the old shape, causing authentication headers to be missing without an explicit error.
### Issue Context
The PR also changed the `RegistryConfig` type to remove `creds?: Creds` and replace it with scope-indexed creds; internal call sites were updated, but runtime compatibility for legacy callers is not preserved.
### Fix Focus Areas
- network/auth-header/src/getAuthHeadersFromConfig.ts[13-33]
- core/types/src/misc.ts[31-57]
### Suggested fix approach
- Add a runtime fallback when selecting creds:
- Prefer `registryConfig[DEFAULT_REGISTRY_SCOPE]`.
- If absent, read `(registryConfig as any).creds` and treat it as the default `@` scope.
- Consider reintroducing `creds?: Creds` into `RegistryConfig` as a **deprecated alias** for `['@']` to keep TS consumers compiling while migrating.
- Add a small regression test that passes `{ creds: { authToken: '...' } }` and asserts a header is produced.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Results up to commit 6fb5b19


🐞 Bugs (5) 📘 Rule violations (0) 📎 Requirement gaps (0)

Context used

Action required
1. Pnpr v1 authHeaders break 🐞 Bug ☼ Reliability
Description
The pnpr server now deserializes authHeaders as a nested {[registryUri]: {[scope]: header}} map,
so older clients sending the previous flat {[registryUri]: header} shape will fail JSON parsing
and receive a 400. This breaks installs/resolution whenever pnpr client/server versions are skewed.
Code

pnpr/crates/pnpr/src/resolver/protocol.rs[R56-61]

+    /// and fetches private content as the caller. Keyed as
+    /// `auth_headers[registry_uri][scope]`; the `@` scope stores
+    /// registry-wide auth. Distinct from the request's HTTP
+    /// `Authorization` header (pnpr identity).
 #[serde(default)]
-    pub auth_headers: BTreeMap<String, String>,
+    pub auth_headers: AuthHeadersByScope,
Evidence
ResolveRequest.auth_headers is now typed as AuthHeadersByScope under camelCase
deserialization, so the server expects nested objects; the handler returns BAD_REQUEST on any JSON
shape mismatch during serde_json::from_slice.

pnpr/crates/pnpr/src/resolver/protocol.rs[37-62]
pnpr/crates/pnpr/src/resolver.rs[210-214]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The pnpr `/v1` request schema changed for `authHeaders` from a flat map to a nested `by-scope` map. Because the server directly deserializes into the new type and returns `400 Bad Request` on any parse error, older clients (or any third-party client) still sending the old shape will break.
### Issue Context
- The server is at `/v1/...` but the request schema changed without versioning or backwards-compat deserialization.
- The resolver immediately rejects bodies that fail `serde_json::from_slice`.
### Fix Focus Areas
- pnpr/crates/pnpr/src/resolver/protocol.rs[38-62]
- pnpr/crates/pnpr/src/resolver.rs[210-223]
### Suggested fix approach
1. Introduce a backward-compatible wire type for `authHeaders`, e.g. an untagged enum:
- `Flat(BTreeMap<String, String>)` (legacy)
- `ByScope(AuthHeadersByScope)` (new)
2. In deserialization (or immediately after), normalize into `AuthHeadersByScope` by mapping the flat value into `{ registryUri: { "@": header } }`.
3. Keep internal logic working on the normalized `AuthHeadersByScope`.
4. (Optional) If you want to keep strictness, consider moving the new schema to `/v2` and keep `/v1` accepting the old schema.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Scoped auth bypassed 🐞 Bug ≡ Correctness
Description
createGetAuthHeaderByURI only considers scope-specific credentials when opts.pkgName is
provided, but multiple package-specific commands still call it as getAuthHeader(registryUrl)
without passing the package name. This causes scope-specific tokens (e.g.
//npm.pkg.github.com/@org:_authToken) to be ignored and can break operations on private scoped
packages with 401/403 when no registry-wide @ credential is set.
Code

network/auth-header/src/index.ts[R41-53]

if (!uri.endsWith('/')) {
uri += '/'
}
const parsedUri = new URL(uri)
const basic = basicAuth(parsedUri)
if (basic) return basic
+  const scope = getScope(opts?.pkgName)
+  if (scope) {
+    const scopedAuth = getScopedAuthHeaderByNerfedURI(authHeaders.scopedAuthHeaderValueByURI, maxParts.maxScopedParts, uri, scope)
+    if (scopedAuth) return scopedAuth
+  }
+  return getAuthHeaderByNerfedURI(authHeaders.authHeaderValueByURI, maxParts.maxParts, uri)
+}
Evidence
The new auth-header implementation only checks scoped headers when opts.pkgName is provided;
otherwise it immediately falls back to registry-wide auth. The listed command paths all have a
concrete packageName in hand but still call getAuthHeader(registryUrl) without passing it, so
scope-specific auth entries cannot be selected on those requests.

network/auth-header/src/index.ts[41-53]
registry-access/commands/src/deprecation/common.ts[39-50]
registry-access/commands/src/distTag.ts[276-283]
registry-access/commands/src/owner.ts[157-187]
registry-access/commands/src/unpublish.ts[100-114]
registry-access/commands/src/star/common.ts[132-138]
deps/inspection/commands/src/fetchPackageInfo.ts[41-73]
releasing/commands/src/stage/context.ts[22-32]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`createGetAuthHeaderByURI()` now prefers scope-specific auth only when the caller supplies `opts.pkgName` (so it can derive `@scope`). Several package-specific commands still compute `authHeaderValue` by calling `getAuthHeader(registryUrl)` without the package name, so scoped tokens are never selected on those paths.
### Issue Context
This PR changes auth config shape to `configByUri[registryUrl][scope]` and updates the auth-header lookup to resolve scoped credentials before falling back to registry-wide `@` credentials.
### Fix
Update package-specific callsites to pass the known package name:
- Change `getAuthHeader(registryUrl)` to `getAuthHeader(registryUrl, { pkgName: packageName })` (or equivalent variable) wherever the operation targets a specific package.
- Where helpers exist (e.g. `getAuthHeaderForRegistry(configByUri, registryUrl)`), extend them to accept `pkgName?: string` and thread it through.
- For stage context, `createStageContext(opts, packageName?)` should call `getAuthHeaderByUri(registry, { pkgName: packageName })` when `packageName` is provided.
### Fix Focus Areas
- registry-access/commands/src/deprecation/common.ts[39-50]
- registry-access/commands/src/distTag.ts[276-283]
- registry-access/commands/src/owner.ts[157-187]
- registry-access/commands/src/unpublish.ts[100-114]
- registry-access/commands/src/star/common.ts[132-138]
- deps/inspection/commands/src/fetchPackageInfo.ts[41-73]
- releasing/commands/src/stage/context.ts[22-32]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended
3. Scoped TLS keys ignored 🐞 Bug ☼ Reliability
Description
getNetworkConfigs() strips a trailing package scope segment from auth keys (e.g.
//host/@scope:_authToken) but does not apply the same scope split/normalization to TLS keys
(//host/@scope:ca|cert|key). This can store TLS material under configByUri["//host/@scope"] which
dispatcher TLS matching never selects for real requests to https://host/…, causing mTLS/custom-CA
config to be skipped.
Code

config/reader/src/getNetworkConfigs.ts[R90-121]

 if (!credsField) {
   throw new Error(`Unexpected key: ${match.groups.key}`)
 }
-  return { registry, credsField }
+  return { ...splitScopeFromRegistry(registry), credsField }
+}
+
+function getScopedCreds (rawCredsByScope: Record<string, RawCreds> = {}): Record<string, Creds> {
+  const scopedCreds: Record<string, Creds> = {}
+  for (const [scope, rawCreds] of Object.entries(rawCredsByScope)) {
+    const creds = parseCreds(rawCreds)
+    if (creds) {
+      scopedCreds[scope] = creds
+    }
+  }
+  return scopedCreds
+}
+
+function splitScopeFromRegistry (registry: string): { registry: string, scope?: string } {
+  if (!registry.startsWith('//')) return { registry }
+  const trimmed = registry.endsWith('/') ? registry.slice(0, -1) : registry
+  const lastSlashIndex = trimmed.lastIndexOf('/')
+  if (lastSlashIndex === -1) return { registry }
+  const scope = trimmed.slice(lastSlashIndex + 1)
+  if (!isPackageScope(scope)) return { registry }
+  return {
+    registry: trimmed.slice(0, lastSlashIndex + 1),
+    scope,
+  }
+}
+
+function isPackageScope (scope: string): boolean {
+  return scope.startsWith('@') && scope.length > 1
Evidence
TLS parsing stores entries under the raw registry prefix from the .npmrc key, while auth parsing
was changed to split a trailing @scope segment off the registry key. Dispatcher TLS selection
matches by the nerf-darted request URL and shorter prefixes, so a TLS key stored under
//host/@scope/ cannot be selected for requests to https://host/... (nerf-dart //host/).

config/reader/src/getNetworkConfigs.ts[23-41]
config/reader/src/getNetworkConfigs.ts[83-118]
network/fetch/src/dispatcher.ts[393-433]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`getNetworkConfigs()` now supports scope-suffixed auth keys by splitting `//host/@scope` into `{ registry: "//host/", scope: "@scope" }`, but TLS parsing (`:ca`, `:cert`, `:key`) still uses the raw prefix as the configByUri key. If a user writes TLS keys with a scope suffix (mirroring the new auth syntax), the TLS ends up stored under `configByUri['//host/@scope']` and is never matched for outgoing requests to `https://host/...`.
### Issue Context
`network/fetch` selects TLS settings by matching the *request URL* via nerf-dart prefix walk; it cannot match a longer key like `//host/@scope/` when the request nerf-dart is `//host/`.
### Fix Focus Areas
- config/reader/src/getNetworkConfigs.ts[32-42]
- config/reader/src/getNetworkConfigs.ts[83-143]
- network/fetch/src/dispatcher.ts[393-433]
### What to change
1. In `tryParseSslKey`, normalize/scope-split the `registry` prefix the same way as auth keys:
 - If the registry key starts with `//` and ends with a package scope segment, strip that scope and store TLS under the base registry URI.
 - Ensure a consistent trailing `/` for TLS keys as well (so downstream matching is stable).
2. Add/extend a unit test in `config/reader/test/getNetworkConfigs.test.ts` that includes a scoped TLS key (e.g. `//npm.pkg.github.com/@orgA:ca=...`) and asserts TLS lands under `//npm.pkg.github.com/` (not under the scoped key).
### Acceptance criteria
- TLS settings specified with a scope suffix are applied to requests to the base registry host/path.
- Existing non-scoped TLS behavior remains unchanged.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. @path treated as scope 🐞 Bug ≡ Correctness

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 13, 2026

Copy link
Copy Markdown

PR Summary by Qodo

Support scope-specific registry auth tokens across pnpm, pnpr, and pacquet
🐞 Bug fix ✨ Enhancement 🧪 Tests 🕐 40+ Minutes

Grey Divider

Walkthroughs

Description
• Store registry auth as per-registry, per-package-scope entries ("@" is default scope).
• Prefer scope auth for scoped packages across metadata, tarballs, and registry commands.
• Forward scoped auth to pnpr/pacquet as structured authHeaders[registryUri][scope].
Diagram
graph TD
A[".npmrc / auth.ini"] --> B["getNetworkConfigs()"] --> C["configByUri[uri][scope]"] --> D["createGetAuthHeaderByURI()"] --> E["npm resolver + verifier"]
D --> F["tarball fetcher"]
D --> G["registry + publish/stage"]
C --> H["pnpr + pacquet auth forwarding"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Keep legacy `creds` slot + add `scopedCreds` map
  • ➕ Easier migration for internal callers and plugins
  • ➕ Allows incremental adoption while keeping old configByUri shape stable
  • ➖ Two competing representations increase ambiguity and maintenance burden
  • ➖ More complex precedence/merging rules (especially with path registries)
2. Encode scope only in nerf-darted URI keys (no nested scopes)
  • ➕ Minimal type changes (still a flat URI → header map)
  • ➕ Simpler to forward over the wire (string map only)
  • ➖ Harder to reason about and normalize (colon vs slash variants, trailing slashes)
  • ➖ Lookup becomes more error-prone and less explicit than authHeaders[uri][scope]
3. Resolve auth strictly via registry selection (no pkgName threading)
  • ➕ No signature changes across fetchers/resolvers
  • ➕ Smaller surface-area change
  • ➖ Does not solve same-registry/different-scope token requirements (core issue)
  • ➖ Still inconsistent across metadata vs tarball hosts and pnpr forwarding

Recommendation: The PR’s approach (explicit per-registry, per-scope credentials with '@' as the default and pkgName-aware lookup) is the most robust and debuggable way to support multiple tokens on the same registry host. The main thing to watch is compatibility: ensure any remaining internal/third-party callers are updated for the new RegistryConfig shape and the expanded GetAuthHeader(uri, {pkgName}) signature.

Grey Divider

File Changes

Enhancement (9)
getNetworkConfigs.ts Parse registry creds into per-scope entries keyed by registry URI +60/-8

Parse registry creds into per-scope entries keyed by registry URI

• Reworks credential parsing to group creds under 'configByUri[registryUri][scope]' with '@' as the default scope. Supports extracting scopes from both colon-separated ('.../:@scope') and slash-suffixed ('.../@scope') auth keys and normalizes registry keys.

config/reader/src/getNetworkConfigs.ts


misc.ts Introduce DEFAULT_REGISTRY_SCOPE and redefine RegistryConfig as scope-indexed creds +3/-1

Introduce DEFAULT_REGISTRY_SCOPE and redefine RegistryConfig as scope-indexed creds

• Adds 'DEFAULT_REGISTRY_SCOPE = '@'' and changes 'RegistryConfig' from a single 'creds' field to an index signature keyed by '@scope'. Keeps 'tls' alongside scope entries.

core/types/src/misc.ts


index.ts Expand GetAuthHeader to accept optional pkgName +5/-1

Expand GetAuthHeader to accept optional pkgName

• Introduces 'GetAuthHeaderOptions' and changes 'GetAuthHeader' to '(uri, opts?) => header' to support scope-aware lookup.

fetching/types/src/index.ts


getAuthHeadersFromConfig.ts Build auth headers for default and scoped registry credentials +50/-6

Build auth headers for default and scoped registry credentials

• Changes header construction to emit both a registry-wide header map and a scoped-by-registry map. Adds 'getAuthHeadersByScope()' to produce the pnpr wire shape 'authHeaders[uri][scope]' and normalizes URI keys with trailing slashes.

network/auth-header/src/getAuthHeadersFromConfig.ts


npmrc_auth.rs Parse and store creds as [registry_uri][scope] and build structured AuthHeaders +84/-23

Parse and store creds as [registry_uri][scope] and build structured AuthHeaders

• Replaces 'creds_by_uri' with 'creds_by_scope_by_uri' using '@' as the default scope. Adds scope splitting logic (colon and slash forms), pins unscoped creds into the default scope, and builds 'AuthHeaders' via 'from_parts' with both registry-wide and scoped headers.

pacquet/crates/config/src/npmrc_auth.rs


auth.rs Add scope-aware auth lookup and structured forwarding shape +169/-11

Add scope-aware auth lookup and structured forwarding shape

• Introduces 'DEFAULT_REGISTRY_SCOPE', stores scoped headers indexed by scope, and adds 'for_url_with_package()' to prefer scope credentials when pkg is scoped. Adds 'to_by_scope()' and 'from_by_scope()' to round-trip the pnpr wire format.

pacquet/crates/network/src/auth.rs


lib.rs Change pnpr client options to use AuthHeadersByScope +5/-4

Change pnpr client options to use AuthHeadersByScope

• Updates 'ResolveOptions' and 'VerifyLockfileOptions' to send auth as 'auth_headers[registry_uri][scope]' (with '@' as default scope).

pacquet/crates/pnpr-client/src/lib.rs


resolveViaPnprServer.ts Define pnpr client auth headers as [registry][scope] map +6/-3

Define pnpr client auth headers as [registry][scope] map

• Introduces 'AuthHeadersByScope' and updates 'ResolveViaPnprServerOptions' to forward scoped credentials with '@' as the registry-wide default scope.

pnpr/client/src/resolveViaPnprServer.ts


protocol.rs Update pnpr protocol to accept AuthHeadersByScope +6/-5

Update pnpr protocol to accept AuthHeadersByScope

• Changes the resolver request schema to accept 'auth_headers[registry_uri][scope]' and updates documentation comments accordingly.

pnpr/crates/pnpr/src/resolver/protocol.rs


Bug fix (22)
login.ts Write login tokens to scoped auth keys when --scope is used +3/-8

Write login tokens to scoped auth keys when --scope is used

• Updates 'pnpm login' to persist '_authToken' under '//registry/:@scope:_authToken' when a scope is provided, while still writing unscoped tokens to '//registry/:_authToken'. Keeps writing the '@scope:registry' mapping for future installs.

auth/commands/src/login.ts


fetchPackageInfo.ts Pass packageName into registry auth lookup +1/-1

Pass packageName into registry auth lookup

• Updates metadata fetching to call 'getAuthHeader(registry, { pkgName })' so scoped packages can select scope-specific auth headers.

deps/inspection/commands/src/fetchPackageInfo.ts


remoteTarballFetcher.ts Thread pkg name into tarball auth header lookup +5/-4

Thread pkg name into tarball auth header lookup

• Extends download options to optionally include 'pkg' and uses 'pkg.name' when calling 'getAuthHeaderByURI(url, { pkgName })'. Keeps backward compatibility when 'pkg' is absent.

fetching/tarball-fetcher/src/remoteTarballFetcher.ts


index.ts Forward auth headers to pnpr by scope +2/-2

Forward auth headers to pnpr by scope

• Switches pnpr forwarding from a flat header map to 'authHeaders[registryUri][scope]' via 'getAuthHeadersByScope()'. Keeps pnpr identity header ('authorization') separate from upstream registry auth.

installing/deps-installer/src/install/index.ts


index.ts Prefer scope auth in createGetAuthHeaderByURI when pkgName is scoped +71/-10

Prefer scope auth in createGetAuthHeaderByURI when pkgName is scoped

• Updates 'createGetAuthHeaderByURI' to accept optional '{ pkgName }' and select scope-specific headers before falling back to registry-wide auth. Optimizes lookups by indexing scoped headers by scope to avoid unnecessary URI-prefix walks.

network/auth-header/src/index.ts


install.rs Send pnpr auth headers as structured by-scope map +1/-6

Send pnpr auth headers as structured by-scope map

• Changes pnpr request construction to forward 'state.config.auth_headers.to_by_scope()' rather than flattening to '(uri, value)' pairs.

pacquet/crates/cli/src/cli_args/install.rs


package.rs Use package-aware auth lookup for registry requests +1/-1

Use package-aware auth lookup for registry requests

• Changes registry fetch requests to call 'auth_headers.for_url_with_package(..., Some(name))' so scope-specific creds are applied consistently.

pacquet/crates/registry/src/package.rs


package_version.rs Use package-aware auth lookup for version fetches +1/-1

Use package-aware auth lookup for version fetches

• Updates version registry requests to prefer scope creds using 'for_url_with_package'.

pacquet/crates/registry/src/package_version.rs


fetch_attestation_published_at.rs Apply scope-aware auth when fetching attestations +1/-1

Apply scope-aware auth when fetching attestations

• Uses 'for_url_with_package(..., Some(pkg_name))' so attestation endpoints respect package-scope auth.

pacquet/crates/resolving-npm-resolver/src/fetch_attestation_published_at.rs


fetch_full_metadata.rs Apply scope-aware auth when fetching metadata +1/-1

Apply scope-aware auth when fetching metadata

• Uses 'for_url_with_package(..., Some(pkg_name))' during metadata fetch, aligning with pnpm’s scoped auth precedence.

pacquet/crates/resolving-npm-resolver/src/fetch_full_metadata.rs


fetch_full_metadata_cached.rs Apply scope-aware auth when fetching cached metadata +1/-1

Apply scope-aware auth when fetching cached metadata

• Updates cached metadata fetch to use 'for_url_with_package' so cache paths don’t bypass scoped auth selection.

pacquet/crates/resolving-npm-resolver/src/fetch_full_metadata_cached.rs


lib.rs Apply package-aware auth to tarball downloads (tgz/zip) +2/-2

Apply package-aware auth to tarball downloads (tgz/zip)

• Updates tarball fetchers to resolve auth with 'for_url_with_package(package_url, Some(package_id))', ensuring scoped tokens are applied even when tarball hosts differ.

pacquet/crates/tarball/src/lib.rs


resolver.rs Rebuild request auth from structured by-scope map +2/-6

Rebuild request auth from structured by-scope map

• Switches request auth construction to 'AuthHeaders::from_by_scope(request.auth_headers)' for both resolve and verify endpoints.

pnpr/crates/pnpr/src/resolver.rs


common.ts Use packageName-aware auth header for deprecation requests +1/-1

Use packageName-aware auth header for deprecation requests

• Passes '{ pkgName: packageName }' into auth header lookup to ensure scoped packages use scoped credentials.

registry-access/commands/src/deprecation/common.ts


distTag.ts Use package-scope auth for dist-tag operations +6/-5

Use package-scope auth for dist-tag operations

• Updates dist-tag subcommands to select auth based on the target package name, not just the registry URL.

registry-access/commands/src/distTag.ts


owner.ts Use package-scope auth for owner operations +7/-6

Use package-scope auth for owner operations

• Updates owner list/add/remove to select auth headers using the package name so scoped packages can use scoped registry tokens.

registry-access/commands/src/owner.ts


common.ts Use optional packageName-aware auth header for star/unstar +4/-3

Use optional packageName-aware auth header for star/unstar

• Updates star/unstar auth lookup to pass pkgName when available, allowing scoped auth to be selected.

registry-access/commands/src/star/common.ts


unpublish.ts Use packageName-aware auth header for unpublish +1/-1

Use packageName-aware auth header for unpublish

• Passes '{ pkgName: packageName }' into auth header selection so scoped unpublishes can use scoped credentials.

registry-access/commands/src/unpublish.ts


publishPackedPkg.ts Preserve package-scoped publish creds when computing libnpmpublish options +10/-4

Preserve package-scoped publish creds when computing libnpmpublish options

• Updates registry info resolution to select creds by package scope (named registry key or '@') and then collapse to libnpmpublish’s default auth slot. Keeps TLS merge behavior while shifting auth selection to the new scoped config shape.

releasing/commands/src/publish/publishPackedPkg.ts


context.ts Use packageName-aware auth lookup for stage context +1/-1

Use packageName-aware auth lookup for stage context

• Updates stage context to pass pkgName into auth header lookup when stage actions are package-specific.

releasing/commands/src/stage/context.ts


createNpmResolutionVerifier.ts Thread pkgName into auth lookup for metadata, abbreviated meta, and attestations +7/-6

Thread pkgName into auth lookup for metadata, abbreviated meta, and attestations

• Updates verifier options to use the new 'GetAuthHeader' signature and passes package names into auth header selection across metadata/attestation fetches.

resolving/npm-resolver/src/createNpmResolutionVerifier.ts


index.ts Thread pkgName into resolver auth lookup for package picking +3/-3

Thread pkgName into resolver auth lookup for package picking

• Updates npm resolution flows to call 'getAuthHeaderValueByURI(registry, { pkgName: spec.name })' so scope-specific tokens apply during metadata resolution.

resolving/npm-resolver/src/index.ts


Refactor (3)
index.ts Adopt new GetAuthHeader signature in tarball fetcher +1/-1

Adopt new GetAuthHeader signature in tarball fetcher

• Updates the tarball fetcher context type so 'getAuthHeaderByURI' supports optional '{ pkgName }' during lookup.

fetching/tarball-fetcher/src/index.ts


lib.rs Update env auth presence check for scoped credential store +1/-1

Update env auth presence check for scoped credential store

• Switches the condition for enabling env-scoped auth from 'creds_by_uri' to the new 'creds_by_scope_by_uri' structure.

pacquet/crates/config/src/lib.rs


lib.rs Re-export scoped-auth types and constants +1/-1

Re-export scoped-auth types and constants

• Exports 'AuthHeadersByScope' and 'DEFAULT_REGISTRY_SCOPE' for consumers (pnpr client/server and config).

pacquet/crates/network/src/lib.rs


Tests (27)
login.test.ts Add/adjust login tests for scoped token persistence +45/-4

Add/adjust login tests for scoped token persistence

• Updates expectations to ensure scoped logins do not overwrite the registry-wide auth slot. Adds coverage for path registries and for '--scope' normalization behavior.

auth/commands/test/login.test.ts


getNetworkConfigs.test.ts Update network config tests for '@' default scope and scoped auth grouping +49/-5

Update network config tests for '@' default scope and scoped auth grouping

• Updates existing fixtures from 'creds' to the '@' default scope entry. Adds tests verifying that multiple scope tokens are grouped under a single normalized registry URI for both colon and slash key formats.

config/reader/test/getNetworkConfigs.test.ts


index.ts Adjust config reader integration expectations for new auth shape +11/-11

Adjust config reader integration expectations for new auth shape

• Updates assertions to reflect 'configByUri[uri]['@']' rather than 'configByUri[uri].creds'. Preserves security-related expectations that project-level auth keys are ignored/pinned as before.

config/reader/test/index.ts


index.ts Update audit test fixture for '@' auth scope +1/-1

Update audit test fixture for '@' auth scope

• Migrates the test config from 'creds.authToken' to '{'@': {authToken}}' for registry auth.

deps/compliance/commands/test/audit/index.ts


fetch.ts Test tarball downloads pass pkgName into auth lookup +92/-0

Test tarball downloads pass pkgName into auth lookup

• Adds a regression test ensuring tarball downloads attach scoped auth when 'pkg.name' is provided. Adds a second test verifying downloads still work without a package name.

fetching/tarball-fetcher/test/fetch.ts


auth.ts Update deps-installer auth tests to new RegistryConfig shape +7/-7

Update deps-installer auth tests to new RegistryConfig shape

• Migrates multiple fixtures from 'creds' to '{'@': ...}' for token/basic auth while preserving test intent around authenticated installs.

installing/deps-installer/test/install/auth.ts


getAuthHeaderByURI.ts Add tests for package-scope auth precedence and path registries +43/-7

Add tests for package-scope auth precedence and path registries

• Updates fixtures to use '{'@': ...}' and adds coverage ensuring '@scope' auth overrides registry auth for scoped packages (including path registries). Verifies URL basic auth still takes priority.

network/auth-header/test/getAuthHeaderByURI.ts


getAuthHeadersFromConfig.test.ts Test structured auth header outputs and by-scope conversion +65/-15

Test structured auth header outputs and by-scope conversion

• Updates tests for the new 'AuthHeaders' return shape and adds coverage for scoped headers plus 'getAuthHeadersByScope()' conversion.

network/auth-header/test/getAuthHeadersFromConfig.test.ts


tests.rs Extend pacquet npmrc auth tests for scope-specific tokens +99/-40

Extend pacquet npmrc auth tests for scope-specific tokens

• Updates existing assertions for the new creds structure and adds new tests for colon and slash scoped keys, precedence of scoped over registry-wide auth, and parity with pnpm behavior.

pacquet/crates/config/src/npmrc_auth/tests.rs


tests.rs Add pacquet tests for scoped auth precedence and round-tripping +118/-1

Add pacquet tests for scoped auth precedence and round-tripping

• Adds coverage mirroring pnpm: scoped auth wins, registry path is preserved, structured forwarding round-trips, and URL basic auth overrides scoped auth.

pacquet/crates/network/src/auth/tests.rs


integration.rs Update pnpr integration tests for structured auth headers +20/-8

Update pnpr integration tests for structured auth headers

• Adjusts request-body assertions and test helpers to build 'AuthHeadersByScope', ensuring forwarded upstream credentials are transmitted in the new shape.

pacquet/crates/pnpr-client/tests/integration.rs


tests.rs Test scoped auth is used for metadata fetches +54/-0

Test scoped auth is used for metadata fetches

• Adds a test verifying a scoped package fetch attaches the scoped bearer token in the Authorization header.

pacquet/crates/resolving-npm-resolver/src/fetch_full_metadata/tests.rs


tests.rs Test scoped auth is attached based on package id +42/-0

Test scoped auth is attached based on package id

• Adds a regression test that scoped credentials are applied when tarball URL alone would not match but the package id indicates a scoped package.

pacquet/crates/tarball/src/tests.rs


switchCliVersion.test.ts Update CLI version switch tests for new auth config shape +3/-9

Update CLI version switch tests for new auth config shape

• Migrates config fixtures from 'creds' to '{'@': ...}' for package-manager and project registry auth in tests.

pnpm/src/switchCliVersion.test.ts


syncEnvLockfile.test.ts Update env lockfile sync tests for new auth config shape +3/-9

Update env lockfile sync tests for new auth config shape

• Updates registry auth fixtures to use the '@' default scope entry in 'configByUri'.

pnpm/src/syncEnvLockfile.test.ts


deprecate.ts Update deprecate tests for '@' auth scope +1/-3

Update deprecate tests for '@' auth scope

• Migrates registry config fixtures from 'creds' to '{'@': {basicAuth}}'.

registry-access/commands/test/deprecate.ts


dist-tag.ts Add test asserting dist-tag uses package-scoped auth +35/-3

Add test asserting dist-tag uses package-scoped auth

• Updates fixtures to new auth shape and adds a mock-agent test verifying the scoped token is attached for '@scope/pkg'.

registry-access/commands/test/dist-tag.ts


star.ts Update star tests for '@' auth scope +1/-3

Update star tests for '@' auth scope

• Migrates fixture from 'creds' to '{'@': {authToken}}'.

registry-access/commands/test/star.ts


unpublish.ts Update unpublish tests for '@' auth scope +1/-3

Update unpublish tests for '@' auth scope

• Migrates fixture from 'creds' to '{'@': {basicAuth}}'.

registry-access/commands/test/unpublish.ts


whoami.ts Update whoami tests for '@' auth scope +1/-3

Update whoami tests for '@' auth scope

• Migrates fixture from 'creds' to '{'@': {authToken}}'.

registry-access/commands/test/whoami.ts


batchPublish.test.ts Update batch publish tests for '@' auth scope +2/-2

Update batch publish tests for '@' auth scope

• Migrates test config fixtures from 'creds' to '{'@': ...}' for token and basic auth.

releasing/commands/test/publish/batchPublish.test.ts


publish.ts Update publish tests for '@' auth scope +3/-9

Update publish tests for '@' auth scope

• Migrates publish-related fixtures (basic auth and token helper) from 'creds' to '{'@': ...}'.

releasing/commands/test/publish/publish.ts


publishConfigAccess.test.ts Add test: publish prefers package-scoped token over registry-wide token +20/-0

Add test: publish prefers package-scoped token over registry-wide token

• Adds coverage ensuring 'createPublishOptions()' selects '@scope' token for '@scope/pkg' even when a registry-wide token exists.

releasing/commands/test/publish/publishConfigAccess.test.ts


recursivePublish.ts Update recursive publish tests for '@' auth scope +1/-3

Update recursive publish tests for '@' auth scope

• Migrates fixture from 'creds' to '{'@': {basicAuth}}' for registry auth.

releasing/commands/test/publish/recursivePublish.ts


stage.test.ts Add stage test asserting package-scoped auth is used for list filters +24/-0

Add stage test asserting package-scoped auth is used for list filters

• Adds a regression test verifying stage list requests attach the scoped token when filtering on a scoped package name.

releasing/commands/test/stage.test.ts


createNpmResolutionVerifier.test.ts Test verifier passes pkgName to auth header lookup +35/-0

Test verifier passes pkgName to auth header lookup

• Adds a regression test ensuring scoped package verification results in auth lookup receiving the package name and attaching the scoped bearer header.

resolving/npm-resolver/test/createNpmResolutionVerifier.test.ts


index.ts Test resolver passes pkgName to auth header lookup +25/-0

Test resolver passes pkgName to auth header lookup

• Adds coverage ensuring 'resolveFromNpm()' passes the scoped package name into auth header lookup and uses the scoped token in registry requests.

resolving/npm-resolver/test/index.ts


Documentation (1)
scoped-registry-auth.md Document scope-specific auth keys and behavior +34/-0

Document scope-specific auth keys and behavior

• Adds a changeset describing the new auth key format ('//registry/:@scope:_authToken') with '@' as the registry-wide fallback. Includes an example and notes that 'pnpm login --scope' writes the scoped key.

.changeset/scoped-registry-auth.md


Other (1)
Cargo.toml Add pacquet-network dependency for AuthHeadersByScope +1/-0

Add pacquet-network dependency for AuthHeadersByScope

• Adds 'pacquet-network' as a workspace dependency so pnpr-client can use the structured auth header type.

pacquet/crates/pnpr-client/Cargo.toml


Grey Divider

Qodo Logo

Comment thread network/auth-header/src/index.ts Outdated
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 13, 2026

Copy link
Copy Markdown

Code Review by Qodo

Grey Divider

New Review Started

This review has been superseded by a new analysis

Grey Divider

Qodo Logo

@zkochan zkochan marked this pull request as draft June 13, 2026 23:59
@zkochan zkochan marked this pull request as ready for review June 14, 2026 00:01
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 14, 2026

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit cda8102

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
releasing/commands/src/publish/publishPackedPkg.ts (1)

241-255: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Preserve package-scope creds for publish, not just the @ fallback.

For scoped packages, this loop still only reads entry[DEFAULT_REGISTRY_SCOPE], so pnpm publish will ignore @orgA/@orgB credentials when both scopes point at the same registry host and fall back to the registry-wide token instead.

Suggested fix
   let creds: Creds | undefined
   let tls: RegistryConfig['tls'] = {}
+  const scopedCredKey = registryName === 'default'
+    ? DEFAULT_REGISTRY_SCOPE
+    : registryName
   for (const registryConfigKey of allRegistryConfigKeys(initialRegistryConfigKey)) {
     const entry = configByUri[registryConfigKey]
     if (!entry) continue
     // Auth from longer path collectively overrides shorter path
-    creds ??= entry[DEFAULT_REGISTRY_SCOPE]
+    creds ??= entry[scopedCredKey] ?? entry[DEFAULT_REGISTRY_SCOPE]
     // TLS from longer path individually overrides shorter path
     tls = { ...entry.tls, ...tls }
   }

   const config: RegistryConfig = { tls }
   if (creds) {
     config[DEFAULT_REGISTRY_SCOPE] = creds
   }

This conflicts with the PR objective of allowing different auth tokens per scope on the same registry host.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@releasing/commands/src/publish/publishPackedPkg.ts` around lines 241 - 255,
The credential lookup in the loop only reads from entry[DEFAULT_REGISTRY_SCOPE],
ignoring package-scoped credentials like `@orgA` or `@orgB`. Modify the credential
assignment logic to read the package-specific scope credentials first before
falling back to DEFAULT_REGISTRY_SCOPE. Extract the specific scope identifier
from initialRegistryConfigKey (which contains the scoped package information)
and use it to look up entry[specificScope] first, only using
entry[DEFAULT_REGISTRY_SCOPE] as a fallback. This ensures that when multiple
scopes point to the same registry host, each scope retains its own
authentication token rather than all falling back to the default registry-wide
token.
🧹 Nitpick comments (1)
pacquet/crates/config/src/npmrc_auth/tests.rs (1)

1-5: 💤 Low value

Consider reordering imports to follow standard Rust conventions.

Standard Rust style places std imports first, external crates second (alphabetically), and local imports (crate, super) last. The current order mixes these groups.

📦 Suggested import order
-use super::{EnvVar, NpmrcAuth, RawCreds, base64_decode, base64_encode};
-use crate::Config;
-use pacquet_network::{DEFAULT_REGISTRY_SCOPE, NoProxySetting};
-use pretty_assertions::assert_eq;
-use std::path::Path;
+use std::path::Path;
+
+use pacquet_network::{DEFAULT_REGISTRY_SCOPE, NoProxySetting};
+use pretty_assertions::assert_eq;
+
+use crate::Config;
+use super::{EnvVar, NpmrcAuth, RawCreds, base64_decode, base64_encode};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pacquet/crates/config/src/npmrc_auth/tests.rs` around lines 1 - 5, The
imports in the test file do not follow standard Rust conventions for import
ordering. Reorder the imports at the top of the file to place std library
imports first (use std::path::Path), followed by external crates in alphabetical
order (use pacquet_network and use pretty_assertions), and finally local imports
(use crate and use super) last. This ensures the imports follow the standard
Rust style guide.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@releasing/commands/src/publish/publishPackedPkg.ts`:
- Around line 241-255: The credential lookup in the loop only reads from
entry[DEFAULT_REGISTRY_SCOPE], ignoring package-scoped credentials like `@orgA` or
`@orgB`. Modify the credential assignment logic to read the package-specific scope
credentials first before falling back to DEFAULT_REGISTRY_SCOPE. Extract the
specific scope identifier from initialRegistryConfigKey (which contains the
scoped package information) and use it to look up entry[specificScope] first,
only using entry[DEFAULT_REGISTRY_SCOPE] as a fallback. This ensures that when
multiple scopes point to the same registry host, each scope retains its own
authentication token rather than all falling back to the default registry-wide
token.

---

Nitpick comments:
In `@pacquet/crates/config/src/npmrc_auth/tests.rs`:
- Around line 1-5: The imports in the test file do not follow standard Rust
conventions for import ordering. Reorder the imports at the top of the file to
place std library imports first (use std::path::Path), followed by external
crates in alphabetical order (use pacquet_network and use pretty_assertions),
and finally local imports (use crate and use super) last. This ensures the
imports follow the standard Rust style guide.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 85e533a0-f8bd-469f-b010-de2370aea018

📥 Commits

Reviewing files that changed from the base of the PR and between 8dfe438 and cc61e74.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (52)
  • .changeset/scoped-registry-auth.md
  • config/reader/src/getNetworkConfigs.ts
  • config/reader/test/getNetworkConfigs.test.ts
  • config/reader/test/index.ts
  • core/types/src/misc.ts
  • deps/compliance/commands/test/audit/index.ts
  • fetching/tarball-fetcher/src/index.ts
  • fetching/tarball-fetcher/src/remoteTarballFetcher.ts
  • fetching/tarball-fetcher/test/fetch.ts
  • fetching/types/src/index.ts
  • installing/deps-installer/src/install/index.ts
  • installing/deps-installer/test/install/auth.ts
  • network/auth-header/src/getAuthHeadersFromConfig.ts
  • network/auth-header/src/index.ts
  • network/auth-header/test/getAuthHeaderByURI.ts
  • network/auth-header/test/getAuthHeadersFromConfig.test.ts
  • pacquet/crates/cli/src/cli_args/install.rs
  • pacquet/crates/config/src/lib.rs
  • pacquet/crates/config/src/npmrc_auth.rs
  • pacquet/crates/config/src/npmrc_auth/tests.rs
  • pacquet/crates/network/src/auth.rs
  • pacquet/crates/network/src/auth/tests.rs
  • pacquet/crates/network/src/lib.rs
  • pacquet/crates/pnpr-client/Cargo.toml
  • pacquet/crates/pnpr-client/src/lib.rs
  • pacquet/crates/pnpr-client/tests/integration.rs
  • pacquet/crates/registry/src/package.rs
  • pacquet/crates/registry/src/package_version.rs
  • pacquet/crates/resolving-npm-resolver/src/fetch_attestation_published_at.rs
  • pacquet/crates/resolving-npm-resolver/src/fetch_full_metadata.rs
  • pacquet/crates/resolving-npm-resolver/src/fetch_full_metadata/tests.rs
  • pacquet/crates/resolving-npm-resolver/src/fetch_full_metadata_cached.rs
  • pacquet/crates/tarball/src/lib.rs
  • pacquet/crates/tarball/src/tests.rs
  • pnpm/src/switchCliVersion.test.ts
  • pnpm/src/syncEnvLockfile.test.ts
  • pnpr/client/src/resolveViaPnprServer.ts
  • pnpr/crates/pnpr/src/resolver.rs
  • pnpr/crates/pnpr/src/resolver/protocol.rs
  • registry-access/commands/test/deprecate.ts
  • registry-access/commands/test/dist-tag.ts
  • registry-access/commands/test/star.ts
  • registry-access/commands/test/unpublish.ts
  • registry-access/commands/test/whoami.ts
  • releasing/commands/src/publish/publishPackedPkg.ts
  • releasing/commands/test/publish/batchPublish.test.ts
  • releasing/commands/test/publish/publish.ts
  • releasing/commands/test/publish/recursivePublish.ts
  • resolving/npm-resolver/src/createNpmResolutionVerifier.ts
  • resolving/npm-resolver/src/index.ts
  • resolving/npm-resolver/test/createNpmResolutionVerifier.test.ts
  • resolving/npm-resolver/test/index.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: ubuntu-latest / Node.js 26.3.0 / Test
  • GitHub Check: ubuntu-latest / Node.js 22.13.0 / Test
  • GitHub Check: windows-latest / Node.js 22.13.0 / Test
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Use Standard Style with trailing commas, prefer functions over classes, declare functions after they are used relying on hoisting, limit function arguments to two or three with options objects for additional parameters
Follow import order: standard libraries, external dependencies (sorted alphabetically), then relative imports
Use JSDoc for function contracts (preconditions, postconditions, edge cases, why it exists) not for re-narrating the body; do not record past implementation shape or refactor history in comments

Files:

  • fetching/types/src/index.ts
  • fetching/tarball-fetcher/src/index.ts
  • core/types/src/misc.ts
  • releasing/commands/test/publish/recursivePublish.ts
  • registry-access/commands/test/dist-tag.ts
  • registry-access/commands/test/unpublish.ts
  • registry-access/commands/test/deprecate.ts
  • pnpr/client/src/resolveViaPnprServer.ts
  • releasing/commands/test/publish/batchPublish.test.ts
  • fetching/tarball-fetcher/test/fetch.ts
  • registry-access/commands/test/whoami.ts
  • registry-access/commands/test/star.ts
  • pnpm/src/syncEnvLockfile.test.ts
  • resolving/npm-resolver/test/index.ts
  • installing/deps-installer/src/install/index.ts
  • resolving/npm-resolver/test/createNpmResolutionVerifier.test.ts
  • deps/compliance/commands/test/audit/index.ts
  • network/auth-header/test/getAuthHeadersFromConfig.test.ts
  • releasing/commands/src/publish/publishPackedPkg.ts
  • pnpm/src/switchCliVersion.test.ts
  • fetching/tarball-fetcher/src/remoteTarballFetcher.ts
  • releasing/commands/test/publish/publish.ts
  • config/reader/test/getNetworkConfigs.test.ts
  • network/auth-header/src/index.ts
  • resolving/npm-resolver/src/index.ts
  • network/auth-header/test/getAuthHeaderByURI.ts
  • installing/deps-installer/test/install/auth.ts
  • network/auth-header/src/getAuthHeadersFromConfig.ts
  • config/reader/test/index.ts
  • config/reader/src/getNetworkConfigs.ts
  • resolving/npm-resolver/src/createNpmResolutionVerifier.ts
pacquet/**/*.rs

📄 CodeRabbit inference engine (pacquet/AGENTS.md)

pacquet/**/*.rs: Match how the same feature is implemented in the TypeScript pnpm CLI in pnpm/, pkg-manager/, resolving/, lockfile/, store/, fetching/, config/, hooks/, and other TypeScript workspaces — behavior, flags, defaults, error codes, file formats, and directory layout must match pnpm exactly
When porting a function that fires pnpm: events through globalLogger, logger.debug(...), or streamParser.write(...), mirror the call site, payload, and ordering so @pnpm/cli.default-reporter parses pacquet's NDJSON the same way it parses pnpm's. Follow the Reporter / log events convention for channel mapping, threading R: Reporter, emit-site placement, and recording-fake tests.
Prefer real fixtures using tempfile::TempDir, the mocked registry, or integration tests over dependency-injection seams for testing. Only use the DI seam (Host capability trait, Sys bounds, etc.) for branches real fixtures cannot cover: filesystem error kinds (PermissionDenied, ENOSPC), deterministic time, shared process-global state (env::set_var, set_current_dir, umask), or external-service happy paths (pnpm login 2FA, pnpm publish OIDC/provenance). Follow the eight principles and naming conventions (Sys, Host, Fs*, Clock, EnvVar) documented in CODE_STYLE_GUIDE.md.
Declare a newtype wrapper when porting code using branded string types from TypeScript pnpm. Do not collapse the brand into plain String or &str. Give the type its own struct so misuse is a type error.
If upstream always validates before construction of a branded string type, validate too. The Rust wrapper must construct only via TryFrom and/or FromStr. Do not provide an infallible public constructor that takes an arbitrary string.
If upstream never validates a branded string type, just brand for type-safety. Expose an infallible From and From<&str> when convenient. The type-safety win is the whole point, and no validator is needed.
If upstream occasionally constructs a branded string type without validatio...

Files:

  • pacquet/crates/config/src/lib.rs
  • pacquet/crates/network/src/lib.rs
  • pacquet/crates/resolving-npm-resolver/src/fetch_full_metadata/tests.rs
  • pacquet/crates/resolving-npm-resolver/src/fetch_full_metadata.rs
  • pacquet/crates/resolving-npm-resolver/src/fetch_full_metadata_cached.rs
  • pacquet/crates/tarball/src/tests.rs
  • pacquet/crates/cli/src/cli_args/install.rs
  • pacquet/crates/resolving-npm-resolver/src/fetch_attestation_published_at.rs
  • pacquet/crates/registry/src/package_version.rs
  • pacquet/crates/registry/src/package.rs
  • pacquet/crates/tarball/src/lib.rs
  • pacquet/crates/network/src/auth/tests.rs
  • pacquet/crates/pnpr-client/src/lib.rs
  • pacquet/crates/pnpr-client/tests/integration.rs
  • pacquet/crates/config/src/npmrc_auth/tests.rs
  • pacquet/crates/network/src/auth.rs
  • pacquet/crates/config/src/npmrc_auth.rs
pnpr/**/pnpr/**/*.rs

📄 CodeRabbit inference engine (pnpr/AGENTS.md)

pnpr/**/pnpr/**/*.rs: Follow the pacquet code-style guide (../pacquet/CODE_STYLE_GUIDE.md) for Rust-level conventions including imports, naming, ownership, and error handling
Follow the pacquet contributing guide (../pacquet/CONTRIBUTING.md) for test layout and Rust conventions

Files:

  • pnpr/crates/pnpr/src/resolver.rs
  • pnpr/crates/pnpr/src/resolver/protocol.rs
**/*.test.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

When checking if a caught error is an Error object in Jest, use util.types.isNativeError() instead of instanceof Error because instanceof checks can fail across VM realms

Files:

  • releasing/commands/test/publish/batchPublish.test.ts
  • pnpm/src/syncEnvLockfile.test.ts
  • resolving/npm-resolver/test/createNpmResolutionVerifier.test.ts
  • network/auth-header/test/getAuthHeadersFromConfig.test.ts
  • pnpm/src/switchCliVersion.test.ts
  • config/reader/test/getNetworkConfigs.test.ts
🧠 Learnings (10)
📚 Learning: 2026-05-26T21:01:06.666Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11966
File: .changeset/require-tarball-integrity.md:6-6
Timestamp: 2026-05-26T21:01:06.666Z
Learning: In pnpm lockfile-related release notes/docs (especially changeset markdown), preserve URL hostnames exactly as they appear in pnpm-lock.yaml tarball resolution entries—keep hosts like `codeload.github.com`, `bitbucket.org`, and `gitlab.com` in lowercase. Do not “correct” them to title-case/preserve brand capitalization (e.g., LanguageTool rules like `GITHUB` capitalization) because these are literal URL fragments, not platform brand names.

Applied to files:

  • .changeset/scoped-registry-auth.md
📚 Learning: 2026-05-14T09:04:00.133Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11622
File: resolving/npm-resolver/test/publishedBy.test.ts:350-354
Timestamp: 2026-05-14T09:04:00.133Z
Learning: In the pnpm/pnpm repository, ESLint is the authoritative style linter. Do not raise review findings for missing trailing commas in multiline function calls (e.g., `fs.writeFileSync(...)`) when this repo’s ESLint configuration does not report them and lint passes. Prefer deferring to the ESLint results for this specific trailing-comma rule rather than enforcing it manually in code review.

Applied to files:

  • fetching/types/src/index.ts
  • fetching/tarball-fetcher/src/index.ts
  • core/types/src/misc.ts
  • releasing/commands/test/publish/recursivePublish.ts
  • registry-access/commands/test/dist-tag.ts
  • registry-access/commands/test/unpublish.ts
  • registry-access/commands/test/deprecate.ts
  • pnpr/client/src/resolveViaPnprServer.ts
  • releasing/commands/test/publish/batchPublish.test.ts
  • fetching/tarball-fetcher/test/fetch.ts
  • registry-access/commands/test/whoami.ts
  • registry-access/commands/test/star.ts
  • pnpm/src/syncEnvLockfile.test.ts
  • resolving/npm-resolver/test/index.ts
  • installing/deps-installer/src/install/index.ts
  • resolving/npm-resolver/test/createNpmResolutionVerifier.test.ts
  • deps/compliance/commands/test/audit/index.ts
  • network/auth-header/test/getAuthHeadersFromConfig.test.ts
  • releasing/commands/src/publish/publishPackedPkg.ts
  • pnpm/src/switchCliVersion.test.ts
  • fetching/tarball-fetcher/src/remoteTarballFetcher.ts
  • releasing/commands/test/publish/publish.ts
  • config/reader/test/getNetworkConfigs.test.ts
  • network/auth-header/src/index.ts
  • resolving/npm-resolver/src/index.ts
  • network/auth-header/test/getAuthHeaderByURI.ts
  • installing/deps-installer/test/install/auth.ts
  • network/auth-header/src/getAuthHeadersFromConfig.ts
  • config/reader/test/index.ts
  • config/reader/src/getNetworkConfigs.ts
  • resolving/npm-resolver/src/createNpmResolutionVerifier.ts
📚 Learning: 2026-06-05T13:47:26.046Z
Learnt from: vsumner
Repo: pnpm/pnpm PR: 12190
File: installing/deps-installer/src/install/index.ts:2337-2343
Timestamp: 2026-06-05T13:47:26.046Z
Learning: In the pnpm/pnpm codebase, `PnpmError` automatically prefixes `err.code` with `ERR_PNPM_` when you pass a code that does not already start with `ERR_PNPM_` (it normalizes `this.code` via `code.startsWith('ERR_PNPM_') ? code : `ERR_PNPM_${code}``). Therefore, during code review you should NOT flag `new PnpmError(...)` call sites for passing a bare error code (e.g., `new PnpmError('FROZEN_STORE_INCOMPATIBLE_WITH_PNPR', ...)`); the resulting `err.code` will still be `ERR_PNPM_FROZEN_STORE_INCOMPATIBLE_WITH_PNPR`.

Applied to files:

  • fetching/types/src/index.ts
  • fetching/tarball-fetcher/src/index.ts
  • core/types/src/misc.ts
  • releasing/commands/test/publish/recursivePublish.ts
  • registry-access/commands/test/dist-tag.ts
  • registry-access/commands/test/unpublish.ts
  • registry-access/commands/test/deprecate.ts
  • pnpr/client/src/resolveViaPnprServer.ts
  • releasing/commands/test/publish/batchPublish.test.ts
  • fetching/tarball-fetcher/test/fetch.ts
  • registry-access/commands/test/whoami.ts
  • registry-access/commands/test/star.ts
  • pnpm/src/syncEnvLockfile.test.ts
  • resolving/npm-resolver/test/index.ts
  • installing/deps-installer/src/install/index.ts
  • resolving/npm-resolver/test/createNpmResolutionVerifier.test.ts
  • deps/compliance/commands/test/audit/index.ts
  • network/auth-header/test/getAuthHeadersFromConfig.test.ts
  • releasing/commands/src/publish/publishPackedPkg.ts
  • pnpm/src/switchCliVersion.test.ts
  • fetching/tarball-fetcher/src/remoteTarballFetcher.ts
  • releasing/commands/test/publish/publish.ts
  • config/reader/test/getNetworkConfigs.test.ts
  • network/auth-header/src/index.ts
  • resolving/npm-resolver/src/index.ts
  • network/auth-header/test/getAuthHeaderByURI.ts
  • installing/deps-installer/test/install/auth.ts
  • network/auth-header/src/getAuthHeadersFromConfig.ts
  • config/reader/test/index.ts
  • config/reader/src/getNetworkConfigs.ts
  • resolving/npm-resolver/src/createNpmResolutionVerifier.ts
📚 Learning: 2026-05-20T19:40:55.051Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11774
File: pacquet/crates/resolving-deps-resolver/src/resolve_peers.rs:0-0
Timestamp: 2026-05-20T19:40:55.051Z
Learning: In the pacquet Rust code, ensure the semver implementation uses the `node-semver` crate (not `nodejs-semver`). `node-semver`’s public API does not include a `satisfies_with_prerelease`-style method; prerelease-tolerant matching should be implemented inline by first calling `Range::satisfies`, and when it rejects a prerelease version, retry matching against a stripped `MAJOR.MINOR.PATCH` base of the prerelease version.

Applied to files:

  • pacquet/crates/config/src/lib.rs
  • pacquet/crates/network/src/lib.rs
  • pacquet/crates/resolving-npm-resolver/src/fetch_full_metadata/tests.rs
  • pacquet/crates/resolving-npm-resolver/src/fetch_full_metadata.rs
  • pacquet/crates/resolving-npm-resolver/src/fetch_full_metadata_cached.rs
  • pacquet/crates/tarball/src/tests.rs
  • pacquet/crates/cli/src/cli_args/install.rs
  • pacquet/crates/resolving-npm-resolver/src/fetch_attestation_published_at.rs
  • pacquet/crates/registry/src/package_version.rs
  • pacquet/crates/registry/src/package.rs
  • pacquet/crates/tarball/src/lib.rs
  • pacquet/crates/network/src/auth/tests.rs
  • pacquet/crates/pnpr-client/src/lib.rs
  • pacquet/crates/pnpr-client/tests/integration.rs
  • pacquet/crates/config/src/npmrc_auth/tests.rs
  • pacquet/crates/network/src/auth.rs
  • pacquet/crates/config/src/npmrc_auth.rs
📚 Learning: 2026-05-22T00:08:44.646Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11837
File: pacquet/crates/resolving-npm-resolver/src/pick_package.rs:33-51
Timestamp: 2026-05-22T00:08:44.646Z
Learning: In the pnpm/pnpm repo’s pacquet Rust crates, do not flag Unicode ellipsis characters (U+2026, `…`) in Rust doc comments (`///` / `/** */`) as a lint violation. The pacquet crate’s `dylint.toml` only enables `perfectionist::derive_ordering`, and the Dylint `unicode-ellipsis` rule is not enabled for this project—so `…` in doc comments is an intentional, repo-consistent style.

Applied to files:

  • pacquet/crates/config/src/lib.rs
  • pacquet/crates/network/src/lib.rs
  • pacquet/crates/resolving-npm-resolver/src/fetch_full_metadata/tests.rs
  • pacquet/crates/resolving-npm-resolver/src/fetch_full_metadata.rs
  • pacquet/crates/resolving-npm-resolver/src/fetch_full_metadata_cached.rs
  • pacquet/crates/tarball/src/tests.rs
  • pacquet/crates/cli/src/cli_args/install.rs
  • pacquet/crates/resolving-npm-resolver/src/fetch_attestation_published_at.rs
  • pacquet/crates/registry/src/package_version.rs
  • pacquet/crates/registry/src/package.rs
  • pacquet/crates/tarball/src/lib.rs
  • pacquet/crates/network/src/auth/tests.rs
  • pacquet/crates/pnpr-client/src/lib.rs
  • pacquet/crates/pnpr-client/tests/integration.rs
  • pacquet/crates/config/src/npmrc_auth/tests.rs
  • pacquet/crates/network/src/auth.rs
  • pacquet/crates/config/src/npmrc_auth.rs
📚 Learning: 2026-05-20T23:07:58.444Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11784
File: pacquet/crates/resolving-deps-resolver/src/hoist_peers.rs:120-133
Timestamp: 2026-05-20T23:07:58.444Z
Learning: When reviewing code in this pacquet Rust port, follow the upstream pnpm compatibility rule: only match pnpm’s behavior exactly. Do not propose review changes that intentionally deviate from pnpm’s documented/observed behavior, even if pnpm appears buggy. If you identify a real bug in pnpm behavior, the review should prioritize fixing it upstream in pnpm first, and avoid implementing a pnpm-behavior workaround here unless the same fix has already landed upstream.

Applied to files:

  • pacquet/crates/config/src/lib.rs
  • pacquet/crates/network/src/lib.rs
  • pacquet/crates/resolving-npm-resolver/src/fetch_full_metadata/tests.rs
  • pacquet/crates/resolving-npm-resolver/src/fetch_full_metadata.rs
  • pacquet/crates/resolving-npm-resolver/src/fetch_full_metadata_cached.rs
  • pacquet/crates/tarball/src/tests.rs
  • pacquet/crates/cli/src/cli_args/install.rs
  • pacquet/crates/resolving-npm-resolver/src/fetch_attestation_published_at.rs
  • pacquet/crates/registry/src/package_version.rs
  • pacquet/crates/registry/src/package.rs
  • pacquet/crates/tarball/src/lib.rs
  • pacquet/crates/network/src/auth/tests.rs
  • pacquet/crates/pnpr-client/src/lib.rs
  • pacquet/crates/pnpr-client/tests/integration.rs
  • pacquet/crates/config/src/npmrc_auth/tests.rs
  • pacquet/crates/network/src/auth.rs
  • pacquet/crates/config/src/npmrc_auth.rs
📚 Learning: 2026-06-06T18:58:37.156Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 12243
File: pacquet/crates/package-manager/src/install_package_by_snapshot.rs:319-322
Timestamp: 2026-06-06T18:58:37.156Z
Learning: When reviewing Rust code, do not assume `matches!(expr, Pattern(_))` will move out of `expr` if `Pattern(_)` contains no by-value bindings. `matches!` desugars to a `match` that auto-borrows the scrutinee for discrimination, so even if `expr` is a non-`Copy` value behind a shared reference (e.g., `&T`), the macro should not move-out of the borrowed data purely due to `matches!`. Treat `matches!(&expr, Pattern(_))` as a readability/clarity improvement, not a correctness requirement. Only flag potential move-out-of-borrow risks when the pattern includes by-value bindings that would require moving the matched value.

Applied to files:

  • pacquet/crates/config/src/lib.rs
  • pacquet/crates/network/src/lib.rs
  • pacquet/crates/resolving-npm-resolver/src/fetch_full_metadata/tests.rs
  • pacquet/crates/resolving-npm-resolver/src/fetch_full_metadata.rs
  • pacquet/crates/resolving-npm-resolver/src/fetch_full_metadata_cached.rs
  • pacquet/crates/tarball/src/tests.rs
  • pacquet/crates/cli/src/cli_args/install.rs
  • pnpr/crates/pnpr/src/resolver.rs
  • pacquet/crates/resolving-npm-resolver/src/fetch_attestation_published_at.rs
  • pacquet/crates/registry/src/package_version.rs
  • pacquet/crates/registry/src/package.rs
  • pnpr/crates/pnpr/src/resolver/protocol.rs
  • pacquet/crates/tarball/src/lib.rs
  • pacquet/crates/network/src/auth/tests.rs
  • pacquet/crates/pnpr-client/src/lib.rs
  • pacquet/crates/pnpr-client/tests/integration.rs
  • pacquet/crates/config/src/npmrc_auth/tests.rs
  • pacquet/crates/network/src/auth.rs
  • pacquet/crates/config/src/npmrc_auth.rs
📚 Learning: 2026-06-12T20:41:57.558Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 12364
File: pacquet/crates/package-manager/src/install/tests.rs:5717-5717
Timestamp: 2026-06-12T20:41:57.558Z
Learning: In the pnpm/pnpm Rust workspace (toolchain Rust 1.95.0), keep using `std::time::Duration::from_mins(...)` and `std::time::Duration::from_hours(...)` as-is. Do not flag these as invalid or suggest replacing them with `Duration::from_secs(...)`, because Clippy’s `clippy::duration_suboptimal_units` lint is denied by `-D warnings` in CI, and changing to seconds may trigger CI failures.

Applied to files:

  • pacquet/crates/config/src/lib.rs
  • pacquet/crates/network/src/lib.rs
  • pacquet/crates/resolving-npm-resolver/src/fetch_full_metadata/tests.rs
  • pacquet/crates/resolving-npm-resolver/src/fetch_full_metadata.rs
  • pacquet/crates/resolving-npm-resolver/src/fetch_full_metadata_cached.rs
  • pacquet/crates/tarball/src/tests.rs
  • pacquet/crates/cli/src/cli_args/install.rs
  • pnpr/crates/pnpr/src/resolver.rs
  • pacquet/crates/resolving-npm-resolver/src/fetch_attestation_published_at.rs
  • pacquet/crates/registry/src/package_version.rs
  • pacquet/crates/registry/src/package.rs
  • pnpr/crates/pnpr/src/resolver/protocol.rs
  • pacquet/crates/tarball/src/lib.rs
  • pacquet/crates/network/src/auth/tests.rs
  • pacquet/crates/pnpr-client/src/lib.rs
  • pacquet/crates/pnpr-client/tests/integration.rs
  • pacquet/crates/config/src/npmrc_auth/tests.rs
  • pacquet/crates/network/src/auth.rs
  • pacquet/crates/config/src/npmrc_auth.rs
📚 Learning: 2026-06-05T13:47:05.929Z
Learnt from: vsumner
Repo: pnpm/pnpm PR: 12190
File: installing/deps-installer/test/install/frozenStore.ts:2-17
Timestamp: 2026-06-05T13:47:05.929Z
Learning: In the pnpm/pnpm repository, the shared Jest preset keeps `injectGlobals` at its default (`true`), so `test` and `expect` are available as Jest globals. Therefore, reviewers should not flag (or treat as TypeScript/compilation errors) missing `import { test, expect } from 'jest/globals'` when a test file uses `test`/`expect` without importing them. Importing from `jest/globals` may still be used for consistency with sibling files, but it is not required for execution in this repo unless a Jest preset is explicitly configured with `injectGlobals: false`.

Applied to files:

  • releasing/commands/test/publish/recursivePublish.ts
  • registry-access/commands/test/dist-tag.ts
  • registry-access/commands/test/unpublish.ts
  • registry-access/commands/test/deprecate.ts
  • releasing/commands/test/publish/batchPublish.test.ts
  • fetching/tarball-fetcher/test/fetch.ts
  • registry-access/commands/test/whoami.ts
  • registry-access/commands/test/star.ts
  • resolving/npm-resolver/test/index.ts
  • resolving/npm-resolver/test/createNpmResolutionVerifier.test.ts
  • deps/compliance/commands/test/audit/index.ts
  • network/auth-header/test/getAuthHeadersFromConfig.test.ts
  • releasing/commands/test/publish/publish.ts
  • config/reader/test/getNetworkConfigs.test.ts
  • network/auth-header/test/getAuthHeaderByURI.ts
  • installing/deps-installer/test/install/auth.ts
  • config/reader/test/index.ts
📚 Learning: 2026-05-23T17:29:56.247Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11878
File: resolving/npm-resolver/src/createNpmResolutionVerifier.ts:381-418
Timestamp: 2026-05-23T17:29:56.247Z
Learning: When reviewing the npm resolver code, note that `PackageMetaCache` is intentionally keyed only by `name` and `name:full` (no registry component). As a result, code that shares this cache (e.g., `createNpmResolutionVerifier.ts` via the shared `validateSharedMeta` guard) can prevent cross-package contamination by matching names, but it cannot distinguish two different registries that serve packages with the same name within a single install. Don’t flag (or attempt to fix) this as a local issue in the verifier alone—correctly distinguishing registries would require a coordinated change to the resolver cache key shape. (The Pacquet/Rust cache is already registry-qualified, unlike the npm-resolver cache.)

Applied to files:

  • resolving/npm-resolver/src/index.ts
  • resolving/npm-resolver/src/createNpmResolutionVerifier.ts
🔇 Additional comments (49)
core/types/src/misc.ts (1)

31-32: LGTM!

Also applies to: 54-57

fetching/types/src/index.ts (1)

23-27: LGTM!

.changeset/scoped-registry-auth.md (1)

1-16: LGTM!

config/reader/src/getNetworkConfigs.ts (1)

3-3: LGTM!

Also applies to: 14-14, 25-28, 44-51, 77-81, 93-122

network/auth-header/src/getAuthHeadersFromConfig.ts (1)

4-56: LGTM!

Also applies to: 58-61

network/auth-header/src/index.ts (1)

4-4: LGTM!

Also applies to: 7-26, 35-96

pnpr/client/src/resolveViaPnprServer.ts (1)

11-12: LGTM!

Also applies to: 40-45

pnpm/src/switchCliVersion.test.ts (1)

109-109: LGTM!

Also applies to: 118-118, 171-171

registry-access/commands/test/star.ts (1)

10-10: LGTM!

registry-access/commands/test/whoami.ts (1)

11-11: LGTM!

resolving/npm-resolver/test/createNpmResolutionVerifier.test.ts (1)

78-111: LGTM!

resolving/npm-resolver/test/index.ts (1)

323-346: LGTM!

config/reader/test/getNetworkConfigs.test.ts (1)

91-92: LGTM!

Also applies to: 105-111, 125-131, 144-145, 157-158, 164-183

config/reader/test/index.ts (1)

727-727: LGTM!

Also applies to: 908-913, 1314-1315, 1332-1333, 1350-1351, 1376-1377, 1393-1394, 1409-1410, 1429-1430, 1449-1450

deps/compliance/commands/test/audit/index.ts (1)

283-283: LGTM!

network/auth-header/test/getAuthHeadersFromConfig.test.ts (1)

6-7: LGTM!

Also applies to: 34-43, 47-54, 58-65, 69-76, 80-86, 90-94, 95-129

installing/deps-installer/test/install/auth.ts (1)

24-24: LGTM!

Also applies to: 40-40, 62-63, 83-84, 131-132, 179-180, 205-206

pnpm/src/syncEnvLockfile.test.ts (1)

150-150: LGTM!

Also applies to: 160-160, 198-198

registry-access/commands/test/deprecate.ts (1)

16-20: LGTM!

registry-access/commands/test/dist-tag.ts (1)

13-17: LGTM!

registry-access/commands/test/unpublish.ts (1)

16-20: LGTM!

releasing/commands/test/publish/batchPublish.test.ts (1)

87-89: LGTM!

Also applies to: 210-212

releasing/commands/test/publish/publish.ts (1)

21-24: LGTM!

Also applies to: 983-985, 1010-1012

releasing/commands/test/publish/recursivePublish.ts (1)

17-20: LGTM!

pacquet/crates/network/src/auth.rs (1)

20-24: LGTM!

Also applies to: 30-32, 39-42, 47-49, 85-85, 98-113, 115-172, 186-194, 213-223, 234-250, 278-309

pacquet/crates/network/src/lib.rs (1)

9-9: LGTM!

pacquet/crates/resolving-npm-resolver/src/fetch_full_metadata.rs (1)

114-114: LGTM!

pnpr/crates/pnpr/src/resolver/protocol.rs (1)

6-7: LGTM!

Also applies to: 56-61

pacquet/crates/pnpr-client/Cargo.toml (1)

17-17: LGTM!

pacquet/crates/network/src/auth/tests.rs (1)

1-1: LGTM!

Also applies to: 136-234

pacquet/crates/resolving-npm-resolver/src/fetch_full_metadata/tests.rs (1)

101-153: LGTM!

pacquet/crates/config/src/npmrc_auth.rs (1)

3-5: LGTM!

Also applies to: 53-55, 203-204, 369-370, 515-538, 546-547, 601-606, 657-661, 696-703, 899-913

pacquet/crates/config/src/lib.rs (1)

1799-1799: LGTM!

pacquet/crates/registry/src/package.rs (1)

132-132: LGTM!

pacquet/crates/registry/src/package_version.rs (1)

270-270: LGTM!

pacquet/crates/pnpr-client/src/lib.rs (1)

24-25: LGTM!

Also applies to: 50-53, 92-92

pnpr/crates/pnpr/src/resolver.rs (1)

222-222: LGTM!

Also applies to: 318-318

pacquet/crates/config/src/npmrc_auth/tests.rs (3)

36-48: LGTM!


149-168: LGTM!


170-192: LGTM!

pacquet/crates/resolving-npm-resolver/src/fetch_full_metadata_cached.rs (1)

126-127: LGTM!

pacquet/crates/resolving-npm-resolver/src/fetch_attestation_published_at.rs (1)

58-60: LGTM!

pacquet/crates/cli/src/cli_args/install.rs (1)

603-605: LGTM!

pacquet/crates/tarball/src/lib.rs (1)

1571-1572: No correctness issue found: for_url_with_package correctly handles package_id shapes passed at these call sites.

The package_scope() function extracts the scope from package IDs like @fastify/error@3.3.0 by splitting on the first /, which correctly isolates @fastify for scoped token lookup. Existing tests explicitly verify this behavior with the same @scope/name@version format used in the tarball and zip fetch paths.

pacquet/crates/tarball/src/tests.rs (1)

1641-1681: LGTM!

pacquet/crates/pnpr-client/tests/integration.rs (4)

17-17: LGTM!


55-61: LGTM!


125-125: LGTM!

Also applies to: 135-136


157-160: LGTM!

Also applies to: 381-384

Comment thread pnpr/crates/pnpr/src/resolver/protocol.rs
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 14, 2026

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 10adc40

@zkochan zkochan marked this pull request as draft June 14, 2026 00:12
@zkochan zkochan marked this pull request as ready for review June 14, 2026 08:10
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 14, 2026

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 10adc40

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 14, 2026

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 6fb5b19

@zkochan zkochan marked this pull request as draft June 14, 2026 08:27
@zkochan zkochan marked this pull request as ready for review June 14, 2026 08:50
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 14, 2026

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit a8f7949

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 14, 2026

Copy link
Copy Markdown

Code Review by Qodo

Grey Divider

New Review Started

This review has been superseded by a new analysis

Grey Divider

Qodo Logo

@zkochan zkochan marked this pull request as draft June 14, 2026 09:37
@zkochan zkochan marked this pull request as ready for review June 14, 2026 09:38
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 14, 2026

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 719439c

@zkochan zkochan merged commit 681b593 into main Jun 14, 2026
25 checks passed
@zkochan zkochan deleted the feat/12390 branch June 14, 2026 09:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Using different auth tokens for different scopes in the same registry

2 participants