refactor(auth): unify auth/SSL into structured configByUri (#11201)

Replaces the dual `authConfig` (raw .npmrc) + `authInfos` (parsed auth) + `sslConfigs` (parsed SSL) pattern with a single structured `configByUri: Record<string, RegistryConfig>` field on Config.

### New types (`@pnpm/types`)
- **`RegistryConfig`** — per-registry config: `{ creds?: Creds, tls?: TlsConfig }`
- **`Creds`** — auth credentials: `{ authToken?, basicAuth?, tokenHelper? }`
- **`TlsConfig`** — TLS config: `{ cert?, key?, ca? }`

### Key changes
- Rewrite `createGetAuthHeaderByURI` to accept `Record<string, RegistryConfig>` instead of raw .npmrc key-value pairs
- Eliminate duplicate auth parsing between `getAuthHeadersFromConfig` and `getNetworkConfigs`
- Remove `authConfig` from the install pipeline (`StrictInstallOptions`, `HeadlessOptions`), replaced by `configByUri`
- Remove `sslConfigs` from Config — SSL fields now live in `configByUri[uri].tls`
- Remove `authConfig['registry']` mutation in `extendInstallOptions` (default registry now passed directly to `createGetAuthHeaderByURI`)
- `authConfig` remains on Config only for raw .npmrc access (config commands, error reporting, config inheritance)

### Security
- tokenHelper in project .npmrc now throws instead of being silently stripped
- tokenHelper execution uses `shell: false` to prevent shell metacharacter injection
- Basic auth uses `Buffer.from().toString('base64')` instead of `btoa()` for Unicode safety
- Dispatcher only creates custom agents when entries actually have TLS fields
This commit is contained in:
Zoltan Kochan
2026-04-05 20:15:10 +02:00
committed by GitHub
parent b5d93c6ba9
commit 45a6cb6b2a
81 changed files with 554 additions and 748 deletions

View File

@@ -4,7 +4,7 @@ import { DEFAULT_REGISTRIES, normalizeRegistries } from '@pnpm/config.normalize-
import type { Config, ConfigContext } from '@pnpm/config.reader'
import type { LogBase } from '@pnpm/logger'
import type { StoreController } from '@pnpm/store.controller-types'
import type { Registries } from '@pnpm/types'
import type { Registries, RegistryConfig } from '@pnpm/types'
import { loadJsonFile } from 'load-json-file'
export type StrictBuildOptions = {
@@ -35,7 +35,7 @@ export type StrictBuildOptions = {
production: boolean
development: boolean
optional: boolean
authConfig: object
configByUri: Record<string, RegistryConfig>
userConfig: Record<string, string>
userAgent: string
packageManager: {
@@ -52,7 +52,7 @@ export type StrictBuildOptions = {
peersSuffixMaxLength: number
strictStorePkgContentCheck: boolean
fetchFullMetadata?: boolean
} & Pick<Config, 'sslConfigs' | 'allowBuilds'>
} & Pick<Config, 'allowBuilds'>
export type BuildOptions = Partial<StrictBuildOptions> &
Pick<StrictBuildOptions, 'storeDir' | 'storeController'> & Pick<ConfigContext, 'rootProjectManifest' | 'rootProjectManifestDir'>
@@ -73,7 +73,7 @@ const defaults = async (opts: BuildOptions): Promise<StrictBuildOptions> => {
packageManager,
pending: false,
production: true,
authConfig: {},
configByUri: {},
registries: DEFAULT_REGISTRIES,
scriptsPrependNodePath: false,
shamefullyHoist: false,

View File

@@ -35,7 +35,7 @@ export const DEFAULT_OPTS = {
pnpmfile: ['./.pnpmfile.cjs'],
pnpmHomeDir: '',
proxy: undefined,
authConfig: { registry: REGISTRY },
configByUri: {},
registries: { default: REGISTRY },
registry: REGISTRY,
rootProjectManifestDir: '',

View File

@@ -6,7 +6,7 @@ import { censorProtectedSettings } from './protectedSettings.js'
// Auth-related Config fields that are internal objects, not user settings.
const NON_SETTING_CONFIG_KEYS = new Set([
'authConfig', 'authInfos', 'sslConfigs',
'authConfig', 'configByUri',
])
/**

View File

@@ -7,12 +7,11 @@ import type {
ProjectManifest,
ProjectsGraph,
Registries,
SslConfig,
RegistryConfig,
TrustPolicy,
} from '@pnpm/types'
import type { OptionsFromRootManifest } from './getOptionsFromRootManifest.js'
import type { AuthInfo } from './parseAuthInfo.js'
export type UniversalOptions = Pick<Config, 'color' | 'dir' | 'authConfig'>
@@ -51,7 +50,7 @@ export interface ConfigContext {
* User-facing settings + auth/network config.
* Does NOT include runtime state — see {@link ConfigContext} for that.
*/
export interface Config extends AuthInfo, OptionsFromRootManifest {
export interface Config extends OptionsFromRootManifest {
allowNew: boolean
autoConfirmAllPrompts?: boolean
autoInstallPeers?: boolean
@@ -213,8 +212,7 @@ export interface Config extends AuthInfo, OptionsFromRootManifest {
blockExoticSubdeps?: boolean
registries: Registries
authInfos: Record<string, AuthInfo>
sslConfigs: Record<string, SslConfig>
configByUri: Record<string, RegistryConfig>
ignoreWorkspaceRootCheck: boolean
workspaceRoot: boolean

View File

@@ -32,6 +32,7 @@ const RAW_AUTH_CFG_KEY_SUFFIXES = [
const AUTH_CFG_KEYS = [
'ca',
'cert',
'configByUri',
'key',
'localAddress',
'gitShallowHosts',

View File

@@ -1,74 +1,59 @@
import fs from 'node:fs'
import type { SslConfig } from '@pnpm/types'
import type { Creds, RegistryConfig } from '@pnpm/types'
import normalizeRegistryUrl from 'normalize-registry-url'
import { type AuthInfo, type AuthInfoInput, parseAuthInfo } from './parseAuthInfo.js'
import { parseCreds, type RawCreds } from './parseCreds.js'
export interface NetworkConfigs {
authInfos?: Record<string, AuthInfo> // TODO: remove optional from here, this means that tests would have to be updated.
sslConfigs: Record<string, SslConfig>
configByUri?: Record<string, RegistryConfig> // TODO: remove optional from here, this means that tests would have to be updated.
registries: Record<string, string>
}
export function getNetworkConfigs (rawConfig: Record<string, unknown>): NetworkConfigs {
const authInfoInputs: Record<string, AuthInfoInput> = {}
const sslConfigs: Record<string, SslConfig> = {}
const rawCredsMap: Record<string, RawCreds> = {}
const registries: Record<string, string> = {}
const networkConfigs: NetworkConfigs = { registries }
for (const [configKey, value] of Object.entries(rawConfig)) {
if (configKey[0] === '@' && configKey.endsWith(':registry')) {
registries[configKey.slice(0, configKey.indexOf(':'))] = normalizeRegistryUrl(value as string)
continue
}
const parsed = tryParseAuthSetting(configKey) ?? tryParseSslSetting(configKey)
const parsedCreds = tryParseCredsKey(configKey)
if (parsedCreds) {
const { credsField, registry } = parsedCreds
rawCredsMap[registry] ??= {}
rawCredsMap[registry][credsField] = value as string
continue
}
switch (parsed?.target) {
case undefined:
continue
case 'auth': {
const { authInputKey, registry } = parsed
authInfoInputs[registry] ??= {}
authInfoInputs[registry][authInputKey] = value as string
continue
}
case 'ssl': {
const { registry, sslConfigKey, isFile } = parsed
sslConfigs[registry] ??= { cert: '', key: '' }
sslConfigs[registry][sslConfigKey] = isFile
? fs.readFileSync(value as string, 'utf8')
: (value as string).replace(/\\n/g, '\n')
continue
}
default: {
const _typeGuard: never = parsed
throw new Error(`Unhandled variant: ${JSON.stringify(_typeGuard)}`)
}
const parsedSsl = tryParseSslKey(configKey)
if (parsedSsl) {
const { registry, sslField, isFile } = parsedSsl
networkConfigs.configByUri ??= {}
networkConfigs.configByUri[registry] ??= {}
networkConfigs.configByUri[registry].tls ??= {}
networkConfigs.configByUri[registry].tls[sslField] = isFile
? fs.readFileSync(value as string, 'utf8')
: (value as string).replace(/\\n/g, '\n')
}
}
// Instead of directly returning the object literal at the end of the function,
// we create a temporary object of `networkConfigs` to avoid adding
// `authInfos: undefined` to the returning object to prevent the failures of
// existing tests which use `expect().to[Strict]Equal()` methods.
const networkConfigs: NetworkConfigs = {
registries,
sslConfigs,
}
for (const key in authInfoInputs) {
const authInfo = parseAuthInfo(authInfoInputs[key])
if (authInfo) {
networkConfigs.authInfos ??= {}
networkConfigs.authInfos[key] = authInfo
for (const uri in rawCredsMap) {
const creds = parseCreds(rawCredsMap[uri])
if (creds) {
networkConfigs.configByUri ??= {}
networkConfigs.configByUri[uri] ??= {}
networkConfigs.configByUri[uri].creds = creds
}
}
return networkConfigs
}
export function getDefaultAuthInfo (rawConfig: Record<string, unknown>): AuthInfo | undefined {
const input: AuthInfoInput = {}
export function getDefaultCreds (rawConfig: Record<string, unknown>): Creds | undefined {
const input: RawCreds = {}
for (const rawKey in AUTH_SUFFIX_KEY_MAP) {
const key = AUTH_SUFFIX_KEY_MAP[rawKey]
const value = rawConfig[rawKey] as string | undefined
@@ -76,11 +61,11 @@ export function getDefaultAuthInfo (rawConfig: Record<string, unknown>): AuthInf
input[key] = value
}
}
return parseAuthInfo(input)
return parseCreds(input)
}
const AUTH_SUFFIX_RE = /:(?<key>_auth|_authToken|_password|username|tokenHelper)$/
const AUTH_SUFFIX_KEY_MAP: Record<string, keyof AuthInfoInput> = {
const AUTH_SUFFIX_KEY_MAP: Record<string, keyof RawCreds> = {
_auth: 'authPairBase64',
_authToken: 'authToken',
_password: 'authPassword',
@@ -88,41 +73,41 @@ const AUTH_SUFFIX_KEY_MAP: Record<string, keyof AuthInfoInput> = {
tokenHelper: 'tokenHelper',
}
interface ParsedAuthSetting {
target: 'auth'
interface ParsedCredsKey {
registry: string
authInputKey: keyof AuthInfoInput
credsField: keyof RawCreds
}
function tryParseAuthSetting (key: string): ParsedAuthSetting | undefined {
function tryParseCredsKey (key: string): ParsedCredsKey | undefined {
const match = key.match(AUTH_SUFFIX_RE)
if (!match?.groups) {
return undefined
}
const registry = key.slice(0, match.index!) // already includes the trailing slash
const authInputKey = AUTH_SUFFIX_KEY_MAP[match.groups.key]
if (!authInputKey) {
const credsField = AUTH_SUFFIX_KEY_MAP[match.groups.key]
if (!credsField) {
throw new Error(`Unexpected key: ${match.groups.key}`)
}
return { target: 'auth', registry, authInputKey }
return { registry, credsField }
}
const SSL_SUFFIX_RE = /:(?<id>cert|key|ca)(?<kind>file)?$/
interface ParsedSslSetting {
target: 'ssl'
type SslField = 'cert' | 'key' | 'ca'
interface ParsedSslKey {
registry: string
sslConfigKey: keyof SslConfig
sslField: SslField
isFile: boolean
}
function tryParseSslSetting (key: string): ParsedSslSetting | undefined {
function tryParseSslKey (key: string): ParsedSslKey | undefined {
const match = key.match(SSL_SUFFIX_RE)
if (!match?.groups) {
return undefined
}
const registry = key.slice(0, match.index!) // already includes the trailing slash
const sslConfigKey = match.groups.id as keyof SslConfig
const sslField = match.groups.id as SslField
const isFile = Boolean(match.groups.kind)
return { target: 'ssl', registry, sslConfigKey, isFile }
return { registry, sslField, isFile }
}

View File

@@ -37,7 +37,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 { getDefaultAuthInfo, getNetworkConfigs } from './getNetworkConfigs.js'
import { getDefaultCreds, getNetworkConfigs } from './getNetworkConfigs.js'
import { getOptionsFromPnpmSettings } from './getOptionsFromRootManifest.js'
import { loadNpmrcConfig } from './loadNpmrcFiles.js'
import { npmDefaults } from './npmDefaults.js'
@@ -51,6 +51,7 @@ export { types }
export { getDefaultWorkspaceConcurrency, getWorkspaceConcurrency } from './concurrency.js'
export { getOptionsFromPnpmSettings, type OptionsFromRootManifest } from './getOptionsFromRootManifest.js'
export type { Creds } from './parseCreds.js'
export {
createProjectConfigRecord,
type CreateProjectConfigRecordOptions,
@@ -63,7 +64,6 @@ export {
ProjectConfigsMatchItemIsNotAStringError,
ProjectConfigUnsupportedFieldError,
} from './projectConfig.js'
export type { Config, ConfigContext, ProjectConfig, UniversalOptions, VerifyDepsBeforeRun }
export { isIniConfigKey } from './auth.js'
@@ -302,9 +302,22 @@ export async function getConfig (opts: {
...networkConfigs.registries,
}
pnpmConfig.registries = { ...registriesFromNpmrc }
pnpmConfig.authInfos = networkConfigs.authInfos ?? {} // TODO: remove `?? {}` (when possible)
pnpmConfig.sslConfigs = networkConfigs.sslConfigs
Object.assign(pnpmConfig, getDefaultAuthInfo(pnpmConfig.authConfig))
const defaultCreds = getDefaultCreds(pnpmConfig.authConfig)
pnpmConfig.configByUri = {
...networkConfigs.configByUri,
...defaultCreds ? { '': { creds: defaultCreds } } : {},
}
// 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>
for (const [key, value] of Object.entries(pnpmConfig.authConfig)) {
if (!key.endsWith('tokenHelper') && key !== 'tokenHelper') continue
if (!(key in userConfig) || userConfig[key] !== value) {
throw new PnpmError('TOKEN_HELPER_IN_PROJECT_CONFIG',
'tokenHelper must not be configured in project-level .npmrc',
{ hint: `The key "${key}" was found in project config. Move it to ~/.npmrc or the global pnpm auth.ini.` })
}
}
pnpmConfig.pnpmHomeDir = getDataDir({ env, platform: process.platform })
let globalDirRoot
if (pnpmConfig.globalDir) {

View File

@@ -1,17 +1,10 @@
import { PnpmError } from '@pnpm/error'
import type { BasicAuth, Creds, TokenHelper } from '@pnpm/types'
/** Authentication information of each registry in the rc file. */
export interface AuthInfo {
/** Parsed value of `_auth` of each registry in the rc file. */
authUserPass?: AuthUserPass
/** The value of `_authToken` of each registry in the rc file. */
authToken?: string
/** Parsed value of `tokenHelper` of each registry in the rc file. */
tokenHelper?: TokenHelper
}
export type { BasicAuth, Creds, TokenHelper }
/** Unparsed authentication information of each registry in the rc file. */
export interface AuthInfoInput {
export interface RawCreds {
/** Value of `_authToken` in the rc file. */
authToken?: string
/** Value of `_auth` in the rc file. */
@@ -24,39 +17,34 @@ export interface AuthInfoInput {
tokenHelper?: string
}
export function parseAuthInfo (input: AuthInfoInput): AuthInfo | undefined {
let authInfo: AuthInfo | undefined
export function parseCreds (input: RawCreds): Creds | undefined {
let parsedCreds: Creds | undefined
if (input.tokenHelper) {
authInfo = {
...authInfo,
parsedCreds = {
...parsedCreds,
tokenHelper: parseTokenHelper(input.tokenHelper),
}
}
if (input.authToken) {
authInfo = {
...authInfo,
parsedCreds = {
...parsedCreds,
authToken: input.authToken,
}
}
const authUserPass = getAuthUserPass(input)
if (authUserPass) {
authInfo = {
...authInfo,
authUserPass,
const basicAuth = parseBasicAuth(input)
if (basicAuth) {
parsedCreds = {
...parsedCreds,
basicAuth,
}
}
return authInfo
return parsedCreds
}
/** Parsed value of `_auth` of each registry in the rc file. */
export interface AuthUserPass {
username: string
password: string
}
/**
* Extract a pair of username and password from either a base64 encoded string
@@ -65,11 +53,11 @@ export interface AuthUserPass {
* The function input mirrors the rc file which has 3 properties to define username
* and password which are: `_auth`, `username`, and `_password`.
*/
function getAuthUserPass ({
function parseBasicAuth ({
authPairBase64,
authUsername,
authPassword,
}: Pick<AuthInfoInput, 'authPairBase64' | 'authUsername' | 'authPassword'>): AuthUserPass | undefined {
}: Pick<RawCreds, 'authPairBase64' | 'authUsername' | 'authPassword'>): BasicAuth | undefined {
if (authPairBase64) {
const pair = atob(authPairBase64)
const colonIndex = pair.indexOf(':')
@@ -96,8 +84,6 @@ export class AuthMissingSeparatorError extends PnpmError {
}
}
/** Parsed value of `tokenHelper` of each registry in the rc file. */
export type TokenHelper = [string, ...string[]]
/** Characters reserved for more advanced features in the future. */
const RESERVED_CHARACTERS = new Set(['$', '%', '`', '"', "'"])

View File

@@ -7,7 +7,6 @@ import { getNetworkConfigs, type NetworkConfigs } from '../src/getNetworkConfigs
test('without files', () => {
expect(getNetworkConfigs({})).toStrictEqual({
registries: {},
sslConfigs: {},
} as NetworkConfigs)
expect(getNetworkConfigs({
@@ -16,18 +15,15 @@ test('without files', () => {
registries: {
'@foo': 'https://example.com/foo',
},
sslConfigs: {},
} as NetworkConfigs)
expect(getNetworkConfigs({
'//example.com/foo:ca': 'some-ca',
})).toStrictEqual({
registries: {},
sslConfigs: {
configByUri: {
'//example.com/foo': {
ca: 'some-ca',
cert: '',
key: '',
tls: { ca: 'some-ca' },
},
},
} as NetworkConfigs)
@@ -36,10 +32,9 @@ test('without files', () => {
'//example.com/foo:cert': 'some-cert',
})).toStrictEqual({
registries: {},
sslConfigs: {
configByUri: {
'//example.com/foo': {
cert: 'some-cert',
key: '',
tls: { cert: 'some-cert' },
},
},
} as NetworkConfigs)
@@ -52,11 +47,9 @@ test('without files', () => {
registries: {
'@foo': 'https://example.com/foo',
},
sslConfigs: {
configByUri: {
'//example.com/foo': {
ca: 'some-ca',
cert: 'some-cert',
key: '',
tls: { ca: 'some-ca', cert: 'some-cert' },
},
},
} as NetworkConfigs)
@@ -76,17 +69,15 @@ test('with files', () => {
registries: {
'@foo': 'https://example.com/foo',
},
sslConfigs: {
configByUri: {
'//example.com/foo': {
ca: 'some-ca',
cert: 'some-cert',
key: '',
tls: { ca: 'some-ca', cert: 'some-cert' },
},
},
} as NetworkConfigs)
})
test('auth infos', () => {
test('auth and tls combined', () => {
expect(getNetworkConfigs({
'@foo:registry': 'https://example.com/foo',
'//example.com/foo:_authToken': 'example auth token',
@@ -94,12 +85,11 @@ test('auth infos', () => {
registries: {
'@foo': 'https://example.com/foo',
},
authInfos: {
configByUri: {
'//example.com/foo': {
authToken: 'example auth token',
creds: { authToken: 'example auth token' },
},
},
sslConfigs: {},
} as NetworkConfigs)
expect(getNetworkConfigs({
@@ -109,15 +99,16 @@ test('auth infos', () => {
registries: {
'@foo': 'https://example.com/foo',
},
authInfos: {
configByUri: {
'//example.com/foo': {
authUserPass: {
username: 'foo',
password: 'bar',
creds: {
basicAuth: {
username: 'foo',
password: 'bar',
},
},
},
},
sslConfigs: {},
} as NetworkConfigs)
expect(getNetworkConfigs({
@@ -128,15 +119,16 @@ test('auth infos', () => {
registries: {
'@foo': 'https://example.com/foo',
},
authInfos: {
configByUri: {
'//example.com/foo': {
authUserPass: {
username: 'foo',
password: 'bar',
creds: {
basicAuth: {
username: 'foo',
password: 'bar',
},
},
},
},
sslConfigs: {},
} as NetworkConfigs)
expect(getNetworkConfigs({
@@ -146,12 +138,25 @@ test('auth infos', () => {
registries: {
'@foo': 'https://example.com/foo',
},
authInfos: {
configByUri: {
'//example.com/foo': {
tokenHelper: ['node', './my-token-helper.cjs'],
creds: { tokenHelper: ['node', './my-token-helper.cjs'] },
},
},
} as NetworkConfigs)
expect(getNetworkConfigs({
'//example.com/foo:_authToken': 'token',
'//example.com/foo:cert': 'some-cert',
'//example.com/foo:key': 'some-key',
})).toStrictEqual({
registries: {},
configByUri: {
'//example.com/foo': {
creds: { authToken: 'token' },
tls: { cert: 'some-cert', key: 'some-key' },
},
},
sslConfigs: {},
} as NetworkConfigs)
})
@@ -163,6 +168,5 @@ test('unsupported key', () => {
registries: {
'@foo': 'https://example.com/foo',
},
sslConfigs: {},
} as NetworkConfigs)
})

View File

@@ -1017,8 +1017,7 @@ test('getConfig() should read inline SSL certificates from .npmrc', async () =>
})
// After processing, \n should be converted to actual newlines
expect(config.sslConfigs).toBeDefined()
expect(config.sslConfigs['//registry.example.com/']).toStrictEqual({
expect(config.configByUri['//registry.example.com/']?.tls).toMatchObject({
ca: inlineCa.replace(/\\n/g, '\n'),
cert: inlineCert.replace(/\\n/g, '\n'),
key: inlineKey.replace(/\\n/g, '\n'),

View File

@@ -1,107 +1,107 @@
import {
type AuthInfo,
AuthMissingSeparatorError,
parseAuthInfo,
type Creds,
parseCreds,
TokenHelperUnsupportedCharacterError,
} from '../src/parseAuthInfo.js'
} from '../src/parseCreds.js'
describe('parseAuthInfo', () => {
describe('parseCreds', () => {
test('empty object', () => {
expect(parseAuthInfo({})).toBeUndefined()
expect(parseCreds({})).toBeUndefined()
})
test('authToken', () => {
expect(parseAuthInfo({
expect(parseCreds({
authToken: 'example auth token',
})).toStrictEqual({
authToken: 'example auth token',
} as AuthInfo)
} as Creds)
})
test('authPairBase64', () => {
expect(parseAuthInfo({
expect(parseCreds({
authPairBase64: btoa('foo:bar'),
})).toStrictEqual({
authUserPass: {
basicAuth: {
username: 'foo',
password: 'bar',
},
} as AuthInfo)
} as Creds)
expect(parseAuthInfo({
expect(parseCreds({
authPairBase64: btoa('foo:bar:baz'),
})).toStrictEqual({
authUserPass: {
basicAuth: {
username: 'foo',
password: 'bar:baz',
},
} as AuthInfo)
} as Creds)
})
test('authPairBase64 must have a separator', () => {
expect(() => parseAuthInfo({
expect(() => parseCreds({
authPairBase64: btoa('foo'),
})).toThrow(new AuthMissingSeparatorError())
})
test('authUsername and authPassword', () => {
expect(parseAuthInfo({
expect(parseCreds({
authUsername: 'foo',
authPassword: btoa('bar'),
})).toStrictEqual({
authUserPass: {
basicAuth: {
username: 'foo',
password: 'bar',
},
} as AuthInfo)
} as Creds)
expect(parseAuthInfo({
expect(parseCreds({
authUsername: 'foo',
})).toBeUndefined()
expect(parseAuthInfo({
expect(parseCreds({
authPassword: 'bar',
})).toBeUndefined()
})
test('tokenHelper', () => {
expect(parseAuthInfo({
expect(parseCreds({
tokenHelper: 'example-token-helper --foo --bar baz',
})).toStrictEqual({
tokenHelper: ['example-token-helper', '--foo', '--bar', 'baz'],
} as AuthInfo)
} as Creds)
expect(parseAuthInfo({
expect(parseCreds({
tokenHelper: './example-token-helper.sh --foo --bar baz',
})).toStrictEqual({
tokenHelper: ['./example-token-helper.sh', '--foo', '--bar', 'baz'],
} as AuthInfo)
} as Creds)
expect(parseAuthInfo({
expect(parseCreds({
tokenHelper: 'node ./example-token-helper.js --foo --bar baz',
})).toStrictEqual({
tokenHelper: ['node', './example-token-helper.js', '--foo', '--bar', 'baz'],
} as AuthInfo)
} as Creds)
expect(parseAuthInfo({
expect(parseCreds({
tokenHelper: './example-token-helper.sh',
})).toStrictEqual({
tokenHelper: ['./example-token-helper.sh'],
} as AuthInfo)
} as Creds)
})
test('tokenHelper does not support environment variable', () => {
expect(() => parseAuthInfo({
expect(() => parseCreds({
tokenHelper: 'example-token-helper $MY_VAR',
})).toThrow(new TokenHelperUnsupportedCharacterError('$'))
})
test('tokenHelper does not support quotations', () => {
expect(() => parseAuthInfo({
expect(() => parseCreds({
tokenHelper: 'example-token-helper "hello world"',
})).toThrow(new TokenHelperUnsupportedCharacterError('"'))
expect(() => parseAuthInfo({
expect(() => parseCreds({
tokenHelper: "example-token-helper 'hello world'",
})).toThrow(new TokenHelperUnsupportedCharacterError("'"))
})

View File

@@ -19,12 +19,41 @@ export interface Registries {
[scope: string]: string
}
export interface SslConfig {
cert: string
key: string
/** Parsed value of `_auth` of each registry in the rc file. */
export interface BasicAuth {
username: string
password: string
}
/** Parsed value of `tokenHelper` of each registry in the rc file. */
export type TokenHelper = [string, ...string[]]
/** Per-registry authentication credentials. */
export interface Creds {
/** Parsed value of `_auth` of each registry in the rc file. */
basicAuth?: BasicAuth
/** The value of `_authToken` of each registry in the rc file. */
authToken?: string
/** Parsed value of `tokenHelper` of each registry in the rc file. */
tokenHelper?: TokenHelper
}
/** Per-registry TLS configuration. */
export interface TlsConfig {
/** Client certificate (PEM). */
cert?: string
/** Client private key (PEM). */
key?: string
/** Certificate authority (PEM). */
ca?: string
}
/** Per-registry configuration (credentials + TLS). */
export interface RegistryConfig {
creds?: Creds
tls?: TlsConfig
}
export type HoistedDependencies = Record<DepPath | ProjectId, Record<string, 'public' | 'private'>>
export type PkgResolutionId = string & { __brand: 'PkgResolutionId' }

View File

@@ -44,6 +44,7 @@
"corepack",
"corge",
"cowsay",
"Creds",
"cves",
"cwsay",
"cyclonedx",

View File

@@ -164,8 +164,7 @@ export type AuditOptions = Pick<UniversalOptions, 'dir'> & {
| 'dev'
| 'overrides'
| 'optional'
| 'userConfig'
| 'authConfig'
| 'configByUri'
| 'virtualStoreDirMaxLength'
| 'workspaceDir'
> & Pick<ConfigContext,
@@ -188,7 +187,7 @@ export async function handler (opts: AuditOptions): Promise<{ exitCode: number,
optionalDependencies: opts.optional !== false,
}
let auditReport!: AuditReport
const getAuthHeader = createGetAuthHeaderByURI({ allSettings: opts.authConfig, userSettings: opts.userConfig })
const getAuthHeader = createGetAuthHeaderByURI(opts.configByUri, opts.registries?.default)
try {
auditReport = await audit(lockfile, getAuthHeader, {
dispatcherOptions: {

View File

@@ -201,9 +201,8 @@ describe('plugin-commands-audit', () => {
...AUDIT_REGISTRY_OPTS,
dir: hasVulnerabilitiesDir,
rootProjectManifestDir: hasVulnerabilitiesDir,
authConfig: {
registry: AUDIT_REGISTRY,
[`${AUDIT_REGISTRY.replace(/^https?:/, '')}:_authToken`]: '123',
configByUri: {
'//audit.registry/': { creds: { authToken: '123' } },
},
})

View File

@@ -3,9 +3,6 @@ import { REGISTRY_MOCK_PORT } from '@pnpm/registry-mock'
const registries = {
default: 'https://registry.npmjs.org/',
}
const authConfig = {
registry: registries.default,
}
export const DEFAULT_OPTS = {
argv: {
original: [],
@@ -42,7 +39,7 @@ export const DEFAULT_OPTS = {
pnpmHomeDir: '',
preferWorkspacePackages: true,
proxy: undefined,
authConfig,
configByUri: {},
registries,
rootProjectManifestDir: '',
registry: registries.default,
@@ -65,9 +62,7 @@ export const AUDIT_REGISTRY_OPTS = {
registries: {
default: AUDIT_REGISTRY,
},
authConfig: {
registry: AUDIT_REGISTRY,
},
configByUri: {},
}
export const MOCK_REGISTRY = `http://localhost:${REGISTRY_MOCK_PORT}`
@@ -77,7 +72,5 @@ export const MOCK_REGISTRY_OPTS = {
registries: {
default: MOCK_REGISTRY,
},
authConfig: {
registry: MOCK_REGISTRY,
},
configByUri: {},
}

View File

@@ -36,7 +36,7 @@ export const DEFAULT_OPTS = {
pnpmHomeDir: '',
preferWorkspacePackages: true,
proxy: undefined,
authConfig: { registry: REGISTRY },
configByUri: {},
registries: { default: REGISTRY },
rootProjectManifestDir: '',
// registry: REGISTRY,

View File

@@ -36,7 +36,7 @@ export const DEFAULT_OPTS = {
pnpmHomeDir: '',
preferWorkspacePackages: true,
proxy: undefined,
authConfig: { registry: REGISTRY },
configByUri: {},
registries: { default: REGISTRY },
rootProjectManifestDir: '',
sort: true,

View File

@@ -164,7 +164,7 @@ export type OutdatedCommandOptions = {
| 'offline'
| 'optional'
| 'production'
| 'authConfig'
| 'configByUri'
| 'registries'
| 'strictSsl'
| 'tag'

View File

@@ -87,7 +87,7 @@ export async function handler (
}
const registry = pickRegistryForPackage(opts.registries, packageName)
const fetchFromRegistry = createFetchFromRegistry(opts)
const getAuthHeader = createGetAuthHeaderByURI({ allSettings: opts.authConfig ?? {}, userSettings: opts.userConfig ?? {} })
const getAuthHeader = createGetAuthHeaderByURI(opts.configByUri ?? {}, opts.registries?.default)
const fetchResult = await fetchMetadataFromFromRegistry(
{
fetch: fetchFromRegistry,

View File

@@ -38,7 +38,7 @@ export const DEFAULT_OPTS = {
pnpmHomeDir: '',
proxy: undefined,
preferWorkspacePackages: true,
authConfig: { registry: REGISTRY },
configByUri: {},
registries: { default: REGISTRY },
registry: REGISTRY,
rootProjectManifestDir: '',

View File

@@ -34,7 +34,7 @@ const OUTDATED_OPTIONS = {
global: false,
networkConcurrency: 16,
offline: false,
authConfig: { registry: REGISTRY_URL },
configByUri: {},
registries: { default: REGISTRY_URL },
strictSsl: false,
tag: 'latest',
@@ -83,7 +83,7 @@ test('pnpm outdated: show details (using the public registry to verify that full
...OUTDATED_OPTIONS,
dir: process.cwd(),
long: true,
authConfig: { registry: 'https://registry.npmjs.org/' },
configByUri: {},
registries: { default: 'https://registry.npmjs.org/' },
})

View File

@@ -41,7 +41,7 @@ export const DEFAULT_OPTS = {
pnpmHomeDir: '',
preferWorkspacePackages: true,
proxy: undefined,
authConfig: { registry: REGISTRY },
configByUri: {},
registries: { default: REGISTRY },
registry: REGISTRY,
rootProjectManifestDir: '',

View File

@@ -4,19 +4,19 @@ import {
createResolver,
type ResolveFunction,
} from '@pnpm/installing.client'
import type { DependencyManifest, PackageVersionPolicy } from '@pnpm/types'
import type { DependencyManifest, PackageVersionPolicy, RegistryConfig } from '@pnpm/types'
interface GetManifestOpts {
dir: string
lockfileDir: string
authConfig: object
configByUri: object
minimumReleaseAge?: number
minimumReleaseAgeExclude?: string[]
}
export type ManifestGetterOptions = Omit<ClientOptions, 'authConfig' | 'minimumReleaseAgeExclude' | 'storeIndex'>
export type ManifestGetterOptions = Omit<ClientOptions, 'configByUri' | 'minimumReleaseAgeExclude' | 'storeIndex'>
& GetManifestOpts
& { fullMetadata: boolean, authConfig: Record<string, string> }
& { fullMetadata: boolean, configByUri: Record<string, RegistryConfig> }
export function createManifestGetter (
opts: ManifestGetterOptions
@@ -27,7 +27,7 @@ export function createManifestGetter (
const { resolve } = createResolver({
...opts,
authConfig: opts.authConfig,
configByUri: opts.configByUri,
filterMetadata: false, // We need all the data from metadata for "outdated --long" to work.
strictPublishedByCheck: Boolean(opts.minimumReleaseAge),
})

View File

@@ -8,7 +8,7 @@ test('getManifest()', async () => {
const opts = {
dir: '',
lockfileDir: '',
authConfig: {},
configByUri: {},
}
const resolve: ResolveFunction = async function (_wantedPackage, _opts) {
@@ -52,7 +52,7 @@ test('getManifest() with minimumReleaseAge filters latest when too new', async (
const opts = {
dir: '',
lockfileDir: '',
authConfig: {},
configByUri: {},
minimumReleaseAge: 10080,
}
@@ -78,7 +78,7 @@ test('getManifest() does not convert non-latest specifiers', async () => {
const opts = {
dir: '',
lockfileDir: '',
authConfig: {},
configByUri: {},
}
const resolve = jest.fn<ResolveFunction>(async (wantedPackage) => {
@@ -105,7 +105,7 @@ test('getManifest() returns null for NO_MATCHING_VERSION when publishedBy is set
const opts = {
dir: '',
lockfileDir: '',
authConfig: {},
configByUri: {},
}
const publishedBy = new Date(Date.now() - 10080 * 60 * 1000)
@@ -129,7 +129,7 @@ test('getManifest() throws NO_MATCHING_VERSION when publishedBy is not set', asy
const opts = {
dir: '',
lockfileDir: '',
authConfig: {},
configByUri: {},
}
const resolve: ResolveFunction = jest.fn(async function () {
@@ -145,7 +145,7 @@ test('getManifest() with minimumReleaseAgeExclude', async () => {
const opts = {
dir: '',
lockfileDir: '',
authConfig: {},
configByUri: {},
}
const publishedBy = new Date(Date.now() - 10080 * 60 * 1000)

View File

@@ -272,7 +272,7 @@ async function installFromLockfile (
virtualStoreDirMaxLength: opts.virtualStoreDirMaxLength,
sideEffectsCacheRead: false,
sideEffectsCacheWrite: false,
authConfig: {},
configByUri: {},
unsafePerm: false,
userAgent: '',
packageManager: opts.packageManager ?? { name: 'pnpm', version: '' },

View File

@@ -64,7 +64,7 @@ export async function handler (
if (isExecutedByCorepack()) {
throw new PnpmError('CANT_SELF_UPDATE_IN_COREPACK', 'You should update pnpm with corepack')
}
const { resolve } = createResolver({ ...opts, authConfig: opts.authConfig })
const { resolve } = createResolver({ ...opts, configByUri: opts.configByUri })
const pkgName = 'pnpm'
const bareSpecifier = params[0] ?? 'latest'
const resolution = await resolve({ alias: pkgName, bareSpecifier }, {

View File

@@ -59,7 +59,7 @@ function prepareOptions (dir: string) {
workspaceConcurrency: 1,
extraEnv: {},
pnpmfile: '',
authConfig: {},
configByUri: {},
cacheDir: path.join(dir, '.cache'),
virtualStoreDirMaxLength: process.platform === 'win32' ? 60 : 120,
dir,

View File

@@ -17,14 +17,13 @@ export type NvmNodeCommandOptions = Pick<Config,
| 'localAddress'
| 'noProxy'
| 'nodeDownloadMirrors'
| 'authConfig'
| 'configByUri'
| 'strictSsl'
| 'storeDir'
| 'pnpmHomeDir'
> & Partial<Pick<Config,
| 'cacheDir'
| 'configDir'
| 'sslConfigs'
// Fields needed to forward opts to add.handler for env use
| 'registries'
| 'lockfileDir'

View File

@@ -18,7 +18,7 @@ test('env use calls pnpm add with the correct arguments', async () => {
cacheDir: '/tmp/cache',
global: true,
pnpmHomeDir: '/tmp/pnpm-home',
authConfig: {},
configByUri: {},
storeDir: '/tmp/store',
}, ['use', '18'])
@@ -33,7 +33,7 @@ test('env use passes lts specifier through unchanged', async () => {
bin: '/usr/local/bin',
global: true,
pnpmHomeDir: '/tmp/pnpm-home',
authConfig: {},
configByUri: {},
storeDir: '/tmp/store',
}, ['use', 'lts'])
@@ -48,7 +48,7 @@ test('env use passes codename specifier through unchanged', async () => {
bin: '/usr/local/bin',
global: true,
pnpmHomeDir: '/tmp/pnpm-home',
authConfig: {},
configByUri: {},
storeDir: '/tmp/store',
}, ['use', 'argon'])
@@ -64,7 +64,7 @@ test('fail if not run with --global', async () => {
bin: '/usr/local/bin',
global: false,
pnpmHomeDir: '/tmp/pnpm-home',
authConfig: {},
configByUri: {},
}, ['use', '18'])
).rejects.toEqual(new PnpmError('NOT_IMPLEMENTED_YET', '"pnpm env use <version>" can only be used with the "--global" option currently'))
@@ -78,7 +78,7 @@ test('fail if there is no global bin directory', async () => {
bin: undefined,
global: true,
pnpmHomeDir: '/tmp/pnpm-home',
authConfig: {},
configByUri: {},
}, ['use', 'lts'])
).rejects.toEqual(new PnpmError('CANNOT_MANAGE_NODE', 'Unable to manage Node.js because pnpm was not installed using the standalone installation script'))

View File

@@ -103,7 +103,7 @@ export async function handler (
const catalogResolver = resolveFromCatalog.bind(null, opts.catalogs ?? {})
const { resolve } = createResolver({
...opts,
authConfig: opts.authConfig,
configByUri: opts.configByUri,
fullMetadata,
filterMetadata: fullMetadata,
retry: {

View File

@@ -43,7 +43,7 @@ export const DEFAULT_OPTS = {
pnpmHomeDir: '',
preferWorkspacePackages: true,
proxy: undefined,
authConfig: { registry: REGISTRY_URL },
configByUri: {},
rootProjectManifestDir: '',
registries: { default: REGISTRY_URL },
registry: REGISTRY_URL,
@@ -84,7 +84,7 @@ export const DLX_DEFAULT_OPTS = {
pnpmfile: ['.pnpmfile.cjs'],
pnpmHomeDir: '',
preferWorkspacePackages: true,
authConfig: { registry: REGISTRY_URL },
configByUri: {},
registries: {
default: REGISTRY_URL,
},

View File

@@ -13,23 +13,21 @@ import {
type ResolverFactoryOptions,
} from '@pnpm/resolving.default-resolver'
import type { StoreIndex } from '@pnpm/store.index'
import type { SslConfig } from '@pnpm/types'
import type { RegistryConfig } from '@pnpm/types'
export type { ResolveFunction }
export type ClientOptions = {
authConfig: Record<string, string>
configByUri: Record<string, RegistryConfig>
customResolvers?: CustomResolver[]
customFetchers?: CustomFetcher[]
ignoreScripts?: boolean
sslConfigs?: Record<string, SslConfig>
retry?: RetryTimeoutOptions
storeIndex: StoreIndex
timeout?: number
nodeDownloadMirrors?: Record<string, string>
unsafePerm?: boolean
userAgent?: string
userConfig?: Record<string, string>
gitShallowHosts?: string[]
resolveSymlinksInInjectedDirs?: boolean
includeOnlyPackageFiles?: boolean
@@ -45,7 +43,7 @@ export interface Client {
export function createClient (opts: ClientOptions): Client {
const fetchFromRegistry = createFetchFromRegistry(opts)
const getAuthHeader = createGetAuthHeaderByURI({ allSettings: opts.authConfig, userSettings: opts.userConfig })
const getAuthHeader = createGetAuthHeaderByURI(opts.configByUri, opts.registries?.default)
const { resolve, clearCache: clearResolutionCache } = _createResolver(fetchFromRegistry, getAuthHeader, { ...opts, customResolvers: opts.customResolvers })
return {
@@ -57,7 +55,7 @@ export function createClient (opts: ClientOptions): Client {
export function createResolver (opts: Omit<ClientOptions, 'storeIndex'>): { resolve: ResolveFunction, clearCache: () => void } {
const fetchFromRegistry = createFetchFromRegistry(opts)
const getAuthHeader = createGetAuthHeaderByURI({ allSettings: opts.authConfig, userSettings: opts.userConfig })
const getAuthHeader = createGetAuthHeaderByURI(opts.configByUri, opts.registries?.default)
return _createResolver(fetchFromRegistry, getAuthHeader, { ...opts, customResolvers: opts.customResolvers })
}

View File

@@ -11,7 +11,7 @@ test('createClient()', () => {
const storeIndex = new StoreIndex('.store')
storeIndexes.push(storeIndex)
const client = createClient({
authConfig: {},
configByUri: {},
cacheDir: '',
registries: {
default: 'https://reigstry.npmjs.org/',
@@ -24,7 +24,7 @@ test('createClient()', () => {
test('createResolver()', () => {
const { resolve } = createResolver({
authConfig: {},
configByUri: {},
cacheDir: '',
registries: {
default: 'https://reigstry.npmjs.org/',

View File

@@ -409,10 +409,7 @@ export async function recursive (
saveExact: typeof localConfig.saveExact === 'boolean' ? localConfig.saveExact : opts.saveExact,
savePrefix: typeof localConfig.savePrefix === 'string' ? localConfig.savePrefix : opts.savePrefix,
}),
authConfig: {
...installOpts.authConfig,
...localConfig,
},
configByUri: installOpts.configByUri,
storeController: store.ctrl,
}
)

View File

@@ -32,7 +32,7 @@ const DEFAULT_OPTIONS = {
preferWorkspacePackages: true,
pnpmfile: ['.pnpmfile.cjs'],
pnpmHomeDir: '',
authConfig: { registry: REGISTRY_URL },
configByUri: {},
registries: {
default: REGISTRY_URL,
},

View File

@@ -11,10 +11,7 @@ import { DEFAULT_OPTS } from './utils/index.js'
// This must be a function because some of its values depend on CWD
const createOptions = (jsr: string = 'https://npm.jsr.io/') => ({
...DEFAULT_OPTS,
authConfig: {
...DEFAULT_OPTS.authConfig,
'@jsr:registry': jsr,
},
configByUri: {},
registries: {
...DEFAULT_OPTS.registries,
'@jsr': jsr,

View File

@@ -31,7 +31,7 @@ const DEFAULT_OPTIONS = {
preferWorkspacePackages: true,
pnpmfile: ['.pnpmfile.cjs'],
pnpmHomeDir: '',
authConfig: { registry: REGISTRY_URL },
configByUri: {},
registries: {
default: REGISTRY_URL,
},

View File

@@ -33,7 +33,7 @@ const DEFAULT_OPTS = {
preferWorkspacePackages: true,
proxy: undefined,
pnpmHomeDir: '',
authConfig: { registry: REGISTRY },
configByUri: {},
registries: { default: REGISTRY },
registry: REGISTRY,
rootProjectManifestDir: '',

View File

@@ -31,7 +31,7 @@ const DEFAULT_OPTS = {
preferWorkspacePackages: true,
proxy: undefined,
pnpmHomeDir: '',
authConfig: { registry: REGISTRY },
configByUri: {},
registries: { default: REGISTRY },
registry: REGISTRY,
rootProjectManifestDir: '',

View File

@@ -28,7 +28,7 @@ const DEFAULT_OPTIONS = {
pnpmfile: ['.pnpmfile.cjs'],
pnpmHomeDir: '',
preferWorkspacePackages: true,
authConfig: { registry: REGISTRY_URL },
configByUri: {},
registries: {
default: REGISTRY_URL,
},

View File

@@ -31,7 +31,7 @@ const DEFAULT_OPTIONS = {
pnpmfile: ['.pnpmfile.cjs'],
pnpmHomeDir: '',
preferWorkspacePackages: true,
authConfig: { registry: REGISTRY_URL },
configByUri: {},
registries: {
default: REGISTRY_URL,
},

View File

@@ -85,7 +85,7 @@ test('saveCatalogName works with different protocols', async () => {
.reply(200)
const options = createOptions()
options.registries['@jsr'] = options.authConfig['@jsr:registry'] = 'https://npm.jsr.io/'
options.registries['@jsr'] = 'https://npm.jsr.io/'
await add.handler(options, [
'@pnpm.e2e/foo@100.1.0',
'jsr:@rus/greet@0.0.3',

View File

@@ -35,7 +35,7 @@ const DEFAULT_OPTIONS = {
pnpmfile: ['.pnpmfile.cjs'],
pnpmHomeDir: '',
preferWorkspacePackages: true,
authConfig: { registry: REGISTRY_URL },
configByUri: {},
registries: {
default: REGISTRY_URL,
},

View File

@@ -33,7 +33,7 @@ const DEFAULT_OPTIONS = {
pnpmfile: ['.pnpmfile.cjs'],
pnpmHomeDir: '',
preferWorkspacePackages: true,
authConfig: { registry: REGISTRY_URL },
configByUri: {},
registries: {
default: REGISTRY_URL,
},

View File

@@ -12,10 +12,7 @@ import { DEFAULT_OPTS } from '../utils/index.js'
// This must be a function because some of its values depend on CWD
const createOptions = (jsr: string = DEFAULT_OPTS.registry) => ({
...DEFAULT_OPTS,
authConfig: {
...DEFAULT_OPTS.authConfig,
'@jsr:registry': jsr,
},
configByUri: {},
registries: {
...DEFAULT_OPTS.registries,
'@jsr': jsr,

View File

@@ -40,7 +40,7 @@ export const DEFAULT_OPTS = {
pnpmHomeDir: '',
preferWorkspacePackages: true,
proxy: undefined,
authConfig: { registry: REGISTRY },
configByUri: {},
registries: { default: REGISTRY },
registry: REGISTRY,
rootProjectManifestDir: '',

View File

@@ -20,6 +20,7 @@ import type {
PeerDependencyRules,
ReadPackageHook,
Registries,
RegistryConfig,
SupportedArchitectures,
TrustPolicy,
} from '@pnpm/types'
@@ -77,7 +78,7 @@ export interface StrictInstallOptions {
depth: number
lockfileDir: string
modulesDir: string
authConfig: Record<string, any> // eslint-disable-line @typescript-eslint/no-explicit-any
configByUri: Record<string, RegistryConfig>
verifyStoreIntegrity: boolean
engineStrict: boolean
allowBuilds?: Record<string, boolean | string>
@@ -239,7 +240,7 @@ const defaults = (opts: InstallOptions): StrictInstallOptions => {
preserveWorkspaceProtocol: true,
pruneLockfileImporters: false,
pruneStore: false,
authConfig: {},
configByUri: {},
registries: DEFAULT_REGISTRIES,
resolutionMode: 'highest',
saveWorkspaceProtocol: 'rolling',
@@ -335,7 +336,6 @@ export function extendOptions (
extendedOpts.userAgent = `${extendedOpts.packageManager.name}/${extendedOpts.packageManager.version} ${extendedOpts.userAgent}`
}
extendedOpts.registries = normalizeRegistries(extendedOpts.registries)
extendedOpts.authConfig['registry'] = extendedOpts.registries.default
if (extendedOpts.enableGlobalVirtualStore) {
if (extendedOpts.virtualStoreDir == null) {
extendedOpts.virtualStoreDir = path.join(extendedOpts.storeDir, 'links')

View File

@@ -3,6 +3,7 @@ import path from 'node:path'
import { addDependenciesToPackage, install } from '@pnpm/installing.deps-installer'
import { prepareEmpty } from '@pnpm/prepare'
import { addUser, REGISTRY_MOCK_PORT } from '@pnpm/registry-mock'
import type { RegistryConfig } from '@pnpm/types'
import { rimrafSync } from '@zkochan/rimraf'
import { testDefaults } from '../utils/index.js'
@@ -18,14 +19,13 @@ test('a package that need authentication', async () => {
username: 'foo',
})
let authConfig = {
[`//localhost:${REGISTRY_MOCK_PORT}/:_authToken`]: data.token,
registry: `http://localhost:${REGISTRY_MOCK_PORT}/`,
let configByUri: Record<string, RegistryConfig> = {
[`//localhost:${REGISTRY_MOCK_PORT}/`]: { creds: { authToken: data.token } },
}
const { updatedManifest: manifest } = await addDependenciesToPackage({}, ['@pnpm.e2e/needs-auth'], testDefaults({}, {
authConfig,
configByUri,
}, {
authConfig,
configByUri,
}))
project.has('@pnpm.e2e/needs-auth')
@@ -35,15 +35,14 @@ test('a package that need authentication', async () => {
rimrafSync('node_modules')
rimrafSync(path.join('..', '.store'))
authConfig = {
[`//localhost:${REGISTRY_MOCK_PORT}/:_authToken`]: data.token,
registry: 'https://registry.npmjs.org/',
configByUri = {
[`//localhost:${REGISTRY_MOCK_PORT}/`]: { creds: { authToken: data.token } },
}
await addDependenciesToPackage(manifest, ['@pnpm.e2e/needs-auth'], testDefaults({}, {
authConfig,
configByUri,
registry: 'https://registry.npmjs.org/',
}, {
authConfig,
configByUri,
}))
project.has('@pnpm.e2e/needs-auth')
@@ -58,16 +57,13 @@ test('installing a package that need authentication, using password', async () =
username: 'foo',
})
const encodedPassword = Buffer.from('bar').toString('base64')
const authConfig = {
[`//localhost:${REGISTRY_MOCK_PORT}/:_password`]: encodedPassword,
[`//localhost:${REGISTRY_MOCK_PORT}/:username`]: 'foo',
registry: `http://localhost:${REGISTRY_MOCK_PORT}/`,
const configByUri: Record<string, RegistryConfig> = {
[`//localhost:${REGISTRY_MOCK_PORT}/`]: { creds: { basicAuth: { username: 'foo', password: 'bar' } } },
}
await addDependenciesToPackage({}, ['@pnpm.e2e/needs-auth'], testDefaults({}, {
authConfig,
configByUri,
}, {
authConfig,
configByUri,
}))
project.has('@pnpm.e2e/needs-auth')
@@ -82,14 +78,13 @@ test('a package that need authentication, legacy way', async () => {
username: 'foo',
})
const authConfig = {
_auth: 'Zm9vOmJhcg==', // base64 encoded foo:bar
registry: `http://localhost:${REGISTRY_MOCK_PORT}`,
const configByUri: Record<string, RegistryConfig> = {
'': { creds: { basicAuth: { username: 'foo', password: 'bar' } } },
}
await addDependenciesToPackage({}, ['@pnpm.e2e/needs-auth'], testDefaults({}, {
authConfig,
configByUri,
}, {
authConfig,
configByUri,
}))
project.has('@pnpm.e2e/needs-auth')
@@ -104,16 +99,19 @@ test('a scoped package that need authentication specific to scope', async () =>
username: 'foo',
})
const authConfig = {
[`//localhost:${REGISTRY_MOCK_PORT}/:_authToken`]: data.token,
'@private:registry': `http://localhost:${REGISTRY_MOCK_PORT}/`,
registry: 'https://registry.npmjs.org/',
const configByUri: Record<string, RegistryConfig> = {
[`//localhost:${REGISTRY_MOCK_PORT}/`]: { creds: { authToken: data.token } },
}
let opts = testDefaults({}, {
authConfig,
let opts = testDefaults({
registries: {
default: 'https://registry.npmjs.org/',
'@private': `http://localhost:${REGISTRY_MOCK_PORT}/`,
},
}, {
configByUri,
registry: 'https://registry.npmjs.org/',
}, {
authConfig,
configByUri,
})
const { updatedManifest: manifest } = await addDependenciesToPackage({}, ['@private/foo'], opts)
@@ -124,11 +122,16 @@ test('a scoped package that need authentication specific to scope', async () =>
rimrafSync(path.join('..', '.store'))
// Recreating options to have a new storeController with clean cache
opts = testDefaults({}, {
authConfig,
opts = testDefaults({
registries: {
default: 'https://registry.npmjs.org/',
'@private': `http://localhost:${REGISTRY_MOCK_PORT}/`,
},
}, {
configByUri,
registry: 'https://registry.npmjs.org/',
}, {
authConfig,
configByUri,
})
await addDependenciesToPackage(manifest, ['@private/foo'], opts)
@@ -144,16 +147,19 @@ test('a scoped package that need legacy authentication specific to scope', async
username: 'foo',
})
const authConfig = {
[`//localhost:${REGISTRY_MOCK_PORT}/:_auth`]: 'Zm9vOmJhcg==', // base64 encoded foo:bar
'@private:registry': `http://localhost:${REGISTRY_MOCK_PORT}/`,
registry: 'https://registry.npmjs.org/',
const configByUri: Record<string, RegistryConfig> = {
[`//localhost:${REGISTRY_MOCK_PORT}/`]: { creds: { basicAuth: { username: 'foo', password: 'bar' } } },
}
let opts = testDefaults({}, {
authConfig,
let opts = testDefaults({
registries: {
default: 'https://registry.npmjs.org/',
'@private': `http://localhost:${REGISTRY_MOCK_PORT}/`,
},
}, {
configByUri,
registry: 'https://registry.npmjs.org/',
}, {
authConfig,
configByUri,
})
const { updatedManifest: manifest } = await addDependenciesToPackage({}, ['@private/foo'], opts)
@@ -164,11 +170,16 @@ test('a scoped package that need legacy authentication specific to scope', async
rimrafSync(path.join('..', '.store'))
// Recreating options to have a new storeController with clean cache
opts = testDefaults({}, {
authConfig,
opts = testDefaults({
registries: {
default: 'https://registry.npmjs.org/',
'@private': `http://localhost:${REGISTRY_MOCK_PORT}/`,
},
}, {
configByUri,
registry: 'https://registry.npmjs.org/',
}, {
authConfig,
configByUri,
})
await addDependenciesToPackage(manifest, ['@private/foo'], opts)
@@ -184,19 +195,18 @@ skipOnNode17('a package that need authentication reuses authorization tokens for
username: 'foo',
})
const authConfig = {
[`//127.0.0.1:${REGISTRY_MOCK_PORT}/:_authToken`]: data.token,
registry: `http://127.0.0.1:${REGISTRY_MOCK_PORT}`,
const configByUri: Record<string, RegistryConfig> = {
[`//127.0.0.1:${REGISTRY_MOCK_PORT}/`]: { creds: { authToken: data.token } },
}
await addDependenciesToPackage({}, ['@pnpm.e2e/needs-auth'], testDefaults({
registries: {
default: `http://127.0.0.1:${REGISTRY_MOCK_PORT}`,
},
}, {
authConfig,
configByUri,
registry: `http://127.0.0.1:${REGISTRY_MOCK_PORT}`,
}, {
authConfig,
configByUri,
}))
project.has('@pnpm.e2e/needs-auth')
@@ -211,19 +221,18 @@ skipOnNode17('a package that need authentication reuses authorization tokens for
username: 'foo',
})
const authConfig = {
[`//127.0.0.1:${REGISTRY_MOCK_PORT}/:_authToken`]: data.token,
registry: `http://127.0.0.1:${REGISTRY_MOCK_PORT}`,
const configByUri: Record<string, RegistryConfig> = {
[`//127.0.0.1:${REGISTRY_MOCK_PORT}/`]: { creds: { authToken: data.token } },
}
let opts = testDefaults({
registries: {
default: `http://127.0.0.1:${REGISTRY_MOCK_PORT}`,
},
}, {
authConfig,
configByUri,
registry: `http://127.0.0.1:${REGISTRY_MOCK_PORT}`,
}, {
authConfig,
configByUri,
})
const { updatedManifest: manifest } = await addDependenciesToPackage({}, ['@pnpm.e2e/needs-auth'], opts)
@@ -238,10 +247,10 @@ skipOnNode17('a package that need authentication reuses authorization tokens for
default: `http://127.0.0.1:${REGISTRY_MOCK_PORT}`,
},
}, {
authConfig,
configByUri,
registry: `http://127.0.0.1:${REGISTRY_MOCK_PORT}`,
}, {
authConfig,
configByUri,
})
await install(manifest, opts)

View File

@@ -77,6 +77,7 @@ import {
type ProjectManifest,
type ProjectRootDir,
type Registries,
type RegistryConfig,
type SupportedArchitectures,
} from '@pnpm/types'
import { symlinkAllModules } from '@pnpm/worker'
@@ -160,7 +161,7 @@ export interface HeadlessOptions {
disableRelinkLocalDirDeps?: boolean
force: boolean
storeDir: string
authConfig: object
configByUri: Record<string, RegistryConfig>
unsafePerm: boolean
userAgent: string
registries: Registries
@@ -234,7 +235,7 @@ export async function headlessInstall (opts: HeadlessOptions): Promise<Installat
extraNodePaths: opts.extraNodePaths,
preferSymlinkedExecutables: opts.preferSymlinkedExecutables,
extraEnv: opts.extraEnv,
authConfig: opts.authConfig,
configByUri: opts.configByUri,
resolveSymlinksInInjectedDirs: opts.resolveSymlinksInInjectedDirs,
scriptsPrependNodePath: opts.scriptsPrependNodePath,
scriptShell: opts.scriptShell,

View File

@@ -10,7 +10,7 @@ import { toLockfileResolution } from '@pnpm/lockfile.utils'
import { createGetAuthHeaderByURI } from '@pnpm/network.auth-header'
import { createFetchFromRegistry, type CreateFetchFromRegistryOptions } from '@pnpm/network.fetch'
import { createNpmResolver, type ResolverFactoryOptions } from '@pnpm/resolving.npm-resolver'
import type { ConfigDependencies } from '@pnpm/types'
import type { ConfigDependencies, RegistryConfig } from '@pnpm/types'
import getNpmTarballUrl from 'get-npm-tarball-url'
import { installConfigDeps, type InstallConfigDepsOpts } from './installConfigDeps.js'
@@ -19,7 +19,7 @@ import { pruneEnvLockfile } from './pruneEnvLockfile.js'
export type ResolveAndInstallConfigDepsOpts = CreateFetchFromRegistryOptions & ResolverFactoryOptions & InstallConfigDepsOpts & {
rootDir: string
userConfig?: Record<string, string>
configByUri?: Record<string, RegistryConfig>
}
/**
@@ -98,9 +98,8 @@ export async function resolveAndInstallConfigDeps (
}
// Resolve missing deps
const userConfig = opts.userConfig ?? {}
const fetch = createFetchFromRegistry(opts)
const getAuthHeader = createGetAuthHeaderByURI({ allSettings: userConfig, userSettings: userConfig })
const getAuthHeader = createGetAuthHeaderByURI(opts.configByUri ?? {}, opts.registries?.default)
const { resolveFromNpm } = createNpmResolver(fetch, getAuthHeader, opts)
await Promise.all(depsToResolve.map(async ({ name, specifier }) => {

View File

@@ -12,14 +12,14 @@ import { createGetAuthHeaderByURI } from '@pnpm/network.auth-header'
import { createFetchFromRegistry, type CreateFetchFromRegistryOptions } from '@pnpm/network.fetch'
import { createNpmResolver, type ResolverFactoryOptions } from '@pnpm/resolving.npm-resolver'
import { parseWantedDependency } from '@pnpm/resolving.parse-wanted-dependency'
import type { ConfigDependencies, ConfigDependencySpecifiers } from '@pnpm/types'
import type { ConfigDependencies, ConfigDependencySpecifiers, RegistryConfig } from '@pnpm/types'
import { installConfigDeps, type InstallConfigDepsOpts } from './installConfigDeps.js'
export type ResolveConfigDepsOpts = CreateFetchFromRegistryOptions & ResolverFactoryOptions & InstallConfigDepsOpts & {
configDependencies?: ConfigDependencies
rootDir: string
userConfig?: Record<string, string>
configByUri?: Record<string, RegistryConfig>
}
export async function resolveConfigDeps (configDeps: string[], opts: ResolveConfigDepsOpts): Promise<void> {
@@ -28,7 +28,7 @@ export async function resolveConfigDeps (configDeps: string[], opts: ResolveConf
}
const fetch = createFetchFromRegistry(opts)
const getAuthHeader = createGetAuthHeaderByURI({ allSettings: opts.userConfig!, userSettings: opts.userConfig })
const getAuthHeader = createGetAuthHeaderByURI(opts.configByUri ?? {}, opts.registries?.default)
const { resolveFromNpm } = createNpmResolver(fetch, getAuthHeader, opts)
// Extract existing specifiers from configDependencies (handles both old and new formats)

View File

@@ -19,7 +19,6 @@ test('configuration dependency is resolved', async () => {
},
rootDir: process.cwd(),
cacheDir: path.resolve('cache'),
userConfig: {},
store: storeController,
storeDir,
})
@@ -55,7 +54,6 @@ test('fails with frozenLockfile', async () => {
},
rootDir: process.cwd(),
cacheDir: path.resolve('cache'),
userConfig: {},
store: storeController,
storeDir,
frozenLockfile: true,

View File

@@ -34,7 +34,7 @@ const topStoreIndex = new StoreIndex('.store')
storeIndexes.push(topStoreIndex)
const { resolve, fetchers } = createClient({
authConfig: {},
configByUri: {},
cacheDir: '.store',
storeDir: '.store',
registries,
@@ -45,7 +45,7 @@ function createFetchersForStore (storeDir: string) {
const si = new StoreIndex(storeDir)
storeIndexes.push(si)
return createClient({
authConfig: {},
configByUri: {},
cacheDir: storeDir,
storeDir,
registries,
@@ -591,7 +591,7 @@ test('fetchPackageToStore() does not cache errors', async () => {
const noRetryStoreIndex = new StoreIndex('.store')
storeIndexes.push(noRetryStoreIndex)
const noRetry = createClient({
authConfig: {},
configByUri: {},
retry: { retries: 0 },
cacheDir: '.pnpm',
storeDir: '.store',

View File

@@ -33,7 +33,8 @@
},
"dependencies": {
"@pnpm/config.nerf-dart": "catalog:",
"@pnpm/error": "workspace:*"
"@pnpm/error": "workspace:*",
"@pnpm/types": "workspace:*"
},
"devDependencies": {
"@pnpm/network.auth-header": "workspace:*",

View File

@@ -1,80 +1,54 @@
import { spawnSync } from 'node:child_process'
import fs from 'node:fs'
import path from 'node:path'
import { nerfDart } from '@pnpm/config.nerf-dart'
import { PnpmError } from '@pnpm/error'
import type { Creds, RegistryConfig, TokenHelper } from '@pnpm/types'
export function getAuthHeadersFromConfig (
{ allSettings, userSettings }: {
allSettings: Record<string, string>
userSettings: Record<string, string>
}
export function getAuthHeadersFromCreds (
configByUri: Record<string, RegistryConfig>,
defaultRegistry: string
): Record<string, string> {
const authHeaderValueByURI: Record<string, string> = {}
for (const [key, value] of Object.entries(allSettings)) {
const [uri, authType] = splitKey(key)
switch (authType) {
case '_authToken': {
authHeaderValueByURI[uri] = `Bearer ${value}`
continue
}
case '_auth': {
authHeaderValueByURI[uri] = `Basic ${value}`
continue
}
case 'username': {
if (`${uri}:_password` in allSettings) {
authHeaderValueByURI[uri] = basicAuth(value, allSettings[`${uri}:_password`])
}
}
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
}
}
for (const [key, value] of Object.entries(userSettings)) {
const [uri, authType] = splitKey(key)
if (authType === 'tokenHelper') {
authHeaderValueByURI[uri] = loadToken(value, key)
const defaultConfig = configByUri['']
if (defaultConfig?.creds) {
const header = credsToHeader(defaultConfig.creds)
if (header) {
authHeaderValueByURI[defaultRegistry] = header
}
}
const registry = allSettings['registry'] ? nerfDart(allSettings['registry']) : '//registry.npmjs.org/'
if (userSettings['tokenHelper']) {
authHeaderValueByURI[registry] = loadToken(userSettings['tokenHelper'], 'tokenHelper')
} else if (allSettings['_authToken']) {
authHeaderValueByURI[registry] = `Bearer ${allSettings['_authToken']}`
} else if (allSettings['_auth']) {
authHeaderValueByURI[registry] = `Basic ${allSettings['_auth']}`
} else if (allSettings['_password'] && allSettings['username']) {
authHeaderValueByURI[registry] = basicAuth(allSettings['username'], allSettings['_password'])
}
return authHeaderValueByURI
}
function basicAuth (username: string, encodedPassword: string): string {
const password = Buffer.from(encodedPassword, 'base64').toString('utf8')
return `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`
function credsToHeader (creds?: Creds): string | undefined {
if (!creds) return undefined
if (creds.tokenHelper) {
return executeTokenHelper(creds.tokenHelper)
}
if (creds.authToken) {
return `Bearer ${creds.authToken}`
}
if (creds.basicAuth) {
return `Basic ${Buffer.from(`${creds.basicAuth.username}:${creds.basicAuth.password}`, 'utf8').toString('base64')}`
}
return undefined
}
function splitKey (key: string): string[] {
const index = key.lastIndexOf(':')
if (index === -1) {
return [key, '']
}
return [key.slice(0, index), key.slice(index + 1)]
}
export function loadToken (helperPath: string, settingName: string): string {
if (!path.isAbsolute(helperPath) || !fs.existsSync(helperPath)) {
throw new PnpmError('BAD_TOKEN_HELPER_PATH', `${settingName} must be an absolute path, without arguments`)
}
const spawnResult = spawnSync(helperPath, { shell: true })
function executeTokenHelper (tokenHelper: TokenHelper): string {
const [cmd, ...args] = tokenHelper
const spawnResult = spawnSync(cmd, args, { stdio: 'pipe' })
if (spawnResult.status !== 0) {
throw new PnpmError('TOKEN_HELPER_ERROR_STATUS', `Error running "${helperPath}" as a token helper, configured as ${settingName}. Exit code ${spawnResult.status?.toString() ?? ''}`)
throw new PnpmError('TOKEN_HELPER_ERROR_STATUS', `Error running "${cmd}" as a token helper. Exit code ${spawnResult.status?.toString() ?? ''}`)
}
const token = spawnResult.stdout.toString('utf8').trimEnd()
if (!token) {
throw new PnpmError('TOKEN_HELPER_EMPTY_TOKEN', `Token helper "${helperPath}", configured as ${settingName}, returned an empty token`)
throw new PnpmError('TOKEN_HELPER_EMPTY_TOKEN', `Token helper "${cmd}" returned an empty token`)
}
// If the token already contains an auth scheme (e.g. "Bearer ...", "Basic ..."),
// return it as-is.

View File

@@ -1,22 +1,15 @@
import { nerfDart } from '@pnpm/config.nerf-dart'
import type { RegistryConfig } from '@pnpm/types'
import { getAuthHeadersFromConfig, loadToken } from './getAuthHeadersFromConfig.js'
import { getAuthHeadersFromCreds } from './getAuthHeadersFromConfig.js'
import { removePort } from './helpers/removePort.js'
export {
loadToken,
}
export function createGetAuthHeaderByURI (
opts: {
allSettings: Record<string, string>
userSettings?: Record<string, string>
}
configByUri: Record<string, RegistryConfig>,
defaultRegistry?: string
): (uri: string) => string | undefined {
const authHeaders = getAuthHeadersFromConfig({
allSettings: opts.allSettings,
userSettings: opts.userSettings ?? {},
})
const registry = defaultRegistry ? nerfDart(defaultRegistry) : '//registry.npmjs.org/'
const authHeaders = getAuthHeadersFromCreds(configByUri, registry)
if (Object.keys(authHeaders).length === 0) return (uri: string) => basicAuth(new URL(uri))
return getAuthHeaderByURI.bind(null, authHeaders, getMaxParts(Object.keys(authHeaders)))
}

View File

@@ -1,17 +1,14 @@
import { createGetAuthHeaderByURI } from '@pnpm/network.auth-header'
const opts = {
allSettings: {
'//reg.com/:_authToken': 'abc123',
'//reg.co/tarballs/:_authToken': 'xxx',
'//reg.gg:8888/:_authToken': '0000',
'//custom.domain.com/artifactory/api/npm/npm-virtual/:_authToken': 'xyz',
},
userSettings: {},
const configByUri = {
'//reg.com/': { creds: { authToken: 'abc123' } },
'//reg.co/tarballs/': { creds: { authToken: 'xxx' } },
'//reg.gg:8888/': { creds: { authToken: '0000' } },
'//custom.domain.com/artifactory/api/npm/npm-virtual/': { creds: { authToken: 'xyz' } },
}
test('getAuthHeaderByURI()', () => {
const getAuthHeaderByURI = createGetAuthHeaderByURI(opts)
const getAuthHeaderByURI = createGetAuthHeaderByURI(configByUri)
expect(getAuthHeaderByURI('https://reg.com/')).toBe('Bearer abc123')
expect(getAuthHeaderByURI('https://reg.com/foo/-/foo-1.0.0.tgz')).toBe('Bearer abc123')
expect(getAuthHeaderByURI('https://reg.com:8080/foo/-/foo-1.0.0.tgz')).toBe('Bearer abc123')
@@ -22,9 +19,7 @@ test('getAuthHeaderByURI()', () => {
})
test('getAuthHeaderByURI() basic auth without settings', () => {
const getAuthHeaderByURI = createGetAuthHeaderByURI({
allSettings: {},
})
const getAuthHeaderByURI = createGetAuthHeaderByURI({})
expect(getAuthHeaderByURI('https://user:secret@reg.io/')).toBe('Basic ' + btoa('user:secret'))
expect(getAuthHeaderByURI('https://user:@reg.io/')).toBe('Basic ' + btoa('user:'))
expect(getAuthHeaderByURI('https://:secret@reg.io/')).toBe('Basic ' + btoa(':secret'))
@@ -32,7 +27,7 @@ test('getAuthHeaderByURI() basic auth without settings', () => {
})
test('getAuthHeaderByURI() basic auth with settings', () => {
const getAuthHeaderByURI = createGetAuthHeaderByURI(opts)
const getAuthHeaderByURI = createGetAuthHeaderByURI(configByUri)
expect(getAuthHeaderByURI('https://user:secret@reg.com/')).toBe('Basic ' + btoa('user:secret'))
expect(getAuthHeaderByURI('https://user:secret@reg.com/foo/-/foo-1.0.0.tgz')).toBe('Basic ' + btoa('user:secret'))
expect(getAuthHeaderByURI('https://user:secret@reg.com:8080/foo/-/foo-1.0.0.tgz')).toBe('Basic ' + btoa('user:secret'))
@@ -43,7 +38,7 @@ test('getAuthHeaderByURI() basic auth with settings', () => {
})
test('getAuthHeaderByURI() https port 443 checks', () => {
const getAuthHeaderByURI = createGetAuthHeaderByURI(opts)
const getAuthHeaderByURI = createGetAuthHeaderByURI(configByUri)
expect(getAuthHeaderByURI('https://custom.domain.com:443/artifactory/api/npm/npm-virtual/')).toBe('Bearer xyz')
expect(getAuthHeaderByURI('https://custom.domain.com:443/artifactory/api/npm/')).toBeUndefined()
expect(getAuthHeaderByURI('https://custom.domain.com:443/artifactory/api/npm/-/@platform/device-utils-1.0.0.tgz')).toBeUndefined()
@@ -52,33 +47,39 @@ test('getAuthHeaderByURI() https port 443 checks', () => {
test('getAuthHeaderByURI() when default ports are specified', () => {
const getAuthHeaderByURI = createGetAuthHeaderByURI({
allSettings: {
'//reg.com/:_authToken': 'abc123',
},
userSettings: {},
'//reg.com/': { creds: { authToken: 'abc123' } },
})
expect(getAuthHeaderByURI('https://reg.com:443/')).toBe('Bearer abc123')
expect(getAuthHeaderByURI('http://reg.com:80/')).toBe('Bearer abc123')
})
test('returns undefined when the auth header is not found', () => {
expect(createGetAuthHeaderByURI({ allSettings: {}, userSettings: {} })('http://reg.com')).toBeUndefined()
expect(createGetAuthHeaderByURI({})('http://reg.com')).toBeUndefined()
})
test('getAuthHeaderByURI() when the registry has pathnames', () => {
const getAuthHeaderByURI = createGetAuthHeaderByURI({
allSettings: {
'//npm.pkg.github.com/pnpm/:_authToken': 'abc123',
},
userSettings: {},
'//npm.pkg.github.com/pnpm/': { creds: { authToken: 'abc123' } },
})
expect(getAuthHeaderByURI('https://npm.pkg.github.com/pnpm')).toBe('Bearer abc123')
expect(getAuthHeaderByURI('https://npm.pkg.github.com/pnpm/')).toBe('Bearer abc123')
expect(getAuthHeaderByURI('https://npm.pkg.github.com/pnpm/foo')).toBe('Bearer abc123')
expect(getAuthHeaderByURI('https://npm.pkg.github.com/pnpm/foo/')).toBe('Bearer abc123')
expect(getAuthHeaderByURI('https://npm.pkg.github.com/pnpm/foo/-/foo-1.0.0.tgz')).toBe('Bearer abc123')
expect(getAuthHeaderByURI('https://npm.pkg.github.com/pnpm/foo/-/foo-1.0.0.tgz')).toBe('Bearer abc123')
expect(getAuthHeaderByURI('https://npm.pkg.github.com/pnpm/foo/-/foo-1.0.0.tgz')).toBe('Bearer abc123')
expect(getAuthHeaderByURI('https://npm.pkg.github.com/pnpm/foo/-/foo-1.0.0.tgz')).toBe('Bearer abc123')
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' } } },
})
expect(getAuthHeaderByURI('https://reg.com/')).toBe('Basic ' + btoa('user:pass'))
})

View File

@@ -1,9 +1,7 @@
import os from 'node:os'
import path from 'node:path'
import { Buffer } from 'safe-buffer'
import { getAuthHeadersFromConfig } from '../src/getAuthHeadersFromConfig.js'
import { getAuthHeadersFromCreds } from '../src/getAuthHeadersFromConfig.js'
const osTokenHelper = {
linux: path.join(import.meta.dirname, 'utils/test-exec.js'),
@@ -28,126 +26,62 @@ const osErrorTokenHelper = {
// Only exception is win32, all others behave like linux
const osFamily = os.platform() === 'win32' ? 'win32' : 'linux'
describe('getAuthHeadersFromConfig()', () => {
it('should get settings', () => {
const allSettings = {
'//registry.npmjs.org/:_authToken': 'abc123',
'//registry.foobar.eu/:_password': encodeBase64('foobar'),
'//registry.foobar.eu/:username': 'foobar',
'//registry.hu/:_auth': 'foobar',
'//localhost:3000/:_auth': 'foobar',
}
const userSettings = {}
expect(getAuthHeadersFromConfig({ allSettings, userSettings })).toStrictEqual({
describe('getAuthHeadersFromCreds()', () => {
it('should convert auth token to Bearer header', () => {
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',
})
})
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==',
'//registry.hu/': 'Basic foobar',
'//localhost:3000/': 'Basic foobar',
})
})
describe('should get settings for the default registry', () => {
it('_auth', () => {
const allSettings = {
registry: 'https://reg.com/',
_auth: 'foobar',
}
expect(getAuthHeadersFromConfig({ allSettings, userSettings: {} })).toStrictEqual({
'//reg.com/': 'Basic foobar',
})
})
it('username/_password', () => {
const allSettings = {
registry: 'https://reg.com/',
username: 'foo',
_password: encodeBase64('bar'),
}
expect(getAuthHeadersFromConfig({ allSettings, userSettings: {} })).toStrictEqual({
'//reg.com/': `Basic ${encodeBase64('foo:bar')}`,
})
})
it('tokenHelper', () => {
const allSettings = {
registry: 'https://reg.com/',
}
const userSettings = {
tokenHelper: osTokenHelper[osFamily],
}
expect(getAuthHeadersFromConfig({ allSettings, userSettings })).toStrictEqual({
'//reg.com/': 'Bearer token-from-spawn',
})
})
it('only read token helper from user config', () => {
const allSettings = {
registry: 'https://reg.com/',
tokenHelper: osTokenHelper[osFamily],
}
expect(getAuthHeadersFromConfig({ allSettings, userSettings: {} })).toStrictEqual({})
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 get tokenHelper', () => {
const userSettings = {
'//registry.foobar.eu/:tokenHelper': osTokenHelper[osFamily],
}
expect(getAuthHeadersFromConfig({ allSettings: {}, userSettings })).toStrictEqual({
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',
})
})
it('should throw an error if the token helper is not an absolute path', () => {
expect(() => getAuthHeadersFromConfig({
allSettings: {},
userSettings: {
'//reg.com:tokenHelper': './utils/text-exec.js',
},
})).toThrow('must be an absolute path, without arguments')
})
it('should throw an error if the token helper is not an absolute path with args', () => {
expect(() => getAuthHeadersFromConfig({
allSettings: {},
userSettings: {
'//reg.com:tokenHelper': `${osTokenHelper[osFamily]} arg1`,
},
})).toThrow('must be an absolute path, without arguments')
})
it('should throw an error if the token helper fails', () => {
expect(() => getAuthHeadersFromConfig({
allSettings: {},
userSettings: {
'//reg.com:tokenHelper': osErrorTokenHelper[osFamily],
},
})).toThrow('Exit code')
})
it('only read token helper from user config', () => {
const allSettings = {
'//reg.com:tokenHelper': osTokenHelper[osFamily],
}
expect(getAuthHeadersFromConfig({ allSettings, userSettings: {} })).toStrictEqual({})
})
it('should prepend Bearer to raw token from tokenHelper', () => {
const userSettings = {
'//registry.foobar.eu/:tokenHelper': osRawTokenHelper[osFamily],
}
expect(getAuthHeadersFromConfig({ allSettings: {}, userSettings })).toStrictEqual({
const result = getAuthHeadersFromCreds({
'//registry.foobar.eu/': { creds: { tokenHelper: [osRawTokenHelper[osFamily]] } },
}, '//registry.npmjs.org/')
expect(result).toStrictEqual({
'//registry.foobar.eu/': 'Bearer raw-token-no-scheme',
})
})
it('should not modify token that already has an auth scheme', () => {
const userSettings = {
'//registry.foobar.eu/:tokenHelper': osTokenHelper[osFamily],
}
expect(getAuthHeadersFromConfig({ allSettings: {}, userSettings })).toStrictEqual({
'//registry.foobar.eu/': 'Bearer token-from-spawn',
})
it('should throw an error if the token helper fails', () => {
expect(() => getAuthHeadersFromCreds({
'//reg.com/': { creds: { tokenHelper: [osErrorTokenHelper[osFamily]] } },
}, '//registry.npmjs.org/')).toThrow('Exit code')
})
it('should throw an error if the token helper returns an empty token', () => {
expect(() => getAuthHeadersFromConfig({
allSettings: {},
userSettings: {
'//reg.com:tokenHelper': osEmptyTokenHelper[osFamily],
},
})).toThrow('returned an empty token')
expect(() => getAuthHeadersFromCreds({
'//reg.com/': { creds: { tokenHelper: [osEmptyTokenHelper[osFamily]] } },
}, '//registry.npmjs.org/')).toThrow('returned an empty token')
})
it('should return empty object when no auth infos', () => {
const result = getAuthHeadersFromCreds({}, '//registry.npmjs.org/')
expect(result).toStrictEqual({})
})
})
function encodeBase64 (s: string) {
return Buffer.from(s, 'utf8').toString('base64')
}

View File

@@ -11,6 +11,9 @@
"references": [
{
"path": "../../core/error"
},
{
"path": "../../core/types"
}
]
}

View File

@@ -4,7 +4,7 @@ import { URL } from 'node:url'
import { nerfDart } from '@pnpm/config.nerf-dart'
import { PnpmError } from '@pnpm/error'
import type { SslConfig } from '@pnpm/types'
import type { TlsConfig } from '@pnpm/types'
import { LRUCache } from 'lru-cache'
import { SocksClient } from 'socks'
import { Agent, type Dispatcher, ProxyAgent, setGlobalDispatcher } from 'undici'
@@ -38,6 +38,8 @@ const DISPATCHER_CACHE = new LRUCache<string, Dispatcher>({
},
})
export type ClientCertificates = Record<string, TlsConfig>
export interface DispatcherOptions {
ca?: string | string[] | Buffer
cert?: string | string[] | Buffer
@@ -49,7 +51,7 @@ export interface DispatcherOptions {
httpProxy?: string
httpsProxy?: string
noProxy?: boolean | string
clientCertificates?: Record<string, SslConfig>
clientCertificates?: ClientCertificates
}
/**
@@ -78,6 +80,15 @@ export function getDispatcher (uri: string, opts: DispatcherOptions): Dispatcher
return getNonProxyDispatcher(parsedUri, opts)
}
function hasClientCertificates (certs?: ClientCertificates): boolean {
if (!certs) return false
for (const uri in certs) {
const entry = certs[uri]
if (entry.cert || entry.key || entry.ca) return true
}
return false
}
function needsCustomDispatcher (opts: DispatcherOptions): boolean {
return Boolean(
opts.httpProxy ||
@@ -87,7 +98,7 @@ function needsCustomDispatcher (opts: DispatcherOptions): boolean {
opts.key ||
opts.localAddress ||
opts.strictSsl === false ||
opts.clientCertificates ||
hasClientCertificates(opts.clientCertificates) ||
opts.maxSockets
)
}

View File

@@ -1,9 +1,9 @@
import { URL } from 'node:url'
import type { FetchFromRegistry } from '@pnpm/fetching.types'
import type { SslConfig } from '@pnpm/types'
import type { RegistryConfig } from '@pnpm/types'
import { type DispatcherOptions, getDispatcher } from './dispatcher.js'
import { type ClientCertificates, type DispatcherOptions, getDispatcher } from './dispatcher.js'
import { fetch, isRedirect, type RequestInit } from './fetch.js'
const USER_AGENT = 'pnpm' // or maybe make it `${pkg.name}/${pkg.version} (+https://npm.im/${pkg.name})`
@@ -35,7 +35,7 @@ export type { DispatcherOptions }
export interface CreateFetchFromRegistryOptions extends DispatcherOptions {
userAgent?: string
sslConfigs?: Record<string, SslConfig>
configByUri?: Record<string, RegistryConfig>
}
export function createFetchFromRegistry (defaultOpts: CreateFetchFromRegistryOptions): FetchFromRegistry {
@@ -64,7 +64,7 @@ export function createFetchFromRegistry (defaultOpts: CreateFetchFromRegistryOpt
...defaultOpts,
...opts,
strictSsl: defaultOpts.strictSsl ?? true,
clientCertificates: defaultOpts.sslConfigs,
clientCertificates: extractTlsConfigs(defaultOpts.configByUri),
}
const response = await fetchWithDispatcher(urlObject, {
@@ -115,6 +115,18 @@ function getHeaders (
return headers
}
function extractTlsConfigs (configByUri?: Record<string, RegistryConfig>): ClientCertificates | undefined {
if (!configByUri) return undefined
let result: ClientCertificates | undefined
for (const [uri, config] of Object.entries(configByUri)) {
if (config.tls) {
result ??= {}
result[uri] = config.tls
}
}
return result
}
function resolveRedirectUrl (response: Response, currentUrl: URL): URL {
const location = response.headers.get('location')
if (!location) {

View File

@@ -136,16 +136,18 @@ test('fetch from registry with client certificate authentication', async () => {
await proxyServer.start()
const sslConfigs = {
const configByUri = {
[`//localhost:${randomPort}/`]: {
ca: fs.readFileSync(path.join(CERTS_DIR, 'ca-crt.pem'), 'utf8'),
cert: fs.readFileSync(path.join(CERTS_DIR, 'client-crt.pem'), 'utf8'),
key: fs.readFileSync(path.join(CERTS_DIR, 'client-key.pem'), 'utf8'),
tls: {
ca: fs.readFileSync(path.join(CERTS_DIR, 'ca-crt.pem'), 'utf8'),
cert: fs.readFileSync(path.join(CERTS_DIR, 'client-crt.pem'), 'utf8'),
key: fs.readFileSync(path.join(CERTS_DIR, 'client-key.pem'), 'utf8'),
},
},
}
const fetchFromRegistry = createFetchFromRegistry({
sslConfigs,
configByUri,
strictSsl: false,
})

View File

@@ -25,9 +25,7 @@ const f = fixtures(import.meta.dirname)
const basePatchOption = {
pnpmHomeDir: '',
authConfig: {
registry: `http://localhost:${REGISTRY_MOCK_PORT}/`,
},
configByUri: {},
registries: { default: `http://localhost:${REGISTRY_MOCK_PORT}/` },
userConfig: {},
virtualStoreDir: 'node_modules/.pnpm',

View File

@@ -38,7 +38,7 @@ export const DEFAULT_OPTS = {
pnpmHomeDir: '',
preferWorkspacePackages: true,
proxy: undefined,
authConfig: { registry: REGISTRY },
configByUri: {},
registries: { default: REGISTRY },
registry: REGISTRY,
rootProjectManifestDir: '',

3
pnpm-lock.yaml generated
View File

@@ -6761,6 +6761,9 @@ importers:
'@pnpm/error':
specifier: workspace:*
version: link:../../core/error
'@pnpm/types':
specifier: workspace:*
version: link:../../core/types
devDependencies:
'@pnpm/network.auth-header':
specifier: workspace:*

View File

@@ -27,7 +27,7 @@ export async function checkForUpdates (config: Config): Promise<void> {
const { resolve } = createResolver({
...config,
authConfig: config.authConfig,
configByUri: config.configByUri,
retry: {
retries: 0,
},

View File

@@ -95,7 +95,6 @@ export function help (): string {
export type PackOptions = Pick<UniversalOptions, 'dir'> & Pick<Config, 'catalogs'
| 'ignoreScripts'
| 'authConfig'
| 'embedReadme'
| 'packGzipLevel'
| 'nodeLinker'

View File

@@ -4,6 +4,7 @@ import type { Config } from '@pnpm/config.reader'
import { PnpmError } from '@pnpm/error'
import { globalInfo, globalWarn } from '@pnpm/logger'
import type { ExportedManifest } from '@pnpm/releasing.exportable-manifest'
import type { Creds, RegistryConfig } from '@pnpm/types'
import type { PublishOptions } from 'libnpmpublish'
import { displayError } from './displayError.js'
@@ -16,26 +17,8 @@ import { publishWithOtpHandling } from './otp.js'
import type { PackResult } from './pack.js'
import { allRegistryConfigKeys, type NormalizedRegistryUrl, parseSupportedRegistryUrl } from './registryConfigKeys.js'
type AuthConfigKey =
| 'authToken'
| 'authUserPass'
| 'tokenHelper'
type SslConfigKey =
| 'ca'
| 'cert'
| 'key'
type AuthSslConfigKey =
// default registry
| AuthConfigKey
| SslConfigKey
// other registries
| 'authInfos'
| 'sslConfigs'
export type PublishPackedPkgOptions = Pick<Config,
| AuthSslConfigKey
| 'configByUri'
| 'dryRun'
| 'fetchRetries'
| 'fetchRetryFactor'
@@ -76,7 +59,8 @@ export async function publishPackedPkg (
}
async function createPublishOptions (manifest: ExportedManifest, options: PublishPackedPkgOptions): Promise<PublishOptions> {
const { registry, auth, ssl } = findAuthSslInfo(manifest, options)
const { registry, config } = findRegistryInfo(manifest, options)
const { creds, tls } = config ?? {}
const {
access,
@@ -118,22 +102,15 @@ async function createPublishOptions (manifest: ExportedManifest, options: Publis
// always fall back to prompting the user for an OTP code, even when the user
// has no OTP set up.
authType: 'web',
ca: ssl?.ca,
cert: Array.isArray(ssl?.cert) ? ssl.cert.join('\n') : ssl?.cert,
key: ssl?.key,
ca: tls?.ca,
cert: tls?.cert,
key: tls?.key,
npmCommand: 'publish',
token: auth && extractToken(auth),
username: auth?.authUserPass?.username,
password: auth?.authUserPass?.password,
token: creds && extractToken(creds),
username: creds?.basicAuth?.username,
password: creds?.basicAuth?.password,
}
// This is necessary because getNetworkConfigs initialized them as { cert: '', key: '' }
// which may be a problem.
// The real fix is to change the type `SslConfig` into that of partial properties, but that
// is out of scope for now.
removeEmptyStringProperty(publishOptions, 'cert')
removeEmptyStringProperty(publishOptions, 'key')
if (registry) {
const oidcTokenProvenance = await fetchTokenAndProvenanceByOidcIfApplicable(publishOptions, manifest.name, registry, options)
publishOptions.token ??= oidcTokenProvenance?.authToken
@@ -145,26 +122,19 @@ async function createPublishOptions (manifest: ExportedManifest, options: Publis
return publishOptions
}
interface AuthSslInfo {
interface RegistryInfo {
registry: NormalizedRegistryUrl
auth: Pick<Config, AuthConfigKey>
ssl: Pick<Config, SslConfigKey>
config: RegistryConfig
}
/**
* Find auth and ssl information according to {@link https://docs.npmjs.com/cli/v10/configuring-npm/npmrc#auth-related-configuration}.
*
* The example `.npmrc` demonstrated inheritance.
* Find credentials and SSL info for a package's registry.
* Follows {@link https://docs.npmjs.com/cli/v10/configuring-npm/npmrc#auth-related-configuration}.
*/
function findAuthSslInfo (
function findRegistryInfo (
{ name }: ExportedManifest,
{
authInfos,
sslConfigs,
registries,
...defaultInfos
}: Pick<Config, AuthSslConfigKey | 'registries'>
): Partial<AuthSslInfo> {
{ configByUri, registries }: Pick<Config, 'configByUri' | 'registries'>
): Partial<RegistryInfo> {
// eslint-disable-next-line regexp/no-unused-capturing-group
const scopedMatches = /@(?<scope>[^/]+)\/(?<slug>[^/]+)/.exec(name)
@@ -181,45 +151,36 @@ function findAuthSslInfo (
longestConfigKey: initialRegistryConfigKey,
} = supportedRegistryInfo
const result: Partial<AuthSslInfo> = { registry }
let creds: Creds | undefined
let tls: RegistryConfig['tls'] = {}
for (const registryConfigKey of allRegistryConfigKeys(initialRegistryConfigKey)) {
const auth: Pick<Config, AuthConfigKey> | undefined = authInfos[registryConfigKey]
const ssl: Pick<Config, SslConfigKey> | undefined = sslConfigs[registryConfigKey]
result.auth ??= auth // old auth from longer path collectively overrides new auth from shorter path
result.ssl = {
...ssl,
...result.ssl, // old ssl from longer path individually overrides new ssl from shorter path
}
const entry = configByUri[registryConfigKey]
if (!entry) continue
// Auth from longer path collectively overrides shorter path
creds ??= entry.creds
// TLS from longer path individually overrides shorter path
tls = { ...entry.tls, ...tls }
}
if (
nonNormalizedRegistry !== registries.default &&
registry !== registries.default &&
registry !== parseSupportedRegistryUrl(registries.default)?.normalizedUrl
) {
return result
const isDefaultRegistry =
nonNormalizedRegistry === registries.default ||
registry === registries.default ||
registry === parseSupportedRegistryUrl(registries.default)?.normalizedUrl
if (isDefaultRegistry) {
creds ??= configByUri['']?.creds
}
return {
registry,
auth: result.auth ?? defaultInfos, // old auth from longer path collectively overrides default auth
ssl: {
...defaultInfos,
...result.ssl, // old ssl from longer path individually overrides default ssl
},
config: { creds, tls },
}
}
function extractToken ({
authToken,
tokenHelper,
}: {
authToken?: string
tokenHelper?: [string, ...string[]]
}): string | undefined {
}: Pick<Creds, 'authToken' | 'tokenHelper'>): string | undefined {
if (authToken) return authToken
if (tokenHelper) {
return executeTokenHelper(tokenHelper, { globalWarn })
@@ -341,12 +302,6 @@ function appendAuthOptionsForRegistry (targetPublishOptions: PublishOptions, reg
targetPublishOptions[`${registryConfigKey}:_password`] ??= targetPublishOptions.password && btoa(targetPublishOptions.password)
}
function removeEmptyStringProperty<Key extends string> (object: Partial<Record<Key, string>>, key: Key): void {
if (!object[key]) {
delete object[key]
}
}
function pruneUndefined (object: Record<string, unknown>): void {
for (const key in object) {
if (object[key] === undefined) {

View File

@@ -19,7 +19,7 @@ export type PublishRecursiveOpts = Required<Pick<Config,
| 'cacheDir'
| 'dir'
| 'pnpmHomeDir'
| 'authConfig'
| 'configByUri'
| 'registries'
| 'workspaceDir'
>> &
@@ -51,7 +51,6 @@ Partial<Pick<Config,
| 'strictSsl'
| 'unsafePerm'
| 'userAgent'
| 'userConfig'
| 'verifyStoreIntegrity'
>> &
Partial<Pick<ConfigContext,
@@ -70,8 +69,7 @@ export async function recursivePublish (
const pkgs = Object.values(opts.selectedProjectsGraph).map((wsPkg) => wsPkg.package)
const { resolve } = createResolver({
...opts,
authConfig: opts.authConfig,
userConfig: opts.userConfig,
configByUri: opts.configByUri,
retry: {
factor: opts.fetchRetryFactor,
maxTimeout: opts.fetchRetryMaxtimeout,

View File

@@ -40,7 +40,7 @@ export const DEFAULT_OPTS = {
pnpmHomeDir: '',
preferWorkspacePackages: true,
proxy: undefined,
authConfig: { registry: REGISTRY },
configByUri: {},
registries: { default: REGISTRY },
registry: REGISTRY,
rootProjectManifestDir: '',

View File

@@ -299,10 +299,7 @@ test('errors on fake registry', async () => {
const promise = publish.handler({
...DEFAULT_OPTS,
...await filterProjectsBySelectorObjectsFromDir(process.cwd(), []),
authConfig: {
...DEFAULT_OPTS.authConfig,
registry: fakeRegistry,
},
configByUri: {},
registries: {
...DEFAULT_OPTS.registries,
default: fakeRegistry,

View File

@@ -4,7 +4,7 @@ import { safeExeca as execa } from 'execa'
const REGISTRY = `http://localhost:${REGISTRY_MOCK_PORT}`
export const DEFAULT_OPTS = {
authInfos: {},
configByUri: {},
argv: {
original: [],
},
@@ -36,13 +36,11 @@ export const DEFAULT_OPTS = {
pnpmfile: ['./.pnpmfile.cjs'],
pnpmHomeDir: '',
proxy: undefined,
authConfig: { registry: REGISTRY },
registries: { default: REGISTRY },
registry: REGISTRY,
sort: true,
cacheDir: '../cache',
strictSsl: false,
sslConfigs: {},
userAgent: 'pnpm',
userConfig: {},
useRunningStoreServer: false,

View File

@@ -32,14 +32,13 @@ export function help (): string {
export type CatIndexCommandOptions = Pick<
Config,
| 'authConfig'
| 'configByUri'
| 'pnpmHomeDir'
| 'storeDir'
| 'lockfileDir'
| 'dir'
| 'registries'
| 'cacheDir'
| 'sslConfigs'
>
export async function handler (opts: CatIndexCommandOptions, params: string[]): Promise<string> {
@@ -70,7 +69,7 @@ export async function handler (opts: CatIndexCommandOptions, params: string[]):
})
const { resolve } = createResolver({
...opts,
authConfig: opts.authConfig,
configByUri: opts.configByUri,
})
const pkgSnapshot = await resolve(
{ alias, bareSpecifier },

View File

@@ -17,12 +17,9 @@ test('pnpm store add express@4.16.3', async () => {
cacheDir,
dir: process.cwd(),
pnpmHomeDir: '',
authConfig: {
registry: `http://localhost:${REGISTRY_MOCK_PORT}/`,
},
configByUri: {},
registries: { default: `http://localhost:${REGISTRY_MOCK_PORT}/` },
storeDir,
userConfig: {},
dlxCacheMaxAge: 0,
virtualStoreDirMaxLength: process.platform === 'win32' ? 60 : 120,
}, ['add', 'express@4.16.3'])
@@ -41,15 +38,12 @@ test('pnpm store add scoped package that uses not the standard registry', async
cacheDir,
dir: process.cwd(),
pnpmHomeDir: '',
authConfig: {
registry: 'https://registry.npmjs.org/',
},
configByUri: {},
registries: {
'@foo': `http://localhost:${REGISTRY_MOCK_PORT}/`,
default: 'https://registry.npmjs.org/',
},
storeDir,
userConfig: {},
dlxCacheMaxAge: 0,
virtualStoreDirMaxLength: process.platform === 'win32' ? 60 : 120,
}, ['add', '@foo/no-deps@1.0.0'])
@@ -71,15 +65,12 @@ test('should fail if some packages can not be added', async () => {
cacheDir,
dir: process.cwd(),
pnpmHomeDir: '',
authConfig: {
registry: 'https://registry.npmjs.org/',
},
configByUri: {},
registries: {
'@foo': `http://localhost:${REGISTRY_MOCK_PORT}/`,
default: 'https://registry.npmjs.org/',
},
storeDir,
userConfig: {},
dlxCacheMaxAge: 0,
virtualStoreDirMaxLength: process.platform === 'win32' ? 60 : 120,
}, ['add', '@pnpm/this-does-not-exist'])

View File

@@ -15,12 +15,9 @@ test('CLI prints the current store path', async () => {
cacheDir: path.resolve('cache'),
dir: process.cwd(),
pnpmHomeDir: '',
authConfig: {
registry: REGISTRY,
},
configByUri: {},
registries: { default: REGISTRY },
storeDir: '/home/example/.pnpm-store',
userConfig: {},
dlxCacheMaxAge: 0,
virtualStoreDirMaxLength: process.platform === 'win32' ? 60 : 120,
}, ['path'])
@@ -44,12 +41,9 @@ test('CLI prints the current store path when storeDir is relative', async () =>
dir: subpackageDir,
workspaceDir,
pnpmHomeDir: '',
authConfig: {
registry: REGISTRY,
},
configByUri: {},
registries: { default: REGISTRY },
storeDir: relativeStoreDir,
userConfig: {},
dlxCacheMaxAge: 0,
virtualStoreDirMaxLength: process.platform === 'win32' ? 60 : 120,
}, ['path'])

View File

@@ -47,13 +47,10 @@ test('remove unreferenced packages', async () => {
cacheDir,
dir: process.cwd(),
pnpmHomeDir: '',
authConfig: {
registry: REGISTRY,
},
configByUri: {},
registries: { default: REGISTRY },
reporter,
storeDir,
userConfig: {},
dlxCacheMaxAge: Infinity,
virtualStoreDirMaxLength: process.platform === 'win32' ? 60 : 120,
}, ['prune'])
@@ -72,13 +69,10 @@ test('remove unreferenced packages', async () => {
cacheDir,
dir: process.cwd(),
pnpmHomeDir: '',
authConfig: {
registry: REGISTRY,
},
configByUri: {},
registries: { default: REGISTRY },
reporter,
storeDir,
userConfig: {},
dlxCacheMaxAge: Infinity,
virtualStoreDirMaxLength: process.platform === 'win32' ? 60 : 120,
}, ['prune'])
@@ -120,13 +114,10 @@ test('prune outputs total size of removed files', async () => {
cacheDir,
dir: process.cwd(),
pnpmHomeDir: '',
authConfig: {
registry: REGISTRY,
},
configByUri: {},
registries: { default: REGISTRY },
reporter,
storeDir,
userConfig: {},
dlxCacheMaxAge: Infinity,
virtualStoreDirMaxLength: process.platform === 'win32' ? 60 : 120,
}, ['prune'])
@@ -158,13 +149,10 @@ test('remove packages that are used by project that no longer exist', async () =
cacheDir,
dir: process.cwd(),
pnpmHomeDir: '',
authConfig: {
registry: REGISTRY,
},
configByUri: {},
registries: { default: REGISTRY },
reporter,
storeDir,
userConfig: {},
dlxCacheMaxAge: Infinity,
virtualStoreDirMaxLength: process.platform === 'win32' ? 60 : 120,
}, ['prune'])
@@ -206,12 +194,9 @@ test('keep dependencies used by others', async () => {
cacheDir,
dir: process.cwd(),
pnpmHomeDir: '',
authConfig: {
registry: REGISTRY,
},
configByUri: {},
registries: { default: REGISTRY },
storeDir,
userConfig: {},
dlxCacheMaxAge: Infinity,
virtualStoreDirMaxLength: process.platform === 'win32' ? 60 : 120,
}, ['prune'])
@@ -232,12 +217,9 @@ test('keep dependency used by package', async () => {
cacheDir,
dir: process.cwd(),
pnpmHomeDir: '',
authConfig: {
registry: REGISTRY,
},
configByUri: {},
registries: { default: REGISTRY },
storeDir,
userConfig: {},
dlxCacheMaxAge: Infinity,
virtualStoreDirMaxLength: process.platform === 'win32' ? 60 : 120,
}, ['prune'])
@@ -256,12 +238,9 @@ test('prune will skip scanning non-directory in storeDir', async () => {
cacheDir,
dir: process.cwd(),
pnpmHomeDir: '',
authConfig: {
registry: REGISTRY,
},
configByUri: {},
registries: { default: REGISTRY },
storeDir,
userConfig: {},
dlxCacheMaxAge: Infinity,
virtualStoreDirMaxLength: process.platform === 'win32' ? 60 : 120,
}, ['prune'])
@@ -283,13 +262,10 @@ test('prune does not fail if the store contains an unexpected directory', async
cacheDir,
dir: process.cwd(),
pnpmHomeDir: '',
authConfig: {
registry: REGISTRY,
},
configByUri: {},
registries: { default: REGISTRY },
reporter,
storeDir,
userConfig: {},
dlxCacheMaxAge: Infinity,
virtualStoreDirMaxLength: process.platform === 'win32' ? 60 : 120,
}, ['prune'])
@@ -321,13 +297,10 @@ test('prune removes alien files from the store if the --force flag is used', asy
cacheDir,
dir: process.cwd(),
pnpmHomeDir: '',
authConfig: {
registry: REGISTRY,
},
configByUri: {},
registries: { default: REGISTRY },
reporter,
storeDir,
userConfig: {},
force: true,
dlxCacheMaxAge: Infinity,
virtualStoreDirMaxLength: process.platform === 'win32' ? 60 : 120,
@@ -352,13 +325,10 @@ describe('prune when store directory is not properly configured', () => {
cacheDir: path.resolve('cache'),
dir: process.cwd(),
pnpmHomeDir: '',
authConfig: {
registry: REGISTRY,
},
configByUri: {},
registries: { default: REGISTRY },
reporter,
storeDir: nonExistentStoreDir,
userConfig: {},
dlxCacheMaxAge: Infinity,
virtualStoreDirMaxLength: 120,
}, ['prune'])
@@ -388,13 +358,10 @@ describe('prune when store directory is not properly configured', () => {
cacheDir: path.resolve('cache'),
dir: process.cwd(),
pnpmHomeDir: '',
authConfig: {
registry: REGISTRY,
},
configByUri: {},
registries: { default: REGISTRY },
reporter: jest.fn(),
storeDir: fileInPlaceOfStoreDir,
userConfig: {},
dlxCacheMaxAge: Infinity,
virtualStoreDirMaxLength: 120,
}, ['prune'])
@@ -449,13 +416,10 @@ test('prune removes cache directories that outlives dlx-cache-max-age', async ()
cacheDir,
dir: process.cwd(),
pnpmHomeDir: '',
authConfig: {
registry: REGISTRY,
},
configByUri: {},
registries: { default: REGISTRY },
reporter () {},
storeDir,
userConfig: {},
dlxCacheMaxAge: 7,
virtualStoreDirMaxLength: process.platform === 'win32' ? 60 : 120,
}, ['prune'])
@@ -514,12 +478,9 @@ describe('global virtual store prune', () => {
cacheDir,
dir: process.cwd(),
pnpmHomeDir: '',
authConfig: {
registry: REGISTRY,
},
configByUri: {},
registries: { default: REGISTRY },
storeDir: path.join(storeDir, STORE_VERSION),
userConfig: {},
dlxCacheMaxAge: Infinity,
virtualStoreDirMaxLength: process.platform === 'win32' ? 60 : 120,
}, ['prune'])
@@ -582,12 +543,9 @@ describe('global virtual store prune', () => {
cacheDir,
dir: process.cwd(),
pnpmHomeDir: '',
authConfig: {
registry: REGISTRY,
},
configByUri: {},
registries: { default: REGISTRY },
storeDir: path.join(storeDir, STORE_VERSION),
userConfig: {},
dlxCacheMaxAge: Infinity,
virtualStoreDirMaxLength: process.platform === 'win32' ? 60 : 120,
}, ['prune'])
@@ -653,12 +611,9 @@ describe('global virtual store prune', () => {
cacheDir,
dir: process.cwd(),
pnpmHomeDir: '',
authConfig: {
registry: REGISTRY,
},
configByUri: {},
registries: { default: REGISTRY },
storeDir: path.join(storeDir, STORE_VERSION),
userConfig: {},
dlxCacheMaxAge: Infinity,
virtualStoreDirMaxLength: process.platform === 'win32' ? 60 : 120,
}, ['prune'])
@@ -737,12 +692,9 @@ describe('global virtual store prune', () => {
cacheDir,
dir: process.cwd(),
pnpmHomeDir: '',
authConfig: {
registry: REGISTRY,
},
configByUri: {},
registries: { default: REGISTRY },
storeDir: path.join(storeDir, STORE_VERSION),
userConfig: {},
dlxCacheMaxAge: Infinity,
virtualStoreDirMaxLength: process.platform === 'win32' ? 60 : 120,
}, ['prune'])

View File

@@ -41,12 +41,9 @@ test('CLI fails when store status finds modified packages', async () => {
cacheDir,
dir: process.cwd(),
pnpmHomeDir: '',
authConfig: {
registry: REGISTRY,
},
configByUri: {},
registries: modulesState!.registries!,
storeDir,
userConfig: {},
dlxCacheMaxAge: 0,
virtualStoreDirMaxLength: process.platform === 'win32' ? 60 : 120,
}, ['status'])
@@ -95,12 +92,9 @@ test('CLI does not fail when store status does not find modified packages', asyn
cacheDir,
dir: process.cwd(),
pnpmHomeDir: '',
authConfig: {
registry: REGISTRY,
},
configByUri: {},
registries: modulesState!.registries!,
storeDir,
userConfig: {},
dlxCacheMaxAge: 0,
virtualStoreDirMaxLength: process.platform === 'win32' ? 60 : 120,
}, ['status'])
@@ -141,12 +135,9 @@ storeDir: "${relativeStoreDir}"
dir: subpackageDir,
workspaceDir,
pnpmHomeDir: '',
authConfig: {
registry: REGISTRY,
},
configByUri: {},
registries: modulesState!.registries!,
storeDir: relativeStoreDir,
userConfig: {},
dlxCacheMaxAge: 0,
virtualStoreDirMaxLength: process.platform === 'win32' ? 60 : 120,
}, ['status'])

View File

@@ -12,7 +12,7 @@ type CreateResolverOptions = Pick<Config,
| 'fetchRetryMaxtimeout'
| 'fetchRetryMintimeout'
| 'offline'
| 'authConfig'
| 'configByUri'
| 'verifyStoreIntegrity'
> & Required<Pick<Config, 'cacheDir' | 'storeDir'>>
@@ -54,7 +54,7 @@ export type CreateNewStoreControllerOptions = CreateResolverOptions & Pick<Confi
cafsLocker?: CafsLocker
ignoreFile?: (filename: string) => boolean
fetchFullMetadata?: boolean
} & Partial<Pick<Config, 'userConfig' | 'deployAllFiles' | 'sslConfigs' | 'strictStorePkgContentCheck'>> & Pick<ClientOptions, 'resolveSymlinksInInjectedDirs'>
} & Partial<Pick<Config, 'deployAllFiles' | 'strictStorePkgContentCheck'>> & Pick<ClientOptions, 'resolveSymlinksInInjectedDirs'>
export async function createNewStoreController (
opts: CreateNewStoreControllerOptions
@@ -70,7 +70,6 @@ export async function createNewStoreController (
const { resolve, fetchers, clearResolutionCache } = createClient({
customResolvers: opts.hooks?.customResolvers,
customFetchers: opts.hooks?.customFetchers,
userConfig: opts.userConfig,
unsafePerm: opts.unsafePerm,
ca: opts.ca,
cacheDir: opts.cacheDir,
@@ -89,8 +88,7 @@ export async function createNewStoreController (
noProxy: opts.noProxy,
offline: opts.offline,
preferOffline: opts.preferOffline,
authConfig: opts.authConfig,
sslConfigs: opts.sslConfigs,
configByUri: opts.configByUri,
registries: opts.registries,
retry: {
factor: opts.fetchRetryFactor,

View File

@@ -15,7 +15,7 @@ describe('store.importPackage()', () => {
const registry = 'https://registry.npmjs.org/'
const storeIndex = new StoreIndex(storeDir)
const { resolve, fetchers, clearResolutionCache } = createClient({
authConfig: {},
configByUri: {},
cacheDir: path.join(tmp, 'cache'),
storeDir: path.join(tmp, 'store'),
storeIndex,
@@ -59,7 +59,7 @@ describe('store.importPackage()', () => {
const registry = 'https://registry.npmjs.org/'
const storeIndex = new StoreIndex(storeDir)
const { resolve, fetchers, clearResolutionCache } = createClient({
authConfig: {},
configByUri: {},
cacheDir: path.join(tmp, 'cache'),
storeDir: path.join(tmp, 'store'),
storeIndex,

View File

@@ -20,12 +20,12 @@ export function createTempStore (opts?: {
clientOptions?: Partial<ClientOptions>
storeOptions?: CreatePackageStoreOptions
}): CreateTempStoreResult {
const authConfig = { registry }
const configByUri: ClientOptions['configByUri'] = {}
const cacheDir = path.resolve('cache')
const storeDir = opts?.storeDir ?? path.resolve('.store')
const storeIndex = new StoreIndex(storeDir)
const { resolve, fetchers, clearResolutionCache } = createClient({
authConfig,
configByUri,
retry: {
retries: 4,
factor: 10,