mirror of
https://github.com/pnpm/pnpm.git
synced 2026-06-01 12:41:16 -04:00
fix(config/reader): pin unscoped per-registry settings to their source's registry at load time (#11953)
* fix(config/reader): drop user-level default auth when workspace overrides registry
When a workspace `.npmrc` overrides `registry=` to a different value than the
user's `~/.npmrc` or `~/.config/pnpm/auth.ini` would have set, do not bind
unscoped/default credentials (`_authToken`, `_auth`, `username`/`_password`)
from the user-level config to the workspace-selected registry. The previous
behavior leaked user-trusted credentials to whatever registry an untrusted
workspace `.npmrc` pointed at. Reported by JUNYI LIU.
* chore(cspell): allow JUNYI in changeset and tests
* fix(config/reader): also defend when pnpm-workspace.yaml overrides registry
Move the rebind defense to after all config layers (CLI, env vars,
pnpm-workspace.yaml, .npmrc) have settled. Compare the final resolved
default registry against what the user-level config alone would produce,
and skip the check entirely if the user requested a registry via CLI/env
themselves.
* feat(config/reader): deprecate unscoped authentication credentials
Emit a per-file warning whenever an .npmrc or auth.ini contains an
unscoped auth value (_authToken, _auth, username, _password,
tokenHelper). URL-scoped tokens have been npm's recommended pattern
since npm@9, and unscoped credentials are slated for removal in a
future major. The warning fires independently of whether the rebind
defense rejects the credentials, so users see the deprecation even when
their setup happens to be safe today.
* refactor(config/reader): rescope unscoped credentials at load time instead of detecting rebinds post-merge
Each .npmrc / auth.ini / CLI source's unscoped credential keys
(_authToken, _auth, username, _password, tokenHelper) are rewritten to
their URL-scoped equivalent during load, using the same source's
registry= value (or the npmjs default if it declares none). A later
layer overriding registry= can no longer rebind a credential to its own
registry — the credential is already pinned to the URL its author
intended.
This removes the post-merge source-tracking defense and replaces it
with the simpler per-source normalization. Each rescope emits a
deprecation warning so users migrate to writing the URL-scoped form
directly.
* refactor(network/auth-header): drop empty-string default-registry slot
After load-time rescoping, no source can populate configByUri[''] —
every credential is either URL-scoped from the start or rewritten to
the URL-scoped form during the .npmrc / auth.ini / CLI parse. The
runtime fallback that re-keyed configByUri[''] onto the merged default
registry, and the publish-side fallback that read it, are both dead
code.
Removed:
- empty-string handling in getAuthHeadersFromCreds, including its
defaultRegistry parameter
- defaultRegistry parameter from createGetAuthHeaderByURI
- the corresponding dedicated unit test
- the configByUri['']?.creds fallback in publishPackedPkg.ts
- empty-key assertions in config/reader tests
Updated all ~16 call sites of createGetAuthHeaderByURI to drop the now
unused second argument.
* feat(config/reader): extend per-source rescoping to client TLS cert/key
The same trust-boundary issue that affected unscoped credentials applies
to client TLS settings: an unscoped cert=/key= would be presented to
whatever registry the merged config settles on, even if a later layer
(workspace .npmrc, pnpm-workspace.yaml, CLI flag) overrode it. The
existing rescope helper now also rewrites unscoped `cert` and `key`
to their URL-scoped form, pinning them to the registry their author
named in the same source.
`ca`/`cafile` are intentionally left unscoped: they're trust anchors,
not credentials, and corporate MITM-proxy setups depend on them
applying to every HTTPS request. The default-registry override can't
weaponize an unscoped CA — the attacker would need a cert signed by it.
`certfile`/`keyfile` (file-path variants) are not rescoped either:
`certfile` isn't read unscoped by pnpm today (asymmetric vs. `keyfile`
in NPM_AUTH_SETTINGS), and supporting only one of them would be
confusing. Users wanting the path form can write it URL-scoped
directly.
* chore(config/reader): remove dead unscoped `keyfile` allowlist entry
`keyfile` was listed in NPM_AUTH_SETTINGS so unscoped `keyfile=<path>`
passed the .npmrc filter and ended up in authConfig — but nothing in
the codebase ever read it from there. The dispatcher uses `opts.key`
(inline PEM) and `configByUri[host].tls.key` (URL-scoped path/inline
content), neither of which is populated from unscoped `keyfile=`.
`certfile` was already absent from the allowlist for the same reason,
so this also removes the asymmetry between the two file-path variants.
URL-scoped `//host/:certfile=...` and `//host/:keyfile=...` continue
to work via `tryParseSslKey` and are unaffected.
* test(network/auth-header): drop test for removed default-registry slot
This test exercised the configByUri[''] re-keying path that was
removed in the rescope-at-load refactor. With createGetAuthHeaderByURI
no longer accepting a defaultRegistry parameter and unscoped
credentials no longer reaching the merged config, the scenario the
test described is structurally unreachable.
* fix(config/reader): handle empty/invalid registry value in rescope
Two CI fixes:
1. When a source's `registry=` resolves to an empty string (e.g. an
unresolved `${ENV_VAR}` placeholder), `new URL(...)` inside
`nerfDart` throws. Guard the call with try/catch: drop the
unscoped per-registry keys (a bare token has nowhere safe to bind)
and emit a warning naming the offending source.
2. Update `.npmrc does not load pnpm settings` to expect the rescoped
form of unscoped `_authToken`/`username` in `authConfig` — they
now appear as `//registry.npmjs.org/:_authToken` etc. since the
test's .npmrc declares no `registry=` of its own.
* chore(cspell): allow "rescoping"
* test(installing/deps-installer): drop "legacy way" auth test
This test passed credentials via the configByUri[''] empty-string slot,
which the auth-header layer re-keyed to the merged default registry at
request time. That slot was removed in the rescope-at-load refactor —
credentials are now always URL-scoped before they reach configByUri,
so the empty-key entry is unreachable from any code path.
The scenario the test covered (basicAuth via username/password) is
already exercised by the existing "installing a package that need
authentication, using password" test using the URL-scoped form.
This commit is contained in:
13
.changeset/credential-rebind-defense.md
Normal file
13
.changeset/credential-rebind-defense.md
Normal file
@@ -0,0 +1,13 @@
|
||||
---
|
||||
"@pnpm/config.reader": patch
|
||||
"@pnpm/network.auth-header": major
|
||||
"pnpm": patch
|
||||
---
|
||||
|
||||
Fix a credential disclosure issue where an unscoped `_authToken` (or `_auth`, or `username` + `_password`, or `tokenHelper`) defined in one source — `~/.npmrc`, `~/.config/pnpm/auth.ini`, a workspace `.npmrc`, CLI flags, etc. — would be sent as an `Authorization` header to whichever registry a different (potentially untrusted) source named. The same fix extends to client TLS credentials (`cert`, `key`) so they aren't presented to a registry their author didn't choose.
|
||||
|
||||
pnpm now rewrites each unscoped per-registry setting (`_authToken`, `_auth`, `username`, `_password`, `tokenHelper`, `cert`, `key`) to its URL-scoped form at load time, using the `registry=` value declared in the same source (or the npmjs default registry if the source declares none). A later layer overriding `registry=` therefore cannot pull an unscoped credential along, because it is already pinned to the URL its author intended. `ca`/`cafile` are intentionally not rescoped — they're trust anchors, not credentials, and corporate MITM-proxy setups rely on them applying globally.
|
||||
|
||||
Every rescope emits a deprecation warning telling the user where the setting was pinned and how to write it directly. npm has rejected unscoped credentials outright since `npm@9`, and pnpm intends to remove support in a future major release. To target a specific registry, write the setting URL-scoped (e.g. `//registry.example.com/:_authToken=...` or `//registry.example.com/:cert=...`).
|
||||
|
||||
`@pnpm/network.auth-header`: removed the `defaultRegistry` parameter from `createGetAuthHeaderByURI` and `getAuthHeadersFromCreds`. Now that credentials are URL-scoped at load time, the merged `configByUri` never contains the empty-string "default registry" placeholder slot, so re-keying it onto the merged default registry is no longer needed.
|
||||
@@ -37,6 +37,7 @@
|
||||
"@pnpm/catalogs.types": "workspace:*",
|
||||
"@pnpm/config.env-replace": "catalog:",
|
||||
"@pnpm/config.matcher": "workspace:*",
|
||||
"@pnpm/config.nerf-dart": "catalog:",
|
||||
"@pnpm/constants": "workspace:*",
|
||||
"@pnpm/error": "workspace:*",
|
||||
"@pnpm/hooks.pnpmfile": "workspace:*",
|
||||
|
||||
@@ -38,7 +38,7 @@ import { isConfigFileKey } from './configFileKey.js'
|
||||
import { extractAndRemoveDependencyBuildOptions, hasDependencyBuildOptions } from './dependencyBuildOptions.js'
|
||||
import { getCacheDir, getConfigDir, getDataDir, getStateDir } from './dirs.js'
|
||||
import { parseEnvVars } from './env.js'
|
||||
import { getDefaultCreds, getNetworkConfigs } from './getNetworkConfigs.js'
|
||||
import { getNetworkConfigs } from './getNetworkConfigs.js'
|
||||
import { getOptionsFromPnpmSettings } from './getOptionsFromRootManifest.js'
|
||||
import { loadNpmrcConfig } from './loadNpmrcFiles.js'
|
||||
import { inheritDlxConfig, pickIniConfig } from './localConfig.js'
|
||||
@@ -321,11 +321,8 @@ export async function getConfig (opts: {
|
||||
...networkConfigs.registries,
|
||||
}
|
||||
pnpmConfig.registries = { ...registriesFromNpmrc }
|
||||
const defaultCreds = getDefaultCreds(pnpmConfig.authConfig)
|
||||
pnpmConfig.configByUri = {
|
||||
...networkConfigs.configByUri,
|
||||
...defaultCreds ? { '': { creds: defaultCreds } } : {},
|
||||
}
|
||||
pnpmConfig.configByUri = { ...networkConfigs.configByUri }
|
||||
|
||||
// tokenHelper must only come from user-level config (~/.npmrc or global auth.ini),
|
||||
// not project-level, to prevent project .npmrc from executing arbitrary commands.
|
||||
const userConfig = npmrcResult.userConfig as Record<string, string>
|
||||
|
||||
@@ -3,9 +3,12 @@ import os from 'node:os'
|
||||
import path from 'node:path'
|
||||
|
||||
import { envReplaceLossy } from '@pnpm/config.env-replace'
|
||||
import { nerfDart } from '@pnpm/config.nerf-dart'
|
||||
import normalizeRegistryUrl from 'normalize-registry-url'
|
||||
import { readIniFileSync } from 'read-ini-file'
|
||||
|
||||
import { isNpmrcReadableKey } from './localConfig.js'
|
||||
import { npmDefaults } from './npmDefaults.js'
|
||||
|
||||
export interface NpmrcConfigResult {
|
||||
/**
|
||||
@@ -69,6 +72,11 @@ export function loadNpmrcConfig (opts: LoadNpmrcConfigOpts): NpmrcConfigResult {
|
||||
env
|
||||
)
|
||||
|
||||
// Apply the same per-source rescope to CLI options so an unscoped
|
||||
// `--_authToken` follows the same trust rule as one written into an .npmrc.
|
||||
// We clone first to avoid mutating the caller's cliOptions object.
|
||||
const cliOptions = rescopeUnscopedCreds({ ...opts.cliOptions }, '<command line>', warnings)
|
||||
|
||||
// Read pnpm builtin rc + inline defaults
|
||||
const pnpmBuiltinConfig: Record<string, unknown> = {
|
||||
...readAndFilterNpmrc(
|
||||
@@ -83,7 +91,7 @@ export function loadNpmrcConfig (opts: LoadNpmrcConfigOpts): NpmrcConfigResult {
|
||||
// Handle cafile: expand to ca certs.
|
||||
// Priority: CLI > workspace > auth.ini > user > defaults
|
||||
loadCAFile([
|
||||
opts.cliOptions,
|
||||
cliOptions,
|
||||
workspaceNpmrc,
|
||||
pnpmAuthConfig,
|
||||
userConfig,
|
||||
@@ -93,7 +101,7 @@ export function loadNpmrcConfig (opts: LoadNpmrcConfigOpts): NpmrcConfigResult {
|
||||
// Merge all sources (lowest to highest priority):
|
||||
// builtin < defaults < user < auth.ini < workspace < CLI
|
||||
const mergedConfig: Record<string, unknown> = {}
|
||||
for (const source of [pnpmBuiltinConfig, opts.defaultOptions, userConfig, pnpmAuthConfig, workspaceNpmrc, opts.cliOptions]) {
|
||||
for (const source of [pnpmBuiltinConfig, opts.defaultOptions, userConfig, pnpmAuthConfig, workspaceNpmrc, cliOptions]) {
|
||||
for (const [key, value] of Object.entries(source)) {
|
||||
if (isNpmrcReadableKey(key)) {
|
||||
mergedConfig[key] = value
|
||||
@@ -108,7 +116,7 @@ export function loadNpmrcConfig (opts: LoadNpmrcConfigOpts): NpmrcConfigResult {
|
||||
...userConfig,
|
||||
...pnpmAuthConfig,
|
||||
...workspaceNpmrc,
|
||||
...opts.cliOptions,
|
||||
...cliOptions,
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -121,6 +129,35 @@ export function loadNpmrcConfig (opts: LoadNpmrcConfigOpts): NpmrcConfigResult {
|
||||
}
|
||||
}
|
||||
|
||||
// Per-registry rc keys that, when written without a `//host/` prefix, fall
|
||||
// through to whatever default registry the merged config settles on. We
|
||||
// rewrite each such key to its URL-scoped form at load time, pinning it to
|
||||
// the `registry=` value declared in the same source. A later layer can
|
||||
// still override the merged registry, but it cannot pull along a credential
|
||||
// or client certificate authored for a different host.
|
||||
//
|
||||
// Two groups:
|
||||
// * auth keys — `_authToken` etc. Pinned to prevent credential leaks. npm
|
||||
// rejects these unscoped since npm@9 (ERR_INVALID_AUTH); pnpm keeps them
|
||||
// working but warns so users migrate before a future major drops support.
|
||||
// * client certificate keys — `cert`/`key` (inline PEM). Pinned to prevent
|
||||
// a client certificate (and the identity it carries) being presented to
|
||||
// the wrong host. The `certfile`/`keyfile` path variants are not in
|
||||
// `NPM_AUTH_SETTINGS`, so unscoped forms never reach the merged config
|
||||
// in the first place — only the URL-scoped `//host/:certfile=...` and
|
||||
// `//host/:keyfile=...` forms are honored, and those are already pinned
|
||||
// to their authoring registry by construction.
|
||||
//
|
||||
// `ca`/`cafile` are intentionally left unscoped-by-default: they're trust
|
||||
// anchors, not credentials, and corporate MITM-proxy setups rely on them
|
||||
// applying globally to every HTTPS request. The default registry override
|
||||
// can't weaponize an unscoped CA (the attacker would need a cert signed
|
||||
// by it), so the same pinning isn't warranted.
|
||||
const UNSCOPED_RESCOPABLE_KEYS = [
|
||||
'_authToken', '_auth', 'username', '_password', 'tokenHelper',
|
||||
'cert', 'key',
|
||||
] as const
|
||||
|
||||
function readAndFilterNpmrc (
|
||||
filePath: string,
|
||||
warnings: string[],
|
||||
@@ -157,7 +194,65 @@ function readAndFilterNpmrc (
|
||||
result[key] = value
|
||||
}
|
||||
}
|
||||
return result
|
||||
return rescopeUnscopedCreds(result, filePath, warnings)
|
||||
}
|
||||
|
||||
// Rewrite any unscoped per-registry keys in `source` to their URL-scoped
|
||||
// equivalents (`//host[:port]/path/:<key>=...`) using `source.registry` —
|
||||
// or the builtin default registry if the source doesn't declare its own.
|
||||
// This pins each layer's credential, client certificate, or CA setting to
|
||||
// the registry that layer named (or the implicit npmjs default), so a
|
||||
// later layer overriding `registry=` cannot pull a setting authored for
|
||||
// one host along to a different host. A URL-scoped key for the same
|
||||
// registry already present in `source` wins; we never overwrite an
|
||||
// explicit scoped value.
|
||||
//
|
||||
// Each rewrite triggers a deprecation warning so users migrate to writing
|
||||
// the URL-scoped form directly. npm has rejected unscoped credentials
|
||||
// outright since `npm@9` (`ERR_INVALID_AUTH`).
|
||||
function rescopeUnscopedCreds (
|
||||
source: Record<string, unknown>,
|
||||
sourceLabel: string,
|
||||
warnings: string[]
|
||||
): Record<string, unknown> {
|
||||
// Bail early if there's nothing to rescope. This skips the nerfDart call
|
||||
// when a source like the builtin pnpmrc has only a `registry=` line —
|
||||
// rescoping there would do nothing anyway.
|
||||
if (!UNSCOPED_RESCOPABLE_KEYS.some(key => key in source)) {
|
||||
return source
|
||||
}
|
||||
const rawRegistry = typeof source.registry === 'string' && source.registry !== '' ? source.registry : null
|
||||
const fallbackRegistry = rawRegistry ?? npmDefaults.registry
|
||||
let nerfedRegistry: string
|
||||
try {
|
||||
nerfedRegistry = nerfDart(normalizeRegistryUrl(fallbackRegistry))
|
||||
} catch {
|
||||
// `registry=` resolved to something `URL` can't parse — often an
|
||||
// unresolved `${VAR}` placeholder that left the string empty. Drop the
|
||||
// unscoped keys (a bare token is unsafe to bind anywhere) and warn.
|
||||
const dropped = UNSCOPED_RESCOPABLE_KEYS.filter(key => key in source)
|
||||
for (const key of dropped) delete source[key]
|
||||
warnings.push(`Unscoped per-registry settings (${dropped.join(', ')}) in "${sourceLabel}" were ignored: ` +
|
||||
`the source's "registry" value (${JSON.stringify(source.registry)}) is not a parseable URL, so pnpm cannot pin them anywhere safe. ` +
|
||||
'Write them URL-scoped (e.g. "//registry.example.com/:_authToken=...") to send them to a specific registry.')
|
||||
return source
|
||||
}
|
||||
const rescoped: string[] = []
|
||||
for (const key of UNSCOPED_RESCOPABLE_KEYS) {
|
||||
if (!(key in source)) continue
|
||||
const scopedKey = `${nerfedRegistry}:${key}`
|
||||
if (!(scopedKey in source)) {
|
||||
source[scopedKey] = source[key]
|
||||
}
|
||||
delete source[key]
|
||||
rescoped.push(key)
|
||||
}
|
||||
if (rescoped.length > 0) {
|
||||
warnings.push(`Unscoped per-registry settings (${rescoped.join(', ')}) in "${sourceLabel}" are deprecated. ` +
|
||||
`pnpm pinned them to "${nerfedRegistry}" for this run, but a future release will stop supporting unscoped per-registry settings. ` +
|
||||
`Write them as "${nerfedRegistry}:${rescoped[0]}=..." instead.`)
|
||||
}
|
||||
return source
|
||||
}
|
||||
|
||||
// Use the lossy variant so unresolved `${VAR}` placeholders become '' (each
|
||||
|
||||
@@ -110,7 +110,6 @@ const NPM_AUTH_SETTINGS = [
|
||||
'_authToken',
|
||||
'_password',
|
||||
'email',
|
||||
'keyfile',
|
||||
'username',
|
||||
]
|
||||
|
||||
|
||||
@@ -338,15 +338,19 @@ test('.npmrc does not load pnpm settings', async () => {
|
||||
},
|
||||
})
|
||||
|
||||
// rc options appear as usual
|
||||
// rc options appear as usual. Unscoped credentials (`username`,
|
||||
// `_authToken`) are rescoped to the file's registry at load — the .npmrc
|
||||
// here doesn't set its own `registry=`, so they pin to the npmjs default.
|
||||
expect(config.authConfig).toMatchObject({
|
||||
'//my-org.registry.example.com:username': 'some-employee',
|
||||
'//my-org.registry.example.com:_authToken': 'some-employee-token',
|
||||
'@my-org:registry': 'https://my-org.registry.example.com',
|
||||
'@jsr:registry': 'https://not-actually-jsr.example.com',
|
||||
username: 'example-user-name',
|
||||
_authToken: 'example-auth-token',
|
||||
'//registry.npmjs.org/:username': 'example-user-name',
|
||||
'//registry.npmjs.org/:_authToken': 'example-auth-token',
|
||||
})
|
||||
expect(config.authConfig.username).toBeUndefined()
|
||||
expect(config.authConfig._authToken).toBeUndefined()
|
||||
|
||||
// workspace-specific settings are omitted
|
||||
expect(config.authConfig['dlx-cache-max-age']).toBeUndefined()
|
||||
@@ -854,6 +858,307 @@ describe('unresolved ${VAR} placeholders in .npmrc auth values', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('unscoped credentials are pinned to the registry declared in their source file', () => {
|
||||
// Each .npmrc / auth.ini gets its unscoped credential keys rewritten to
|
||||
// URL-scoped form using the same source's `registry=` value (or the npmjs
|
||||
// default if it has none). A later layer overriding `registry=` therefore
|
||||
// cannot rebind the credential to its own registry — the credential is
|
||||
// already pinned to the URL its author intended.
|
||||
let originalXdg: string | undefined
|
||||
let configHome: string
|
||||
let userconfig: string
|
||||
|
||||
beforeEach(() => {
|
||||
prepareEmpty()
|
||||
fs.mkdirSync('user-home')
|
||||
userconfig = path.resolve('user-home', '.npmrc')
|
||||
configHome = path.resolve('xdg-config')
|
||||
fs.mkdirSync(path.join(configHome, 'pnpm'), { recursive: true })
|
||||
originalXdg = process.env.XDG_CONFIG_HOME
|
||||
process.env.XDG_CONFIG_HOME = configHome
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
if (originalXdg != null) {
|
||||
process.env.XDG_CONFIG_HOME = originalXdg
|
||||
} else {
|
||||
delete process.env.XDG_CONFIG_HOME
|
||||
}
|
||||
})
|
||||
|
||||
test('pins user-level _authToken to that file\'s registry, never the workspace registry', async () => {
|
||||
fs.writeFileSync(userconfig, 'registry=https://trusted.example.com/\n_authToken=user-secret\n', 'utf8')
|
||||
fs.writeFileSync('.npmrc', 'registry=https://attacker.example.com/\n', 'utf8')
|
||||
|
||||
const { config } = await getConfig({
|
||||
cliOptions: { userconfig },
|
||||
env: { ...env, XDG_CONFIG_HOME: configHome },
|
||||
packageManager: { name: 'pnpm', version: '1.0.0' },
|
||||
workspaceDir: process.cwd(),
|
||||
})
|
||||
|
||||
expect(config.configByUri).toMatchObject({
|
||||
'//trusted.example.com/': { creds: { authToken: 'user-secret' } },
|
||||
})
|
||||
expect(config.configByUri['//attacker.example.com/']).toBeUndefined()
|
||||
})
|
||||
|
||||
test('pins user-level _auth (basic) the same way', async () => {
|
||||
// cspell:disable-next-line
|
||||
fs.writeFileSync(userconfig, 'registry=https://trusted.example.com/\n_auth=dXNlcjpwYXNz\n', 'utf8')
|
||||
fs.writeFileSync('.npmrc', 'registry=https://attacker.example.com/\n', 'utf8')
|
||||
|
||||
const { config } = await getConfig({
|
||||
cliOptions: { userconfig },
|
||||
env: { ...env, XDG_CONFIG_HOME: configHome },
|
||||
packageManager: { name: 'pnpm', version: '1.0.0' },
|
||||
workspaceDir: process.cwd(),
|
||||
})
|
||||
|
||||
expect(config.configByUri).toMatchObject({
|
||||
'//trusted.example.com/': { creds: { basicAuth: { username: 'user', password: 'pass' } } },
|
||||
})
|
||||
expect(config.configByUri['//attacker.example.com/']).toBeUndefined()
|
||||
})
|
||||
|
||||
test('pins user-level username/_password the same way', async () => {
|
||||
// cspell:disable-next-line
|
||||
fs.writeFileSync(userconfig, 'registry=https://trusted.example.com/\nusername=alice\n_password=cGFzcw==\n', 'utf8')
|
||||
fs.writeFileSync('.npmrc', 'registry=https://attacker.example.com/\n', 'utf8')
|
||||
|
||||
const { config } = await getConfig({
|
||||
cliOptions: { userconfig },
|
||||
env: { ...env, XDG_CONFIG_HOME: configHome },
|
||||
packageManager: { name: 'pnpm', version: '1.0.0' },
|
||||
workspaceDir: process.cwd(),
|
||||
})
|
||||
|
||||
expect(config.configByUri).toMatchObject({
|
||||
'//trusted.example.com/': { creds: { basicAuth: { username: 'alice', password: 'pass' } } },
|
||||
})
|
||||
expect(config.configByUri['//attacker.example.com/']).toBeUndefined()
|
||||
})
|
||||
|
||||
test('auth.ini with no registry of its own falls back to the npmjs default', async () => {
|
||||
// The split-file case: ~/.npmrc declares a registry but no creds; auth.ini
|
||||
// declares an unscoped credential with no registry. Each file rescopes in
|
||||
// isolation, so the credential pins to the builtin npmjs default — NOT to
|
||||
// whatever the workspace later overrides the merged registry to.
|
||||
fs.writeFileSync(userconfig, 'registry=https://trusted.example.com/\n', 'utf8')
|
||||
fs.writeFileSync(
|
||||
path.join(configHome, 'pnpm', 'auth.ini'),
|
||||
'_authToken=user-secret\n',
|
||||
'utf8'
|
||||
)
|
||||
fs.writeFileSync('.npmrc', 'registry=https://attacker.example.com/\n', 'utf8')
|
||||
|
||||
const { config } = await getConfig({
|
||||
cliOptions: { userconfig },
|
||||
env: { ...env, XDG_CONFIG_HOME: configHome },
|
||||
packageManager: { name: 'pnpm', version: '1.0.0' },
|
||||
workspaceDir: process.cwd(),
|
||||
})
|
||||
|
||||
expect(config.configByUri).toMatchObject({
|
||||
'//registry.npmjs.org/': { creds: { authToken: 'user-secret' } },
|
||||
})
|
||||
expect(config.configByUri['//attacker.example.com/']).toBeUndefined()
|
||||
expect(config.configByUri['//trusted.example.com/']).toBeUndefined()
|
||||
})
|
||||
|
||||
test('user-level credentials work when no workspace .npmrc exists', async () => {
|
||||
fs.writeFileSync(userconfig, 'registry=https://trusted.example.com/\n_authToken=user-secret\n', 'utf8')
|
||||
|
||||
const { config } = await getConfig({
|
||||
cliOptions: { userconfig },
|
||||
env: { ...env, XDG_CONFIG_HOME: configHome },
|
||||
packageManager: { name: 'pnpm', version: '1.0.0' },
|
||||
workspaceDir: process.cwd(),
|
||||
})
|
||||
|
||||
expect(config.configByUri).toMatchObject({
|
||||
'//trusted.example.com/': { creds: { authToken: 'user-secret' } },
|
||||
})
|
||||
})
|
||||
|
||||
test('workspace-supplied unscoped credentials pin to the workspace registry', async () => {
|
||||
fs.writeFileSync(userconfig, '', 'utf8')
|
||||
fs.writeFileSync('.npmrc', 'registry=https://workspace.example.com/\n_authToken=workspace-token\n', 'utf8')
|
||||
|
||||
const { config } = await getConfig({
|
||||
cliOptions: { userconfig },
|
||||
env: { ...env, XDG_CONFIG_HOME: configHome },
|
||||
packageManager: { name: 'pnpm', version: '1.0.0' },
|
||||
workspaceDir: process.cwd(),
|
||||
})
|
||||
|
||||
expect(config.configByUri).toMatchObject({
|
||||
'//workspace.example.com/': { creds: { authToken: 'workspace-token' } },
|
||||
})
|
||||
})
|
||||
|
||||
test('explicit URL-scoped credentials pass through unchanged', async () => {
|
||||
fs.writeFileSync(
|
||||
userconfig,
|
||||
'registry=https://trusted.example.com/\n//trusted.example.com/:_authToken=user-secret\n',
|
||||
'utf8'
|
||||
)
|
||||
fs.writeFileSync('.npmrc', 'registry=https://attacker.example.com/\n', 'utf8')
|
||||
|
||||
const { config, warnings } = await getConfig({
|
||||
cliOptions: { userconfig },
|
||||
env: { ...env, XDG_CONFIG_HOME: configHome },
|
||||
packageManager: { name: 'pnpm', version: '1.0.0' },
|
||||
workspaceDir: process.cwd(),
|
||||
})
|
||||
|
||||
expect(config.configByUri).toMatchObject({
|
||||
'//trusted.example.com/': { creds: { authToken: 'user-secret' } },
|
||||
})
|
||||
// URL-scoped tokens should NOT trigger the deprecation warning.
|
||||
expect(warnings.join('\n')).not.toMatch(/deprecated/i)
|
||||
})
|
||||
|
||||
test('CLI --registry override does not pull an unscoped user-level token along', async () => {
|
||||
// Same trust boundary as the workspace case: an unscoped token is ambient
|
||||
// and shouldn't follow whatever registry the CLI happens to point at.
|
||||
fs.writeFileSync(userconfig, '_authToken=user-secret\n', 'utf8')
|
||||
|
||||
const { config } = await getConfig({
|
||||
cliOptions: { userconfig, registry: 'https://attacker.example.com/' },
|
||||
env: { ...env, XDG_CONFIG_HOME: configHome },
|
||||
packageManager: { name: 'pnpm', version: '1.0.0' },
|
||||
workspaceDir: process.cwd(),
|
||||
})
|
||||
|
||||
// The token rescoped to the npmjs default when the user file was read.
|
||||
expect(config.configByUri).toMatchObject({
|
||||
'//registry.npmjs.org/': { creds: { authToken: 'user-secret' } },
|
||||
})
|
||||
expect(config.configByUri['//attacker.example.com/']).toBeUndefined()
|
||||
})
|
||||
|
||||
test('pins inline client cert/key to the file\'s registry, never the workspace registry', async () => {
|
||||
const inlineCert = '-----BEGIN CERTIFICATE-----\\ncertbody\\n-----END CERTIFICATE-----'
|
||||
const inlineKey = '-----BEGIN PRIVATE KEY-----\\nkeybody\\n-----END PRIVATE KEY-----'
|
||||
fs.writeFileSync(
|
||||
userconfig,
|
||||
`registry=https://trusted.example.com/\ncert=${inlineCert}\nkey=${inlineKey}\n`,
|
||||
'utf8'
|
||||
)
|
||||
fs.writeFileSync('.npmrc', 'registry=https://attacker.example.com/\n', 'utf8')
|
||||
|
||||
const { config } = await getConfig({
|
||||
cliOptions: { userconfig },
|
||||
env: { ...env, XDG_CONFIG_HOME: configHome },
|
||||
packageManager: { name: 'pnpm', version: '1.0.0' },
|
||||
workspaceDir: process.cwd(),
|
||||
})
|
||||
|
||||
// `\n` escapes are expanded to real newlines by getNetworkConfigs.
|
||||
expect(config.configByUri['//trusted.example.com/']?.tls).toMatchObject({
|
||||
cert: inlineCert.replace(/\\n/g, '\n'),
|
||||
key: inlineKey.replace(/\\n/g, '\n'),
|
||||
})
|
||||
expect(config.configByUri['//attacker.example.com/']).toBeUndefined()
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
describe('unscoped credential deprecation warning', () => {
|
||||
// pnpm warns whenever it reads any unscoped auth value from an .npmrc /
|
||||
// auth.ini, regardless of whether the rebind defense fires. URL-scoped tokens
|
||||
// have been npm's recommended pattern since npm@9, and unscoped credentials
|
||||
// are slated for removal in a future major release.
|
||||
let originalXdg: string | undefined
|
||||
let configHome: string
|
||||
let userconfig: string
|
||||
|
||||
beforeEach(() => {
|
||||
prepareEmpty()
|
||||
fs.mkdirSync('user-home')
|
||||
userconfig = path.resolve('user-home', '.npmrc')
|
||||
configHome = path.resolve('xdg-config')
|
||||
fs.mkdirSync(path.join(configHome, 'pnpm'), { recursive: true })
|
||||
originalXdg = process.env.XDG_CONFIG_HOME
|
||||
process.env.XDG_CONFIG_HOME = configHome
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
if (originalXdg != null) {
|
||||
process.env.XDG_CONFIG_HOME = originalXdg
|
||||
} else {
|
||||
delete process.env.XDG_CONFIG_HOME
|
||||
}
|
||||
})
|
||||
|
||||
test('warns about unscoped _authToken in user .npmrc', async () => {
|
||||
fs.writeFileSync(userconfig, 'registry=https://example.com/\n_authToken=secret\n', 'utf8')
|
||||
|
||||
const { warnings } = await getConfig({
|
||||
cliOptions: { userconfig },
|
||||
env: { ...env, XDG_CONFIG_HOME: configHome },
|
||||
packageManager: { name: 'pnpm', version: '1.0.0' },
|
||||
workspaceDir: process.cwd(),
|
||||
})
|
||||
|
||||
expect(warnings.find(w => w.includes('Unscoped per-registry settings'))).toBeDefined()
|
||||
expect(warnings.find(w => w.includes('_authToken'))).toBeDefined()
|
||||
expect(warnings.find(w => w.includes(userconfig))).toBeDefined()
|
||||
})
|
||||
|
||||
test('warns about unscoped _auth, username, _password', async () => {
|
||||
// _auth and _password are base64-encoded per npm convention.
|
||||
// cspell:disable-next-line
|
||||
fs.writeFileSync(userconfig, '_auth=dXNlcjpwYXNz\nusername=alice\n_password=cGFzcw==\n', 'utf8')
|
||||
|
||||
const { warnings } = await getConfig({
|
||||
cliOptions: { userconfig },
|
||||
env: { ...env, XDG_CONFIG_HOME: configHome },
|
||||
packageManager: { name: 'pnpm', version: '1.0.0' },
|
||||
workspaceDir: process.cwd(),
|
||||
})
|
||||
|
||||
const warning = warnings.find(w => w.includes('Unscoped per-registry settings'))
|
||||
expect(warning).toBeDefined()
|
||||
expect(warning).toContain('_auth')
|
||||
expect(warning).toContain('username')
|
||||
expect(warning).toContain('_password')
|
||||
})
|
||||
|
||||
test('warns about unscoped credentials in workspace .npmrc too', async () => {
|
||||
fs.writeFileSync('.npmrc', 'registry=https://workspace.example.com/\n_authToken=workspace-token\n', 'utf8')
|
||||
|
||||
const { warnings } = await getConfig({
|
||||
cliOptions: {},
|
||||
env: { ...env, XDG_CONFIG_HOME: configHome },
|
||||
packageManager: { name: 'pnpm', version: '1.0.0' },
|
||||
workspaceDir: process.cwd(),
|
||||
})
|
||||
|
||||
const warning = warnings.find(w => w.includes('Unscoped per-registry settings'))
|
||||
expect(warning).toBeDefined()
|
||||
expect(warning).toContain(path.resolve('.npmrc'))
|
||||
})
|
||||
|
||||
test('does not warn when only URL-scoped credentials are present', async () => {
|
||||
fs.writeFileSync(
|
||||
userconfig,
|
||||
'registry=https://example.com/\n//example.com/:_authToken=secret\n',
|
||||
'utf8'
|
||||
)
|
||||
|
||||
const { warnings } = await getConfig({
|
||||
cliOptions: { userconfig },
|
||||
env: { ...env, XDG_CONFIG_HOME: configHome },
|
||||
packageManager: { name: 'pnpm', version: '1.0.0' },
|
||||
workspaceDir: process.cwd(),
|
||||
})
|
||||
|
||||
expect(warnings.find(w => w.includes('Unscoped per-registry settings'))).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
test('throw error if --save-prod is used with --save-peer', async () => {
|
||||
await expect(getConfig({
|
||||
cliOptions: {
|
||||
|
||||
@@ -142,6 +142,7 @@
|
||||
"jega",
|
||||
"jhcg",
|
||||
"jnbpamcxayl",
|
||||
"junyi",
|
||||
"kebabcase",
|
||||
"kevva",
|
||||
"keyfile",
|
||||
@@ -285,6 +286,11 @@
|
||||
"renderable",
|
||||
"replit",
|
||||
"reqheaders",
|
||||
"rescopable",
|
||||
"rescope",
|
||||
"rescoped",
|
||||
"rescopes",
|
||||
"rescoping",
|
||||
"rimrafed",
|
||||
"rmgr",
|
||||
"rpmdevtools",
|
||||
|
||||
2
deps/compliance/commands/src/audit/audit.ts
vendored
2
deps/compliance/commands/src/audit/audit.ts
vendored
@@ -213,7 +213,7 @@ export async function handler (opts: AuditOptions, params: string[] = []): Promi
|
||||
const { envLockfile, include, lockfile } = await loadAuditContext(opts)
|
||||
const networkOptions = createAuditNetworkOptions(opts)
|
||||
let auditReport!: AuditReport
|
||||
const getAuthHeader = createGetAuthHeaderByURI(opts.configByUri, opts.registries?.default)
|
||||
const getAuthHeader = createGetAuthHeaderByURI(opts.configByUri)
|
||||
try {
|
||||
auditReport = await audit(lockfile, getAuthHeader, {
|
||||
dispatcherOptions: {
|
||||
|
||||
@@ -20,7 +20,7 @@ export async function auditSignatures (opts: AuditOptions): Promise<{ exitCode:
|
||||
throw new PnpmError('AUDIT_NO_PACKAGES', 'No installed packages found to audit')
|
||||
}
|
||||
|
||||
const getAuthHeader = createGetAuthHeaderByURI(opts.configByUri, opts.registries?.default)
|
||||
const getAuthHeader = createGetAuthHeaderByURI(opts.configByUri)
|
||||
const networkOptions = createAuditNetworkOptions(opts)
|
||||
const result = await verifySignatures(packages, getAuthHeader, {
|
||||
ca: networkOptions.ca,
|
||||
|
||||
@@ -52,7 +52,7 @@ export async function fetchPackageInfo (
|
||||
}
|
||||
const registry = pickRegistryForPackage(opts.registries, packageName)
|
||||
const fetchFromRegistry = createFetchFromRegistry(opts)
|
||||
const getAuthHeader = createGetAuthHeaderByURI(opts.configByUri ?? {}, opts.registries?.default)
|
||||
const getAuthHeader = createGetAuthHeaderByURI(opts.configByUri ?? {})
|
||||
const fetchResult = await fetchMetadataFromFromRegistry(
|
||||
{
|
||||
fetch: fetchFromRegistry,
|
||||
|
||||
@@ -68,7 +68,7 @@ export interface Client {
|
||||
|
||||
export function createClient (opts: ClientOptions): Client {
|
||||
const fetchFromRegistry = createFetchFromRegistry(opts)
|
||||
const getAuthHeader = createGetAuthHeaderByURI(opts.configByUri, opts.registries?.default)
|
||||
const getAuthHeader = createGetAuthHeaderByURI(opts.configByUri)
|
||||
|
||||
// One per-install LRU shared with both the resolver's pickPackage
|
||||
// pass and the verifier's lookup chain. When the resolver populates
|
||||
@@ -87,7 +87,7 @@ export function createClient (opts: ClientOptions): Client {
|
||||
|
||||
export function createResolver (opts: Omit<ClientOptions, 'storeIndex'>): { resolve: ResolveFunction, resolveLatest: ResolveLatestDispatcher, clearCache: () => void } {
|
||||
const fetchFromRegistry = createFetchFromRegistry(opts)
|
||||
const getAuthHeader = createGetAuthHeaderByURI(opts.configByUri, opts.registries?.default)
|
||||
const getAuthHeader = createGetAuthHeaderByURI(opts.configByUri)
|
||||
|
||||
return _createResolver(fetchFromRegistry, getAuthHeader, { ...opts, customResolvers: opts.customResolvers })
|
||||
}
|
||||
|
||||
@@ -71,27 +71,6 @@ test('installing a package that need authentication, using password', async () =
|
||||
project.has('@pnpm.e2e/needs-auth')
|
||||
})
|
||||
|
||||
test('a package that need authentication, legacy way', async () => {
|
||||
const project = prepareEmpty()
|
||||
|
||||
await addUser({
|
||||
email: 'foo@bar.com',
|
||||
password: 'bar',
|
||||
username: 'foo',
|
||||
})
|
||||
|
||||
const configByUri: Record<string, RegistryConfig> = {
|
||||
'': { creds: { basicAuth: { username: 'foo', password: 'bar' } } },
|
||||
}
|
||||
await addDependenciesToPackage({}, ['@pnpm.e2e/needs-auth'], testDefaults({}, {
|
||||
configByUri,
|
||||
}, {
|
||||
configByUri,
|
||||
}))
|
||||
|
||||
project.has('@pnpm.e2e/needs-auth')
|
||||
})
|
||||
|
||||
test('a scoped package that need authentication specific to scope', async () => {
|
||||
const project = prepareEmpty()
|
||||
|
||||
|
||||
@@ -100,7 +100,7 @@ export async function resolveAndInstallConfigDeps (
|
||||
|
||||
// Resolve missing deps
|
||||
const fetch = createFetchFromRegistry(opts)
|
||||
const getAuthHeader = createGetAuthHeaderByURI(opts.configByUri ?? {}, opts.registries?.default)
|
||||
const getAuthHeader = createGetAuthHeaderByURI(opts.configByUri ?? {})
|
||||
const { resolveFromNpm } = createNpmResolver(fetch, getAuthHeader, opts)
|
||||
|
||||
await Promise.all(depsToResolve.map(async ({ name, specifier }) => {
|
||||
|
||||
@@ -30,7 +30,7 @@ export async function resolveConfigDeps (configDeps: string[], opts: ResolveConf
|
||||
}
|
||||
|
||||
const fetch = createFetchFromRegistry(opts)
|
||||
const getAuthHeader = createGetAuthHeaderByURI(opts.configByUri ?? {}, opts.registries?.default)
|
||||
const getAuthHeader = createGetAuthHeaderByURI(opts.configByUri ?? {})
|
||||
const { resolveFromNpm } = createNpmResolver(fetch, getAuthHeader, opts)
|
||||
|
||||
// Extract existing specifiers from configDependencies (handles both old and new formats)
|
||||
|
||||
@@ -4,24 +4,15 @@ import { PnpmError } from '@pnpm/error'
|
||||
import type { Creds, RegistryConfig, TokenHelper } from '@pnpm/types'
|
||||
|
||||
export function getAuthHeadersFromCreds (
|
||||
configByUri: Record<string, RegistryConfig>,
|
||||
defaultRegistry: string
|
||||
configByUri: Record<string, RegistryConfig>
|
||||
): Record<string, string> {
|
||||
const authHeaderValueByURI: Record<string, string> = {}
|
||||
for (const [uri, registryConfig] of Object.entries(configByUri)) {
|
||||
if (uri === '') continue // default auth handled below
|
||||
const header = credsToHeader(registryConfig.creds)
|
||||
if (header) {
|
||||
authHeaderValueByURI[uri] = header
|
||||
}
|
||||
}
|
||||
const defaultConfig = configByUri['']
|
||||
if (defaultConfig?.creds) {
|
||||
const header = credsToHeader(defaultConfig.creds)
|
||||
if (header) {
|
||||
authHeaderValueByURI[defaultRegistry] = header
|
||||
}
|
||||
}
|
||||
return authHeaderValueByURI
|
||||
}
|
||||
|
||||
|
||||
@@ -5,11 +5,9 @@ import { getAuthHeadersFromCreds } from './getAuthHeadersFromConfig.js'
|
||||
import { removePort } from './helpers/removePort.js'
|
||||
|
||||
export function createGetAuthHeaderByURI (
|
||||
configByUri: Record<string, RegistryConfig>,
|
||||
defaultRegistry?: string
|
||||
configByUri: Record<string, RegistryConfig>
|
||||
): (uri: string) => string | undefined {
|
||||
const registry = defaultRegistry ? nerfDart(defaultRegistry) : '//registry.npmjs.org/'
|
||||
const authHeaders = getAuthHeadersFromCreds(configByUri, registry)
|
||||
const authHeaders = getAuthHeadersFromCreds(configByUri)
|
||||
if (Object.keys(authHeaders).length === 0) return (uri: string) => basicAuth(new URL(uri))
|
||||
return getAuthHeaderByURI.bind(null, authHeaders, getMaxParts(Object.keys(authHeaders)))
|
||||
}
|
||||
|
||||
@@ -69,15 +69,6 @@ test('getAuthHeaderByURI() when the registry has pathnames', () => {
|
||||
expect(getAuthHeaderByURI('https://npm.pkg.github.com/pnpm/foo/-/foo-1.0.0.tgz')).toBe('Bearer abc123')
|
||||
})
|
||||
|
||||
test('getAuthHeaderByURI() with default registry auth', () => {
|
||||
const getAuthHeaderByURI = createGetAuthHeaderByURI(
|
||||
{ '': { creds: { authToken: 'default-token' } } },
|
||||
'https://registry.npmjs.org/'
|
||||
)
|
||||
expect(getAuthHeaderByURI('https://registry.npmjs.org/')).toBe('Bearer default-token')
|
||||
expect(getAuthHeaderByURI('https://registry.npmjs.org/foo/-/foo-1.0.0.tgz')).toBe('Bearer default-token')
|
||||
})
|
||||
|
||||
test('getAuthHeaderByURI() with basic auth via basicAuth', () => {
|
||||
const getAuthHeaderByURI = createGetAuthHeaderByURI({
|
||||
'//reg.com/': { creds: { basicAuth: { username: 'user', password: 'pass' } } },
|
||||
|
||||
@@ -33,7 +33,7 @@ describe('getAuthHeadersFromCreds()', () => {
|
||||
const result = getAuthHeadersFromCreds({
|
||||
'//registry.npmjs.org/': { creds: { authToken: 'abc123' } },
|
||||
'//registry.hu/': { creds: { authToken: 'def456' } },
|
||||
}, '//registry.npmjs.org/')
|
||||
})
|
||||
expect(result).toStrictEqual({
|
||||
'//registry.npmjs.org/': 'Bearer abc123',
|
||||
'//registry.hu/': 'Bearer def456',
|
||||
@@ -42,23 +42,15 @@ describe('getAuthHeadersFromCreds()', () => {
|
||||
it('should convert basicAuth to Basic header', () => {
|
||||
const result = getAuthHeadersFromCreds({
|
||||
'//registry.foobar.eu/': { creds: { basicAuth: { username: 'foobar', password: 'foobar' } } },
|
||||
}, '//registry.npmjs.org/')
|
||||
})
|
||||
expect(result).toStrictEqual({
|
||||
'//registry.foobar.eu/': 'Basic Zm9vYmFyOmZvb2Jhcg==',
|
||||
})
|
||||
})
|
||||
it('should handle default registry auth (empty key)', () => {
|
||||
const result = getAuthHeadersFromCreds({
|
||||
'': { creds: { authToken: 'default-token' } },
|
||||
}, '//reg.com/')
|
||||
expect(result).toStrictEqual({
|
||||
'//reg.com/': 'Bearer default-token',
|
||||
})
|
||||
})
|
||||
it('should execute tokenHelper', () => {
|
||||
const result = getAuthHeadersFromCreds({
|
||||
'//registry.foobar.eu/': { creds: { tokenHelper: [osTokenHelper[osFamily]] } },
|
||||
}, '//registry.npmjs.org/')
|
||||
})
|
||||
expect(result).toStrictEqual({
|
||||
'//registry.foobar.eu/': 'Bearer token-from-spawn',
|
||||
})
|
||||
@@ -66,7 +58,7 @@ describe('getAuthHeadersFromCreds()', () => {
|
||||
it('should prepend Bearer to raw token from tokenHelper', () => {
|
||||
const result = getAuthHeadersFromCreds({
|
||||
'//registry.foobar.eu/': { creds: { tokenHelper: [osRawTokenHelper[osFamily]] } },
|
||||
}, '//registry.npmjs.org/')
|
||||
})
|
||||
expect(result).toStrictEqual({
|
||||
'//registry.foobar.eu/': 'Bearer raw-token-no-scheme',
|
||||
})
|
||||
@@ -74,15 +66,15 @@ describe('getAuthHeadersFromCreds()', () => {
|
||||
it('should throw an error if the token helper fails', () => {
|
||||
expect(() => getAuthHeadersFromCreds({
|
||||
'//reg.com/': { creds: { tokenHelper: [osErrorTokenHelper[osFamily]] } },
|
||||
}, '//registry.npmjs.org/')).toThrow('Exit code')
|
||||
})).toThrow('Exit code')
|
||||
})
|
||||
it('should throw an error if the token helper returns an empty token', () => {
|
||||
expect(() => getAuthHeadersFromCreds({
|
||||
'//reg.com/': { creds: { tokenHelper: [osEmptyTokenHelper[osFamily]] } },
|
||||
}, '//registry.npmjs.org/')).toThrow('returned an empty token')
|
||||
})).toThrow('returned an empty token')
|
||||
})
|
||||
it('should return empty object when no auth infos', () => {
|
||||
const result = getAuthHeadersFromCreds({}, '//registry.npmjs.org/')
|
||||
const result = getAuthHeadersFromCreds({})
|
||||
expect(result).toStrictEqual({})
|
||||
})
|
||||
})
|
||||
|
||||
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
@@ -2676,6 +2676,9 @@ importers:
|
||||
'@pnpm/config.matcher':
|
||||
specifier: workspace:*
|
||||
version: link:../matcher
|
||||
'@pnpm/config.nerf-dart':
|
||||
specifier: 'catalog:'
|
||||
version: 1.0.1
|
||||
'@pnpm/constants':
|
||||
specifier: workspace:*
|
||||
version: link:../../core/constants
|
||||
|
||||
@@ -38,7 +38,7 @@ export async function updateDeprecation (
|
||||
): Promise<string> {
|
||||
const registryUrl = pickRegistryForPackage(opts.registries ?? { default: 'https://registry.npmjs.org/' }, packageName)
|
||||
|
||||
const getAuthHeader = createGetAuthHeaderByURI(opts.configByUri ?? {}, registryUrl)
|
||||
const getAuthHeader = createGetAuthHeaderByURI(opts.configByUri ?? {})
|
||||
|
||||
const authHeader = getAuthHeader(registryUrl)
|
||||
|
||||
|
||||
@@ -201,7 +201,7 @@ function getAuthHeaderForRegistry (
|
||||
configByUri: Record<string, RegistryConfig> | undefined,
|
||||
registryUrl: string
|
||||
): string | undefined {
|
||||
const getAuthHeader = createGetAuthHeaderByURI(configByUri ?? {}, registryUrl)
|
||||
const getAuthHeader = createGetAuthHeaderByURI(configByUri ?? {})
|
||||
return getAuthHeader(registryUrl)
|
||||
}
|
||||
|
||||
|
||||
@@ -182,7 +182,7 @@ function getAuthHeaderForRegistry (
|
||||
configByUri: Record<string, RegistryConfig> | undefined,
|
||||
registryUrl: string
|
||||
): string | undefined {
|
||||
const getAuthHeader = createGetAuthHeaderByURI(configByUri ?? {}, registryUrl)
|
||||
const getAuthHeader = createGetAuthHeaderByURI(configByUri ?? {})
|
||||
return getAuthHeader(registryUrl)
|
||||
}
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ export async function handler (opts: PingOptions): Promise<string> {
|
||||
pingUrlObject.searchParams.set('write', 'true')
|
||||
const pingUrl = pingUrlObject.toString()
|
||||
|
||||
const getAuthHeader = createGetAuthHeaderByURI(opts.configByUri ?? {}, normalizedRegistryUrl)
|
||||
const getAuthHeader = createGetAuthHeaderByURI(opts.configByUri ?? {})
|
||||
const authHeaderValue = getAuthHeader(normalizedRegistryUrl)
|
||||
const fetchFromRegistry = createFetchFromRegistry(opts)
|
||||
|
||||
|
||||
@@ -87,7 +87,7 @@ export async function handler (
|
||||
searchUrl.searchParams.set('size', (opts.searchLimit ?? 20).toString())
|
||||
|
||||
const fetchFromRegistry = createFetchFromRegistry(opts)
|
||||
const getAuthHeader = createGetAuthHeaderByURI(opts.configByUri ?? {}, opts.registries?.default)
|
||||
const getAuthHeader = createGetAuthHeaderByURI(opts.configByUri ?? {})
|
||||
|
||||
const response = await fetchFromRegistry(searchUrl.toString(), {
|
||||
authHeaderValue: getAuthHeader(registry),
|
||||
|
||||
@@ -133,6 +133,6 @@ export function getAuthHeaderForRegistry (
|
||||
configByUri: Record<string, RegistryConfig> | undefined,
|
||||
registryUrl: string
|
||||
): string | undefined {
|
||||
const getAuthHeader = createGetAuthHeaderByURI(configByUri ?? {}, registryUrl)
|
||||
const getAuthHeader = createGetAuthHeaderByURI(configByUri ?? {})
|
||||
return getAuthHeader(registryUrl)
|
||||
}
|
||||
|
||||
@@ -104,7 +104,7 @@ async function unpublishPackage (
|
||||
): Promise<string> {
|
||||
const registryUrl = pickRegistryForPackage(opts.registries ?? { default: 'https://registry.npmjs.org/' }, packageName)
|
||||
|
||||
const getAuthHeader = createGetAuthHeaderByURI(opts.configByUri ?? {}, registryUrl)
|
||||
const getAuthHeader = createGetAuthHeaderByURI(opts.configByUri ?? {})
|
||||
|
||||
const authHeader = getAuthHeader(registryUrl)
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ export function help (): string {
|
||||
|
||||
export async function handler (opts: WhoamiOptions): Promise<string> {
|
||||
const registryUrl = normalizeRegistryUrl(opts.registries?.default ?? 'https://registry.npmjs.org/')
|
||||
const getAuthHeader = createGetAuthHeaderByURI(opts.configByUri ?? {}, registryUrl)
|
||||
const getAuthHeader = createGetAuthHeaderByURI(opts.configByUri ?? {})
|
||||
const authHeader = getAuthHeader(registryUrl)
|
||||
if (!authHeader) {
|
||||
throw new PnpmError('WHOAMI_UNAUTHORIZED', 'You must be logged in to use whoami')
|
||||
|
||||
@@ -251,15 +251,6 @@ function findRegistryInfo (
|
||||
tls = { ...entry.tls, ...tls }
|
||||
}
|
||||
|
||||
const isDefaultRegistry =
|
||||
nonNormalizedRegistry === registries.default ||
|
||||
registry === registries.default ||
|
||||
registry === parseSupportedRegistryUrl(registries.default)?.normalizedUrl
|
||||
|
||||
if (isDefaultRegistry) {
|
||||
creds ??= configByUri['']?.creds
|
||||
}
|
||||
|
||||
return {
|
||||
registry,
|
||||
config: { creds, tls },
|
||||
|
||||
@@ -22,8 +22,7 @@ export interface StageContext {
|
||||
export function createStageContext (opts: StageOptions, packageName?: string): StageContext {
|
||||
const registry = getStageRegistry(opts, packageName)
|
||||
const getAuthHeaderByUri = createGetAuthHeaderByURI(
|
||||
opts.configByUri ?? {} as Record<string, RegistryConfig>,
|
||||
registry
|
||||
opts.configByUri ?? {} as Record<string, RegistryConfig>
|
||||
)
|
||||
return {
|
||||
opts,
|
||||
|
||||
@@ -215,7 +215,7 @@ export function createResolutionVerifiers (
|
||||
timeout: opts.timeout ?? 60_000,
|
||||
fetchWarnTimeoutMs: opts.fetchWarnTimeoutMs ?? 10_000,
|
||||
}
|
||||
const getAuthHeaderValueByURI = createGetAuthHeaderByURI(opts.configByUri ?? {}, opts.registries.default)
|
||||
const getAuthHeaderValueByURI = createGetAuthHeaderByURI(opts.configByUri ?? {})
|
||||
const verifiers: ResolutionVerifier[] = []
|
||||
const npmVerifier = createNpmResolutionVerifier({
|
||||
minimumReleaseAge: opts.minimumReleaseAge,
|
||||
|
||||
Reference in New Issue
Block a user