mirror of
https://github.com/pnpm/pnpm.git
synced 2026-05-12 18:49:41 -04:00
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
114 lines
3.5 KiB
TypeScript
114 lines
3.5 KiB
TypeScript
import fs from 'node:fs'
|
|
|
|
import type { Creds, RegistryConfig } from '@pnpm/types'
|
|
import normalizeRegistryUrl from 'normalize-registry-url'
|
|
|
|
import { parseCreds, type RawCreds } from './parseCreds.js'
|
|
|
|
export interface NetworkConfigs {
|
|
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 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 parsedCreds = tryParseCredsKey(configKey)
|
|
if (parsedCreds) {
|
|
const { credsField, registry } = parsedCreds
|
|
rawCredsMap[registry] ??= {}
|
|
rawCredsMap[registry][credsField] = value as string
|
|
continue
|
|
}
|
|
|
|
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')
|
|
}
|
|
}
|
|
|
|
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 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
|
|
if (value != null) {
|
|
input[key] = value
|
|
}
|
|
}
|
|
return parseCreds(input)
|
|
}
|
|
|
|
const AUTH_SUFFIX_RE = /:(?<key>_auth|_authToken|_password|username|tokenHelper)$/
|
|
const AUTH_SUFFIX_KEY_MAP: Record<string, keyof RawCreds> = {
|
|
_auth: 'authPairBase64',
|
|
_authToken: 'authToken',
|
|
_password: 'authPassword',
|
|
username: 'authUsername',
|
|
tokenHelper: 'tokenHelper',
|
|
}
|
|
|
|
interface ParsedCredsKey {
|
|
registry: string
|
|
credsField: keyof RawCreds
|
|
}
|
|
|
|
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 credsField = AUTH_SUFFIX_KEY_MAP[match.groups.key]
|
|
if (!credsField) {
|
|
throw new Error(`Unexpected key: ${match.groups.key}`)
|
|
}
|
|
return { registry, credsField }
|
|
}
|
|
|
|
const SSL_SUFFIX_RE = /:(?<id>cert|key|ca)(?<kind>file)?$/
|
|
|
|
type SslField = 'cert' | 'key' | 'ca'
|
|
|
|
interface ParsedSslKey {
|
|
registry: string
|
|
sslField: SslField
|
|
isFile: boolean
|
|
}
|
|
|
|
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 sslField = match.groups.id as SslField
|
|
const isFile = Boolean(match.groups.kind)
|
|
return { registry, sslField, isFile }
|
|
}
|