Skip to content

feat(pnpr): forward uplink auth token and custom headers to upstreams#12186

Merged
zkochan merged 5 commits into
mainfrom
feat/pnpr-uplink-auth-headers
Jun 5, 2026
Merged

feat(pnpr): forward uplink auth token and custom headers to upstreams#12186
zkochan merged 5 commits into
mainfrom
feat/pnpr-uplink-auth-headers

Conversation

@juanpicado

@juanpicado juanpicado commented Jun 4, 2026

Copy link
Copy Markdown
Member

pnpr now attaches a per-uplink Authorization header (derived from the verdaccio-shaped auth: block) plus any operator-supplied custom headers: on every packument and tarball request it makes to an upstream registry. This unblocks proxying private upstreams (CodeArtifact, GitHub Packages, authed npm Enterprise, private verdaccio) that previously returned 401/403.

auth supports type: bearer | basic, an inline token, and verdaccio's token_env (true -> NPM_TOKEN, or a named var); an inline token takes priority. A custom headers.Authorization overrides the auth-derived one, matching verdaccio's merge order. Tokens/headers resolve once at config load through the existing EnvVar seam, so a missing token or invalid header value fails fast as an InvalidConfig error rather than a silent unauthenticated request.

Implements the "Forward auth.token / custom headers to uplinks" item under Uplinks & caching in the pnpr-verdaccio-parity tracking issue.

Ref: #11973

Summary by CodeRabbit

  • New Features

    • Per-uplink custom HTTP headers are supported and forwarded to upstream requests; auth headers are derived from configured auth settings and redacted in diagnostics.
  • Bug Fixes

    • Invalid or unresolvable tokens and malformed header names/values now surface as configuration errors; unresolved YAML env refs are tolerated (treated as empty).
  • Tests

    • Added unit and integration tests for auth resolution, header precedence, validation, and forwarding.

@coderabbitai

coderabbitai Bot commented Jun 4, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 9688d9c3-83c7-48b8-9158-8cfc81958a7c

📥 Commits

Reviewing files that changed from the base of the PR and between bfd3a6b and 1578ef0.

📒 Files selected for processing (2)
  • pnpr/crates/pnpr/src/config.rs
  • pnpr/crates/pnpr/src/config/tests.rs
🚧 Files skipped from review as they are similar to previous changes (2)
  • pnpr/crates/pnpr/src/config/tests.rs
  • pnpr/crates/pnpr/src/config.rs
📜 Recent 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). (6)
  • GitHub Check: Lint and Test (windows-latest)
  • GitHub Check: Dylint
  • GitHub Check: Run benchmark on ubuntu-latest
  • GitHub Check: Analyze (javascript)
  • GitHub Check: Code Coverage
  • GitHub Check: Compile & Lint

📝 Walkthrough

Walkthrough

Per-uplink headers are resolved at config load (including derived Authorization headers) into runtime UplinkConfig entries. These headers are passed to Upstream instances and attached to all outgoing packument and tarball HTTP requests. Tests verify resolution, validation, and forwarding behavior.

Changes

Uplink Headers and Authentication

Layer / File(s) Summary
Uplink config types and auth resolution
pnpr/crates/pnpr/src/config.rs
Introduces runtime UplinkConfig with headers: HeaderMap alongside url. Adds private YAML structs (UplinkFile, UplinkAuthFile, TokenEnv) and new resolution functions that build Authorization headers from auth config, merge custom headers, and validate header names/values. Updates ConfigFile.uplinks parse target and Config::proxy default uplink construction.
Config loading integration
pnpr/crates/pnpr/src/config.rs
Config::from_yaml_str now iterates YAML uplinks, calls resolve_uplink::<SystemEnv> for each entry, and collects fully-resolved UplinkConfig instances into the runtime config.
Upstream request layer
pnpr/crates/pnpr/src/upstream.rs
Upstream struct gains headers: HeaderMap field. Constructor signature changed to accept headers: HeaderMap alongside the base URL. Both fetch_tarball_response and internal fetch helper now clone and attach self.headers to outgoing reqwest requests; Debug output redacts header values.
Server upstream construction
pnpr/crates/pnpr/src/server.rs
router_with_auth passes both uplink.url and uplink.headers when constructing Upstream instances, ensuring per-uplink headers are available for request forwarding.
Config validation and auth resolution tests
pnpr/crates/pnpr/src/config/tests.rs
Adds FakeEnv test helper for deterministic env var resolution. Tests cover Bearer/Basic token formatting, token_env resolution (flag and named var modes), literal token precedence, custom header forwarding and override semantics, and validation failures for missing/invalid tokens and malformed headers. Includes YAML parsing integration tests confirming unresolved ${VAR} references are tolerated.
Upstream request header forwarding tests
pnpr/crates/pnpr/src/upstream/tests.rs
Async integration tests using mockito verify fetch_packument and fetch_tarball_response forward configured Authorization and custom headers, plus a case confirming no Authorization is sent when headers map is empty.
Server end-to-end integration test
pnpr/crates/pnpr/tests/server.rs
New async test configures per-uplink authorization and x-org headers on the npmjs uplink, mocks the upstream to require both headers, sends a request to the router, and verifies HTTP 200 response and upstream mock invocation.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested reviewers

  • zkochan

Poem

🐰 Hopping through configs with cheer,
I stitch headers far and near,
Tokens resolved, redacted delight,
Upstreams forward them just right,
A rabbit's hop makes HTTP clear.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: forwarding uplink authentication tokens and custom headers to upstream registries.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/pnpr-uplink-auth-headers

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov-commenter

codecov-commenter commented Jun 4, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 79.74684% with 16 lines in your changes missing coverage. Please review.
✅ Project coverage is 87.86%. Comparing base (70554b8) to head (bfd3a6b).

Files with missing lines Patch % Lines
pnpr/crates/pnpr/src/config.rs 86.15% 9 Missing ⚠️
pnpr/crates/pnpr/src/upstream.rs 36.36% 7 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #12186      +/-   ##
==========================================
- Coverage   87.88%   87.86%   -0.03%     
==========================================
  Files         278      278              
  Lines       32075    32150      +75     
==========================================
+ Hits        28188    28247      +59     
- Misses       3887     3903      +16     

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

@github-actions

github-actions Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Integrated-Benchmark Report (Linux)

Each scenario has pacquet rows (direct install) and pnpr rows (the same client through the pnpr install accelerator), so pnpr@HEAD vs pacquet@HEAD is the pnpr-vs-direct ratio. Cold-store scenarios wipe the client store between runs (warm server); hot-store scenarios keep it warm. The pacquet@HEAD rows feed the pacquet Bencher testbed; the pnpr@HEAD rows feed the pnpr testbed.

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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 4.820 ± 0.069 4.749 4.940 2.35 ± 0.07
pacquet@main 4.835 ± 0.077 4.792 5.049 2.35 ± 0.07
pnpr@HEAD 2.071 ± 0.040 2.014 2.129 1.01 ± 0.03
pnpr@main 2.053 ± 0.049 1.992 2.151 1.00
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 4.819984128139998,
      "stddev": 0.0694768617182024,
      "median": 4.78245187744,
      "user": 2.42573452,
      "system": 3.7067731999999998,
      "min": 4.7487503239399995,
      "max": 4.93963303394,
      "times": [
        4.7801994389399995,
        4.77418342794,
        4.892154726939999,
        4.93963303394,
        4.776576599939999,
        4.7487503239399995,
        4.9095303569399995,
        4.78470431594,
        4.83555864194,
        4.758550414939999
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 4.83541919354,
      "stddev": 0.07666639036083331,
      "median": 4.80715011644,
      "user": 2.45406852,
      "system": 3.735816,
      "min": 4.79176911094,
      "max": 5.04896937794,
      "times": [
        4.81013444494,
        4.84148081394,
        4.79176911094,
        4.81621628094,
        4.80416578794,
        4.80408459394,
        4.80215145594,
        4.83558344494,
        4.79963662394,
        5.04896937794
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 2.0714506099400003,
      "stddev": 0.039620115749640854,
      "median": 2.07645522494,
      "user": 2.6401649199999997,
      "system": 3.3633406999999997,
      "min": 2.0144015039400003,
      "max": 2.12919066194,
      "times": [
        2.08515628394,
        2.0710149759400003,
        2.0144015039400003,
        2.01607208794,
        2.06316235494,
        2.03869479294,
        2.12230001394,
        2.09261794994,
        2.12919066194,
        2.08189547394
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 2.0533197448400005,
      "stddev": 0.049216659656360476,
      "median": 2.04072079544,
      "user": 2.64669272,
      "system": 3.3232937999999996,
      "min": 1.9918364909400001,
      "max": 2.15138237894,
      "times": [
        2.08287819894,
        2.10384694594,
        1.99558326294,
        1.9918364909400001,
        2.0289496059400003,
        2.15138237894,
        2.0319093499400003,
        2.04874789494,
        2.06536962394,
        2.03269369594
      ]
    }
  ]
}

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

Command Mean [ms] Min [ms] Max [ms] Relative
pacquet@HEAD 670.7 ± 25.3 651.3 738.3 1.00
pacquet@main 680.5 ± 48.1 642.2 801.5 1.01 ± 0.08
pnpr@HEAD 671.1 ± 61.8 641.0 839.8 1.00 ± 0.10
pnpr@main 726.5 ± 117.5 646.1 1041.1 1.08 ± 0.18
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 0.6707270804200001,
      "stddev": 0.025333491619108583,
      "median": 0.6632321299200001,
      "user": 0.350474,
      "system": 1.3277922,
      "min": 0.65131128392,
      "max": 0.7383255709200001,
      "times": [
        0.7383255709200001,
        0.67204041692,
        0.65131128392,
        0.6597661329200001,
        0.6797279029200001,
        0.6666981269200001,
        0.6531553149200001,
        0.6692352379200001,
        0.6590959769200001,
        0.6579148399200001
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 0.68052255362,
      "stddev": 0.0480617003007627,
      "median": 0.6602542879200001,
      "user": 0.35824789999999995,
      "system": 1.314979,
      "min": 0.6422403149200001,
      "max": 0.8015059669200001,
      "times": [
        0.7229996509200001,
        0.6549638969200001,
        0.6560423649200001,
        0.6795337049200001,
        0.65536035692,
        0.66446621092,
        0.65416160692,
        0.6422403149200001,
        0.8015059669200001,
        0.6739514619200001
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.6711259326200001,
      "stddev": 0.06177598330709484,
      "median": 0.6472660409200002,
      "user": 0.34711479999999995,
      "system": 1.3231514,
      "min": 0.6410300049200001,
      "max": 0.8398086239200001,
      "times": [
        0.7003669679200001,
        0.6515403659200001,
        0.6444717819200001,
        0.64480841292,
        0.6410300049200001,
        0.6490661889200001,
        0.6414540419200001,
        0.6454658929200001,
        0.8398086239200001,
        0.6532470449200001
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 0.7265370294200001,
      "stddev": 0.11750334867137108,
      "median": 0.6970297724200001,
      "user": 0.36211599999999994,
      "system": 1.3034617999999998,
      "min": 0.6461489009200001,
      "max": 1.04106185192,
      "times": [
        1.04106185192,
        0.7304169009200001,
        0.6461489009200001,
        0.6883353739200001,
        0.7275053959200001,
        0.6494829359200001,
        0.7057241709200001,
        0.7627501089200001,
        0.6614146629200001,
        0.65252999192
      ]
    }
  ]
}

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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 2.179 ± 0.023 2.126 2.205 1.02 ± 0.02
pacquet@main 2.142 ± 0.034 2.051 2.173 1.00
pnpr@HEAD 2.155 ± 0.024 2.105 2.184 1.01 ± 0.02
pnpr@main 2.147 ± 0.019 2.114 2.172 1.00 ± 0.02
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 2.1792807801199996,
      "stddev": 0.02264184090634787,
      "median": 2.1799681565199998,
      "user": 3.4725507999999996,
      "system": 3.03577962,
      "min": 2.1262901365199998,
      "max": 2.20481859552,
      "times": [
        2.1713445445199997,
        2.20207045752,
        2.20481859552,
        2.1262901365199998,
        2.1822266565199997,
        2.17770965652,
        2.18473453352,
        2.1991545545199997,
        2.16798478852,
        2.17647387752
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 2.1421200525199997,
      "stddev": 0.0337778336490116,
      "median": 2.1514044120199998,
      "user": 3.4448288999999996,
      "system": 3.02675122,
      "min": 2.0512948455199997,
      "max": 2.17253240452,
      "times": [
        2.15427905552,
        2.1564204825199997,
        2.0512948455199997,
        2.13153170552,
        2.14198646852,
        2.17253240452,
        2.14852976852,
        2.14765140852,
        2.16183588752,
        2.15513849852
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 2.15542986012,
      "stddev": 0.02402393231956696,
      "median": 2.1621033915199996,
      "user": 3.4290115,
      "system": 3.0185916199999996,
      "min": 2.10527298052,
      "max": 2.18352303452,
      "times": [
        2.15958173952,
        2.1675902745199997,
        2.10527298052,
        2.18352303452,
        2.13706832052,
        2.1311990065199997,
        2.1618306335199997,
        2.17994264152,
        2.1659138205199997,
        2.16237614952
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 2.14710630952,
      "stddev": 0.019273215817974,
      "median": 2.1503454925199996,
      "user": 3.4501526999999994,
      "system": 2.9898255199999997,
      "min": 2.11384933552,
      "max": 2.17248019752,
      "times": [
        2.1427931555199997,
        2.16509857552,
        2.15861479952,
        2.13407959852,
        2.15789782952,
        2.13723747252,
        2.16349833052,
        2.12551380052,
        2.17248019752,
        2.11384933552
      ]
    }
  ]
}

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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 1.259 ± 0.025 1.228 1.307 1.00
pacquet@main 1.287 ± 0.063 1.233 1.457 1.02 ± 0.05
pnpr@HEAD 1.289 ± 0.081 1.238 1.502 1.02 ± 0.07
pnpr@main 1.295 ± 0.074 1.250 1.502 1.03 ± 0.06
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 1.2585497698800001,
      "stddev": 0.025440647183153076,
      "median": 1.25446215888,
      "user": 1.35511618,
      "system": 1.70495302,
      "min": 1.2279380848800001,
      "max": 1.3071298638800002,
      "times": [
        1.24843976988,
        1.23010055888,
        1.2377447678800002,
        1.24973075088,
        1.28119763688,
        1.26128809288,
        1.2279380848800001,
        1.3071298638800002,
        1.28273460588,
        1.25919356688
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 1.2870737868800002,
      "stddev": 0.06300847152569453,
      "median": 1.27665173238,
      "user": 1.36295718,
      "system": 1.6955909200000001,
      "min": 1.23252469888,
      "max": 1.4570757098800002,
      "times": [
        1.23252469888,
        1.2498624398800002,
        1.2822531298800002,
        1.27860773488,
        1.29067107188,
        1.4570757098800002,
        1.2746957298800001,
        1.29227506488,
        1.26859434988,
        1.24417793888
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 1.28871890528,
      "stddev": 0.08130952502575242,
      "median": 1.2556119688800003,
      "user": 1.37922428,
      "system": 1.67958392,
      "min": 1.2383283468800002,
      "max": 1.5021022668800001,
      "times": [
        1.2588852518800002,
        1.25233868588,
        1.2489067098800002,
        1.2383283468800002,
        1.27113966088,
        1.5021022668800001,
        1.34830064388,
        1.2468621188800002,
        1.2462166328800002,
        1.27410873488
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 1.2954376841800002,
      "stddev": 0.07384121328317046,
      "median": 1.27474809388,
      "user": 1.39985718,
      "system": 1.67870462,
      "min": 1.25033875588,
      "max": 1.50242582888,
      "times": [
        1.26424896388,
        1.27395003588,
        1.26298313188,
        1.27827207488,
        1.25033875588,
        1.50242582888,
        1.29750634588,
        1.26618632388,
        1.27554615188,
        1.28291922888
      ]
    }
  ]
}

@github-actions

github-actions Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

🐰 Bencher Report

Branchpr/12186
Testbedpacquet

🚨 1 Alert

BenchmarkMeasure
Units
ViewBenchmark Result
(Result Δ%)
Upper Boundary
(Limit %)
isolated-linker.fresh-restore.cold-cache.cold-storeLatency
seconds (s)
📈 plot
🚷 threshold
🚨 alert (🔔)
4.82 s
(+49.67%)Baseline: 3.22 s
3.86 s
(124.72%)

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
2,179.28 ms
(-4.07%)Baseline: 2,271.66 ms
2,725.99 ms
(79.94%)
isolated-linker.fresh-install.hot-cache.hot-store📈 view plot
🚷 view threshold
1,258.55 ms
(-10.46%)Baseline: 1,405.61 ms
1,686.73 ms
(74.61%)
isolated-linker.fresh-restore.cold-cache.cold-store📈 view plot
🚷 view threshold
🚨 view alert (🔔)
4,819.98 ms
(+49.67%)Baseline: 3,220.45 ms
3,864.54 ms
(124.72%)

isolated-linker.fresh-restore.hot-cache.hot-store📈 view plot
🚷 view threshold
670.73 ms
(-1.60%)Baseline: 681.61 ms
817.93 ms
(82.00%)
🐰 View full continuous benchmarking report in Bencher

@juanpicado juanpicado force-pushed the feat/pnpr-uplink-auth-headers branch from 3693f0e to f674927 Compare June 5, 2026 06:18
@juanpicado juanpicado marked this pull request as ready for review June 5, 2026 06:53
@juanpicado juanpicado requested a review from zkochan as a code owner June 5, 2026 06:53
Copilot AI review requested due to automatic review settings June 5, 2026 06:53
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Review Summary by Qodo

Forward uplink auth tokens and custom headers to upstream registries

✨ Enhancement

Grey Divider

Walkthroughs

Description
• Forward uplink Authorization header from auth: block to upstream requests
• Support bearer/basic auth types with token resolution from config or env vars
• Merge custom headers: map with auth-derived headers, with custom overrides
• Validate auth tokens and header names/values at config load time, fail fast on errors
• Add comprehensive test coverage for auth resolution and header forwarding
Diagram
flowchart LR
  A["YAML Config<br/>auth + headers"] --> B["resolve_uplink<br/>at config load"]
  B --> C["HeaderMap<br/>auth-derived +<br/>custom headers"]
  C --> D["Upstream<br/>attaches to<br/>every request"]
  D --> E["Private upstreams<br/>receive credentials"]

Loading

Grey Divider

File Changes

1. pnpr/crates/pnpr/src/config.rs ✨ Enhancement +147/-8

Add auth/header resolution to uplink configuration

• Refactored UplinkConfig to include resolved HeaderMap with auth and custom headers
• Introduced UplinkFile struct for YAML deserialization shape, separate from runtime config
• Added UplinkAuthFile, UplinkAuthType, and TokenEnv types to model verdaccio-shaped auth
 blocks
• Implemented resolve_uplink() function to resolve auth tokens and validate headers at config load
• Implemented resolve_uplink_token() helper to prioritize explicit tokens over env var references
• Updated Config::proxy() to initialize uplinks with empty HeaderMap
• Modified config loading to resolve all uplinks through resolve_uplink::()

pnpr/crates/pnpr/src/config.rs


2. pnpr/crates/pnpr/src/config/tests.rs 🧪 Tests +199/-3

Comprehensive test coverage for uplink auth resolution

• Added FakeEnv test helper to mock environment variable resolution
• Added 13 new test cases covering bearer/basic token types, token_env resolution, and header
 validation
• Tests verify literal tokens override env vars, custom headers override auth-derived ones
• Tests confirm config errors for missing tokens, invalid header names/values, and control
 characters
• Added integration test verifying YAML config with auth/headers resolves correctly
• Added test confirming unresolved env var references are tolerated (not errors)

pnpr/crates/pnpr/src/config/tests.rs


3. pnpr/crates/pnpr/src/upstream.rs ✨ Enhancement +8/-3

Attach resolved headers to upstream HTTP requests

• Added headers: HeaderMap field to Upstream struct for per-uplink request headers
• Updated Upstream::new() constructor to accept and store HeaderMap parameter
• Modified fetch_tarball_response() to attach headers to tarball requests via .headers()
• Modified fetch() to attach headers to packument requests via .headers()

pnpr/crates/pnpr/src/upstream.rs


View more (3)
4. pnpr/crates/pnpr/src/upstream/tests.rs 🧪 Tests +78/-1

Test header forwarding in upstream requests

• Added auth_and_custom_headers() helper to build test header maps with bearer auth and custom
 headers
• Added async test verifying packument fetch forwards configured headers to upstream
• Added async test verifying tarball response fetch forwards configured headers to upstream
• Added async test confirming no authorization header sent when headers map is empty

pnpr/crates/pnpr/src/upstream/tests.rs


5. pnpr/crates/pnpr/tests/server.rs 🧪 Tests +27/-0

Integration test for end-to-end header forwarding

• Added integration test uplink_auth_and_custom_headers_are_forwarded_upstream() verifying
 end-to-end header forwarding
• Test manually inserts auth and custom headers into uplink config and verifies they reach the mock
 upstream
• Uses mockito header matching to confirm both Authorization and custom headers are present in
 requests

pnpr/crates/pnpr/tests/server.rs


6. pnpr/crates/pnpr/src/server.rs Additional files +3/-1

...

pnpr/crates/pnpr/src/server.rs


Grey Divider

Qodo Logo

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR adds verdaccio-parity support for forwarding per-uplink authentication and operator-specified custom headers on all upstream packument and tarball fetches performed by @pnpm/pnpr, enabling proxying of upstream registries that require authorization (e.g. CodeArtifact, GitHub Packages).

Changes:

  • Extend uplink configuration to resolve auth (bearer|basic, token/token_env) and headers into a per-uplink HeaderMap at config-load time, failing fast on invalid/missing values.
  • Attach resolved per-uplink headers to every upstream packument and tarball request.
  • Add unit + integration tests verifying header forwarding and config resolution/error cases.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
pnpr/crates/pnpr/src/config.rs Adds uplink auth/headers parsing + resolution into runtime UplinkConfig.headers.
pnpr/crates/pnpr/src/config/tests.rs Adds extensive tests for uplink auth/header resolution, precedence, and invalid configs.
pnpr/crates/pnpr/src/upstream.rs Stores per-uplink headers on Upstream and attaches them to packument/tarball requests.
pnpr/crates/pnpr/src/upstream/tests.rs Verifies Upstream forwards configured headers (and omits auth when empty).
pnpr/crates/pnpr/src/server.rs Plumbs resolved uplink headers into Upstream::new(...) when building app state.
pnpr/crates/pnpr/tests/server.rs Adds an integration-style test proving forwarded headers reach the upstream server.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread pnpr/crates/pnpr/src/config.rs Outdated
Comment thread pnpr/crates/pnpr/src/upstream.rs Outdated
juanpicado and others added 4 commits June 5, 2026 09:17
pnpr now attaches a per-uplink `Authorization` header (derived from the
verdaccio-shaped `auth:` block) plus any operator-supplied custom
`headers:` on every packument and tarball request it makes to an
upstream registry. This unblocks proxying private upstreams
(CodeArtifact, GitHub Packages, authed npm Enterprise, private
verdaccio) that previously returned 401/403.

`auth` supports `type: bearer | basic`, an inline `token`, and
verdaccio's `token_env` (`true` -> `NPM_TOKEN`, or a named var); an
inline `token` takes priority. A custom `headers.Authorization`
overrides the auth-derived one, matching verdaccio's merge order.
Tokens/headers resolve once at config load through the existing
`EnvVar` seam, so a missing token or invalid header value fails fast as
an `InvalidConfig` error rather than a silent unauthenticated request.

Implements the "Forward auth.token / custom headers to uplinks" item
under Uplinks & caching in the pnpr-verdaccio-parity tracking issue.

Ref: #11973

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add tests for the previously-uncovered branches in resolve_uplink and
from_yaml_str flagged by Codecov on the uplink-auth patch:

- token_env false resolves no token (config error)
- an auth token that is not a valid header value (config error)
- an invalid custom header name (config error)
- an invalid custom header value (config error)
- an unresolved env-var reference is tolerated, not an error

Brings config.rs from 92.05% to 96.69% line coverage; the patch's nine
missing lines are now fully covered.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CI's stricter dylint/perfectionist pass and `cargo doc` (which plain
clippy does not run) flagged the uplink-auth changes:

- collapse the split `reqwest::` and `crate::` imports to one `use` per
  crate root (perfectionist::import-granularity = crate)
- add the trailing comma to the multi-line `format!` in resolve_uplink
  (perfectionist::macro-trailing-comma)
- drop the intra-doc links from the public `UplinkConfig` docs to the
  private `UplinkFile`/`resolve_uplink` items, which broke
  `cargo doc --document-private-items` under -D warnings

No behavior change. Verified locally with `cargo dylint --all`,
`cargo doc --document-private-items`, clippy, and the config tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
UplinkConfig and Upstream both hold a resolved HeaderMap that can carry
an Authorization credential (or a secret in a custom header). The derived
Debug printed those values verbatim, so a debug log or span could leak
them. Replace the derives with hand-written impls that route the map
through a RedactedHeaders wrapper, which lists header names with every
value rendered as <redacted>.
@zkochan zkochan force-pushed the feat/pnpr-uplink-auth-headers branch from 636dcf7 to bfd3a6b Compare June 5, 2026 07:31
Copilot AI review requested due to automatic review settings June 5, 2026 07:40

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@zkochan zkochan merged commit ae212c8 into main Jun 5, 2026
25 of 26 checks passed
@zkochan zkochan deleted the feat/pnpr-uplink-auth-headers branch June 5, 2026 07:54
KSXGitHub pushed a commit that referenced this pull request Jun 5, 2026
Resolve conflicts in pnpr auth.rs and error.rs by taking main's new
networked-SQLite auth backend code (#12186, #12199/#12206); the
clippy::pedantic compliance fixes are re-applied to the merged tree in
a follow-up commit.
KSXGitHub pushed a commit that referenced this pull request Jun 5, 2026
Re-apply pedantic compliance to the networked-SQLite auth backend that
landed on main (#12186, #12199/#12206): doc-comment backticks, #[must_use]
on constructors and status_code, i64::from over `as`, map_or, and a
method-reference closure.
zkochan pushed a commit that referenced this pull request Jun 10, 2026
* chore: enable clippy::pedantic lint group for pacquet workspace

* style(pacquet): comply with clippy::pedantic

Apply clippy's machine-applicable pedantic fixes across the workspace
(inlined format args, removed needless borrows/closures, added
must_use, etc.), fix a few doc-comment backtick nits, and drop
pointless #[inline(always)] on trivial accessors.

Opt specific pedantic lints back out in [workspace.lints.clippy] with
documented justifications, grouped into false positives, library-API
hygiene that doesn't fit an internal CLI, suggestions that conflict
with the cardinal rule of porting pnpm 1:1, and opinionated style.

* style: taplo-format Cargo.toml lint table

* style(pnpr): comply with clippy::pedantic in merged auth backend code

Re-apply pedantic compliance to the networked-SQLite auth backend that
landed on main (#12186, #12199/#12206): doc-comment backticks, #[must_use]
on constructors and status_code, i64::from over `as`, map_or, and a
method-reference closure.

* docs(clippy): trim and inline the pedantic allow-list comments

* docs(clippy): note perfectionist supersedes many_single_char_names

* docs(clippy): note pnpm-mirroring rationale on structure/naming lints

* docs(clippy): mark unused_async as deferred pending audit

* style: enable clippy::match_wildcard_for_single_variants

* refactor: enable clippy::unused_self

Convert two self-less private methods (overrides pick_most_specific,
tarball head_only_result) to associated functions.

* refactor: enable clippy::ref_option

Widen engine_json to Option<&str>; #[expect] the two serde
serialize_with helpers, which serde must call as f(&field, ser).

* perf: enable clippy::trivially_copy_pass_by_ref

Pass the 1-byte Copy types NodeLinker and FilterWorkspaceProjectsOptions
by value; #[expect] the serde skip_serializing_if helper is_false.

* perf: enable clippy::assigning_clones

Use clone_from for seven field assignments to reuse allocations.

* style: enable clippy::manual_let_else

Convert 27 match/if-let guards to let-else; preserve the non-UTF-8
skip rationale comment in the directory walker.

* style: enable clippy::default_trait_access

Name the concrete type on Default::default() call sites; #[expect] two
struct-literal test fixtures where naming each field type would force
~20 imports.

* refactor: enable clippy::format_push_string

Replace push_str(&format!(...)) with write!/writeln! into the target
String (local 'use std::fmt::Write as _'); writeln! preserves the
exact LF/CRLF shell-shim output.

* refactor: enable clippy::needless_pass_by_value

Take by reference where the argument is only read (incl. dropping
some redundant clones in resolve_peers' recursion). Where converting
would cascade badly, #[expect] with a reason: functions that
destructure/consume the arg (build_resolve_result, PrefetchingResolver,
S3Store::new), the by-value `impl IntoIterator + Clone` in
build_direct_deps_by_importer, and the serde/test helpers whose owned
fixtures keep call sites clean.

* fix(perfectionist): satisfy dylint after format_push_string changes

Add trailing commas to the multi-line writeln! shell-shim templates
(macro_trailing_comma) and merge the new `fmt::Write as _` imports into
each file's existing `use std::{...}` block (import_granularity).

* docs(clippy): explain missing_errors_doc suppression; mark missing_panics_doc deferred

* fix(perfectionist): collapse fmt::{self, Write as _} in work_env imports

The format_push_string Write import landed as a sibling fmt:: path next
to the existing fmt import; merge them so import_granularity passes.

* style: enable clippy::return_self_not_must_use

Add #[must_use] to the WorkspaceTreeCtx builder methods, matching the
#[must_use] already on the parallel TreeCtx builders.

* perf: enable clippy::large_stack_arrays

Heap-allocate the 64 KiB read buffer in verify_file_integrity with a Vec
instead of placing it on the stack.

* chore(clippy): enable clippy::nursery group

Enable the nursery lint group on the pacquet/pnpr workspace and bring the
code into compliance.

Fixed in code:
- iter_on_single_items: [x].into_iter()/.iter() -> std::iter::once
- equatable_if_let: pattern match -> equality check (the install_accelerator
  rewrite wraps in a multi-line matches!, which gets a trailing comma for
  perfectionist::macro_trailing_comma)
- needless_pass_by_ref_mut: load_pending_row/apply_write_msg take &StoreIndex

Opted back out in Cargo.toml, each with a documented justification: use_self,
too_long_first_doc_paragraph, missing_const_for_fn, option_if_let_else,
significant_drop_tightening, redundant_pub_crate, derive_partial_eq_without_eq,
branches_sharing_code, useless_let_if_seq, single_option_map, iter_with_drain,
literal_string_with_formatting_args, collection_is_never_read.

Dropped the now-redundant individual nursery warns (needless_collect,
or_fun_call, redundant_clone) the group now covers, plus the default-on
unnecessary_lazy_evaluations. Kept clone_on_ref_ptr and if_then_some_else_none
(restriction lints not enabled by any group).

* style: bring merged main code into clippy pedantic compliance

The 17 commits merged from main predate this branch's pedantic/nursery
lint config, so their new code tripped pedantic lints. Apply the
machine-applicable fixes (uninlined_format_args, if_not_else,
elidable_lifetime_names, must_use_candidate, single_match_else,
map_unwrap_or, default_trait_access, assigning_clones, doc_markdown, ...)
and re-add the documented #[expect(needless_pass_by_value)] on
S3Store::new that this branch had carried on the now-replaced file.

* style: bring merged main code into clippy pedantic compliance

The 28 commits merged from main predate this branch's lint config, so
their new code tripped pedantic lints. Apply the machine-applicable fixes
(uninlined_format_args, manual_let_else, needless_raw_string_hashes,
redundant_closure_for_method_calls, map_unwrap_or, elidable_lifetime_names,
doc_markdown, ...) plus a few by hand:
- derive Copy on LinkSlotsParallel (all fields are Copy/refs) to clear
  needless_pass_by_value without a signature change
- deduplicate_all takes &[Vec<DepPath>] (it only borrows the duplicates)
- pick_most_specific becomes an associated fn (it never used self)
- default_trait_access -> concrete types; assigning_clones -> clone_from;
  format_push_string -> write!
- #[expect] with reasons where a fix would churn main's feature code:
  needless_pass_by_value on the recursive resolve_node and a test helper,
  and float_cmp on two deterministic-fixture assertions

* style: enable clippy::allow_attributes and allow_attributes_without_reason

Both are restriction lints (not implied by any group), enabled alongside
the existing clone_on_ref_ptr / if_then_some_else_none. Convert every
#[allow(...)] (including one nested in cfg_attr) to #[expect(...)]; all
already carried a reason, so allow_attributes_without_reason is satisfied.

Drop two now-redundant suppressions surfaced by the conversion: a
duplicated #[expect(too_many_arguments)] on fetch_and_extract_zip_once
(a prior merge left both an allow and an expect), and the
#[expect(dead_code)] on MissingPeerInfo's fields (the #[derive(Debug,
Clone)] already reads them, so dead_code never fired).

clone_on_ref_ptr was already enabled. mod_module_files is intentionally
NOT enabled: it mandates mod.rs, the opposite of the flat module.rs
pattern this project requires (CODE_STYLE_GUIDE.md, enforced by
perfectionist::flat_module_pattern).

* style: enable clippy::mod_module_files to enforce the flat module layout

mod_module_files bans mod.rs files, enforcing the flat module.rs pattern
this project already uses (0 mod.rs in the tree, so no violations). Update
CODE_STYLE_GUIDE.md to cite it as the enforcer; perfectionist's
flat_module_pattern is being retired in favor of this Clippy rule.

* fix(perfectionist): trailing comma on wrapped assert_eq! in workspace_yaml tests

The default_trait_access fix lengthened the assert_eq! so fmt wrapped it
to multi-line, which perfectionist::macro_trailing_comma requires to end
with a trailing comma.

* fix(fs): use cfg_attr expect instead of allow for Windows-unused mode args

With clippy::allow_attributes enabled, the #[cfg_attr(windows, allow(unused))]
on make_file_executable and the ensure_file/write_atomic mode params fails
Windows CI. Switch to #[cfg_attr(windows, expect(unused, reason = ...))];
on Windows the lint fires (Unix mode unused there) so the expectation is
fulfilled, and the attribute stays inert on Unix.

* fix(fs): drop the Windows unused suppression on ensure_file's mode arg

ensure_file forwards mode to verify_or_rewrite unconditionally, so it is
used on Windows too; the #[cfg_attr(windows, expect(unused))] was therefore
unfulfilled and failed Windows CI under -D warnings. write_atomic and
make_file_executable keep their expect — they use mode/file only under
#[cfg(unix)], so the lint fires (and the expectation holds) on Windows.

* chore(git): revert "fix(fs): drop the Windows unused suppression on ensure_file's mode arg"

This reverts commit 1d617c3.

* chore(git): revert "fix(fs): use cfg_attr expect instead of allow for Windows-unused mode args"

This reverts commit 155e4a3.

* chore(git): revert "style: enable clippy::allow_attributes and allow_attributes_without_reason"

This reverts commit a47d792.

* style: bring merged main code into clippy compliance + fix merge mismatch

- Add & at the two run_postinstall_hooks / run_project_lifecycle_scripts
  call sites: this branch widened lifecycle.rs to take &RunPostinstallHooks,
  but main's by-value call sites came in via the conflict resolution.
- pedantic fixes on main's new code: must_use_candidate, unnested_or_patterns,
  manual_let_else, default_trait_access, iter_on_single_items, and
  trivially_copy_pass_by_ref (map_node_linker takes NodeLinker by value).

---------

Co-authored-by: Claude <noreply@anthropic.com>
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.

4 participants