Background
#490 landed the top-level TLS keys (ca, cafile, cert, key, strict-ssl, local-address). Pnpm v11 also supports per-registry TLS overrides keyed on a nerf-darted URI prefix, the same shape as the existing per-registry auth handling (//host:_authToken=). Pacquet doesn't honor these yet — a multi-registry setup where a private Verdaccio mirror needs its own self-signed CA still falls through to the top-level ca= (or fails strict-ssl entirely).
What pnpm does
getNetworkConfigs.ts:94-113 parses :cert, :key, :ca plus the *file variants :cafile, :certfile, :keyfile off any //host[:port]/path/: prefix and stores them in configByUri[<nerf-dart>].tls = { cert, key, ca }.
Lookup at request time uses pickSettingByUrl in dispatcher.ts:338-375 with the 5-step fallback chain:
- exact URL match
- nerf-dart of the request URL (e.g.
//registry.npmjs.org/)
- URL without port
- progressively shorter nerf-dart path prefixes
- retry without port
Per-registry TLS overrides the top-level opts.ca/cert/key via spread: { ...opts, ...sslConfig } (dispatcher.ts:143, 264). Scoped wins over top-level when scoped fields are set.
Inline ca=\\n… per-registry values get \\n → newline expansion (getNetworkConfigs.ts:38-39) because INI is single-line. Top-level ca= doesn't get this treatment.
What to do
- Extend
NpmrcAuth::creds_by_uri (or add a sibling tls_by_uri: HashMap<String, RawTls>) to capture scoped TLS values during parse. Recognize the same six suffixes pnpm does: :ca, :cafile, :cert, :certfile, :key, :keyfile.
- Expand
\\n → newline on scoped values before storing — matches pnpm's per-registry-only normalization.
- Build a per-URI TLS map at apply time (same lifecycle as
AuthHeaders), keyed by nerf-darted URI. Reuse the existing nerf_dart helper in pacquet-network.
- Decide where the per-URI map lives — most likely
pacquet_network::PerRegistryTls (next to AuthHeaders), since the lookup happens at request-build time and reqwest's Proxy::custom-style per-URL routing is in the network crate.
- Wiring into reqwest is the tricky bit. Reqwest's
Client::builder().add_root_certificate(...) / .identity(...) are global per-client, not per-target. Options:
- (a) Build N client variants — one per
(top_level, per_registry) pair — and dispatch in the install client. Adds complexity to ThrottledClient.
- (b) Use rustls's custom certificate verifier to route at request time. Requires switching the TLS backend.
- (c) Build one client per registry on demand. Wastes connections.
- Decide as part of this issue. Document the trade-offs.
- Port
pickSettingByUrl's 5-step lookup faithfully — bug for bug with pnpm.
Tests
- Parse arms for each scoped suffix in
crates/config/src/npmrc_auth/tests.rs.
\\n expansion on scoped ca=.
- 5-step
pickSettingByUrl lookup precedence (exact > nerf-dart > no-port > shorter prefix > retry).
- Scoped TLS overrides top-level when set.
- Per-registry-only —
//host:strict-ssl=… and //host:local-address=… are not in pnpm's allow-list and should fall through to default arms.
- Mockito integration: two mock registries, one with a self-signed cert under
strict-ssl: false for one host only.
Out of scope
- Switching reqwest's TLS backend to rustls. If we go with option (a) or (c) above this PR stays on native-tls.
- Per-registry
strict-ssl / local-address — not in pnpm's scoped allow-list.
References
Written by an agent (Claude Code, claude-opus-4-7).
Background
#490 landed the top-level TLS keys (
ca,cafile,cert,key,strict-ssl,local-address). Pnpm v11 also supports per-registry TLS overrides keyed on a nerf-darted URI prefix, the same shape as the existing per-registry auth handling (//host:_authToken=). Pacquet doesn't honor these yet — a multi-registry setup where a private Verdaccio mirror needs its own self-signed CA still falls through to the top-levelca=(or fails strict-ssl entirely).What pnpm does
getNetworkConfigs.ts:94-113parses:cert,:key,:caplus the*filevariants:cafile,:certfile,:keyfileoff any//host[:port]/path/:prefix and stores them inconfigByUri[<nerf-dart>].tls = { cert, key, ca }.Lookup at request time uses
pickSettingByUrlindispatcher.ts:338-375with the 5-step fallback chain://registry.npmjs.org/)Per-registry TLS overrides the top-level
opts.ca/cert/keyvia spread:{ ...opts, ...sslConfig }(dispatcher.ts:143, 264). Scoped wins over top-level when scoped fields are set.Inline
ca=\\n…per-registry values get\\n→ newline expansion (getNetworkConfigs.ts:38-39) because INI is single-line. Top-levelca=doesn't get this treatment.What to do
NpmrcAuth::creds_by_uri(or add a siblingtls_by_uri: HashMap<String, RawTls>) to capture scoped TLS values during parse. Recognize the same six suffixes pnpm does::ca,:cafile,:cert,:certfile,:key,:keyfile.\\n→ newline on scoped values before storing — matches pnpm's per-registry-only normalization.AuthHeaders), keyed by nerf-darted URI. Reuse the existingnerf_darthelper inpacquet-network.pacquet_network::PerRegistryTls(next toAuthHeaders), since the lookup happens at request-build time and reqwest'sProxy::custom-style per-URL routing is in the network crate.Client::builder().add_root_certificate(...)/.identity(...)are global per-client, not per-target. Options:(top_level, per_registry)pair — and dispatch in the install client. Adds complexity toThrottledClient.pickSettingByUrl's 5-step lookup faithfully — bug for bug with pnpm.Tests
crates/config/src/npmrc_auth/tests.rs.\\nexpansion on scopedca=.pickSettingByUrllookup precedence (exact > nerf-dart > no-port > shorter prefix > retry).//host:strict-ssl=…and//host:local-address=…are not in pnpm's allow-list and should fall through to default arms.strict-ssl: falsefor one host only.Out of scope
strict-ssl/local-address— not in pnpm's scoped allow-list.References
getNetworkConfigs.ts— parserdispatcher.ts:338-375—pickSettingByUrlWritten by an agent (Claude Code, claude-opus-4-7).