diff --git a/.changeset/all-tables-speak.md b/.changeset/all-tables-speak.md new file mode 100644 index 0000000000..54c4f5abe5 --- /dev/null +++ b/.changeset/all-tables-speak.md @@ -0,0 +1,10 @@ +--- +"@pnpm/config": minor +"pnpm": minor +--- + +Allow loading certificates from `cert`, `ca`, and `key` for specific registry URLs. E.g., `//registry.example.com/:ca=-----BEGIN CERTIFICATE-----...`. Previously this was only working via `certfile`, `cafile`, and `keyfile`. + +These properties are supported in `.npmrc`, but were ignored by pnpm, this will make pnpm read and use them as well. + +Related PR: [#10230](https://github.com/pnpm/pnpm/pull/10230). diff --git a/config/config/src/auth.ts b/config/config/src/auth.ts index afa38ed8e2..8e0439b87a 100644 --- a/config/config/src/auth.ts +++ b/config/config/src/auth.ts @@ -17,8 +17,11 @@ const RAW_AUTH_CFG_KEYS = [ ] satisfies Array const RAW_AUTH_CFG_KEY_SUFFIXES = [ + ':ca', ':cafile', + ':cert', ':certfile', + ':key', ':keyfile', ':registry', ':tokenHelper', diff --git a/config/config/src/getNetworkConfigs.ts b/config/config/src/getNetworkConfigs.ts index bbd6d15f81..d5c1b815e6 100644 --- a/config/config/src/getNetworkConfigs.ts +++ b/config/config/src/getNetworkConfigs.ts @@ -8,29 +8,47 @@ export interface GetNetworkConfigsResult { } export function getNetworkConfigs (rawConfig: Record): GetNetworkConfigsResult { - // Get all the auth options that have :certfile or :keyfile in their name + // Get all the auth options that have SSL certificate data or file references const sslConfigs: Record = {} const registries: Record = {} for (const [configKey, value] of Object.entries(rawConfig)) { if (configKey[0] === '@' && configKey.endsWith(':registry')) { registries[configKey.slice(0, configKey.indexOf(':'))] = normalizeRegistryUrl(value as unknown as string) - } else if (configKey.includes(':certfile') || configKey.includes(':keyfile') || configKey.includes(':cafile')) { - // Split by '/:' because the registry may contain a port - const registry = configKey.split('/:')[0] + '/' - if (!sslConfigs[registry]) { - sslConfigs[registry] = { cert: '', key: '' } - } - if (configKey.includes(':certfile')) { - sslConfigs[registry].cert = fs.readFileSync(value as unknown as string, 'utf8') - } else if (configKey.includes(':keyfile')) { - sslConfigs[registry].key = fs.readFileSync(value as unknown as string, 'utf8') - } else if (configKey.includes(':cafile')) { - sslConfigs[registry].ca = fs.readFileSync(value as unknown as string, 'utf8') - } + continue } + + const parsed = tryParseSslSetting(configKey) + if (!parsed) continue + + const { registry, sslConfigKey, isFile } = parsed + if (!sslConfigs[registry]) { + sslConfigs[registry] = { cert: '', key: '' } + } + sslConfigs[registry][sslConfigKey] = isFile + ? fs.readFileSync(value as unknown as string, 'utf8') + : (value as unknown as string).replace(/\\n/g, '\n') } return { registries, sslConfigs, } } + +const SSL_SUFFIX_RE = /:(?cert|key|ca)(?file)?$/ + +interface ParsedSslSetting { + registry: string + sslConfigKey: keyof SslConfig + isFile: boolean +} + +function tryParseSslSetting (key: string): ParsedSslSetting | null { + const match = key.match(SSL_SUFFIX_RE) + if (!match?.groups) { + return null + } + const registry = key.slice(0, match.index!) // already includes the trailing slash + const sslConfigKey = match.groups.id as keyof SslConfig + const isFile = Boolean(match.groups.kind) + return { registry, sslConfigKey, isFile } +} diff --git a/config/config/test/index.ts b/config/config/test/index.ts index 45afbdc779..02077bf5c0 100644 --- a/config/config/test/index.ts +++ b/config/config/test/index.ts @@ -842,6 +842,38 @@ test('getConfig() should read cafile', async () => { -----END CERTIFICATE-----`]) }) +test('getConfig() should read inline SSL certificates from .npmrc', async () => { + prepareEmpty() + + // These are written to .npmrc with literal \n strings + const inlineCa = '-----BEGIN CERTIFICATE-----\\nMIIFNzCCAx+gAwIBAgIQNB613yRzpKtDztlXiHmOGDANBgkqhkiG9w0BAQsFADAR\\n-----END CERTIFICATE-----' + const inlineCert = '-----BEGIN CERTIFICATE-----\\nMIIClientCert\\n-----END CERTIFICATE-----' + const inlineKey = '-----BEGIN PRIVATE KEY-----\\nMIIClientKey\\n-----END PRIVATE KEY-----' + + const npmrc = [ + '//registry.example.com/:ca=' + inlineCa, + '//registry.example.com/:cert=' + inlineCert, + '//registry.example.com/:key=' + inlineKey, + ].join('\n') + fs.writeFileSync('.npmrc', npmrc, 'utf8') + + const { config } = await getConfig({ + cliOptions: {}, + packageManager: { + name: 'pnpm', + version: '1.0.0', + }, + }) + + // After processing, \n should be converted to actual newlines + expect(config.sslConfigs).toBeDefined() + expect(config.sslConfigs['//registry.example.com/']).toStrictEqual({ + ca: inlineCa.replace(/\\n/g, '\n'), + cert: inlineCert.replace(/\\n/g, '\n'), + key: inlineKey.replace(/\\n/g, '\n'), + }) +}) + test('respect mergeGitBranchLockfilesBranchPattern', async () => { { const { config } = await getConfig({