diff --git a/.changeset/proxy-fallback-for-bootstrap-downloads.md b/.changeset/proxy-fallback-for-bootstrap-downloads.md new file mode 100644 index 0000000000..acbcf4b93e --- /dev/null +++ b/.changeset/proxy-fallback-for-bootstrap-downloads.md @@ -0,0 +1,6 @@ +--- +"@pnpm/config.reader": patch +"pnpm": patch +--- + +When `https-proxy` is `false`, pnpm now downloads the pnpm version pinned by `packageManager` through the legacy `proxy` setting, as installs already do. Setting `proxy` to `false` now also stops that download from using the `HTTP_PROXY` environment variable. The signature check of the pinned pnpm now uses the same proxy and TLS settings as its download [pnpm/tasks#193](https://github.com/pnpm/tasks/issues/193). diff --git a/pnpm11/config/reader/src/getConfig/registries.ts b/pnpm11/config/reader/src/getConfig/registries.ts index 74d2726afa..e4b0271e0a 100644 --- a/pnpm11/config/reader/src/getConfig/registries.ts +++ b/pnpm11/config/reader/src/getConfig/registries.ts @@ -189,13 +189,14 @@ function createPackageManagerNetworkConfig ( // trusted `.npmrc` files, as they do for the project's requests. The // command line's `.npmrc`-spelled tokens sit inside `trustedConfig` too. const cliNetworkSettings = configFromCliOpts as BootstrapNetworkSettings - const httpsProxy = getProxyValue( - trustedConfig['https-proxy'] ?? trustedConfig.proxy, - getEnvValue(env, 'https_proxy') - ) + const httpsProxySetting = getProxyUrl(trustedConfig['https-proxy']) ?? + (trustedConfig.proxy === false ? false : getProxyUrl(trustedConfig.proxy)) + const httpsProxy = getProxyValue(httpsProxySetting, getEnvValue(env, 'https_proxy')) const httpProxy = getProxyValue( - trustedConfig['http-proxy'], - httpsProxy ?? getEnvValue(env, 'http_proxy') ?? getEnvValue(env, 'proxy') + getProxyUrl(trustedConfig['http-proxy']), + httpsProxySetting === false + ? undefined + : httpsProxy ?? getEnvValue(env, 'http_proxy') ?? getEnvValue(env, 'proxy') ) return { ca: trustedConfig.ca as string | string[] | undefined, @@ -214,10 +215,19 @@ function getEnvValue (env: Record, key: string): str return env[key] ?? env[key.toUpperCase()] ?? env[key.toLowerCase()] } -function getProxyValue (value: unknown, fallback: string | undefined): string | undefined { - if (value === false || value === null) return undefined - if (typeof value === 'string' && value.length > 0) return value - return fallback +/** + * A proxy setting of `false`, `null`, or an empty string reads as unset, so + * the next setting and then the environment apply, as they do for installs. + * Only `proxy=false` turns the proxy off, including the environment fallback + * for `http-proxy`. + */ +function getProxyUrl (value: unknown): string | undefined { + return typeof value === 'string' && value.length > 0 ? value : undefined +} + +function getProxyValue (value: string | false | undefined, fallback: string | undefined): string | undefined { + if (value === false) return undefined + return value ?? fallback } /** diff --git a/pnpm11/config/reader/test/index.ts b/pnpm11/config/reader/test/index.ts index af664c7763..e9e665e8f7 100644 --- a/pnpm11/config/reader/test/index.ts +++ b/pnpm11/config/reader/test/index.ts @@ -6065,6 +6065,102 @@ test('proxy settings are still read from .npmrc', async () => { expect(config.noProxy).toBe('internal.example.com') }) +test('package manager downloads use the legacy proxy when https-proxy is false', async () => { + prepareEmpty() + + fs.writeFileSync('user.npmrc', 'https-proxy=false\nproxy=http://legacy-proxy.example.com:8080', 'utf8') + + const { config } = await getConfig({ + cliOptions: { userconfig: path.resolve('user.npmrc') }, + env: {}, + packageManager: { + name: 'pnpm', + version: '1.0.0', + }, + workspaceDir: process.cwd(), + }) + + expect(config.packageManagerNetworkConfig?.httpsProxy).toBe('http://legacy-proxy.example.com:8080') +}) + +test('package manager downloads use the legacy proxy when https-proxy is empty', async () => { + prepareEmpty() + + fs.writeFileSync('user.npmrc', 'https-proxy=\nproxy=http://legacy-proxy.example.com:8080', 'utf8') + + const { config } = await getConfig({ + cliOptions: { userconfig: path.resolve('user.npmrc') }, + env: {}, + packageManager: { + name: 'pnpm', + version: '1.0.0', + }, + workspaceDir: process.cwd(), + }) + + expect(config.packageManagerNetworkConfig?.httpsProxy).toBe('http://legacy-proxy.example.com:8080') +}) + +test('package manager downloads ignore the environment proxy when proxy is false', async () => { + prepareEmpty() + + fs.writeFileSync('user.npmrc', 'https-proxy=false\nproxy=false', 'utf8') + + const { config } = await getConfig({ + cliOptions: { userconfig: path.resolve('user.npmrc') }, + env: { + HTTP_PROXY: 'http://env-proxy.example.com:8080', + HTTPS_PROXY: 'http://env-proxy.example.com:8080', + }, + packageManager: { + name: 'pnpm', + version: '1.0.0', + }, + workspaceDir: process.cwd(), + }) + + expect(config.packageManagerNetworkConfig?.httpsProxy).toBeUndefined() + expect(config.packageManagerNetworkConfig?.httpProxy).toBeUndefined() +}) + +test('package manager downloads use the environment proxy when proxy is null', async () => { + prepareEmpty() + + fs.writeFileSync('user.npmrc', 'proxy=null', 'utf8') + + const { config } = await getConfig({ + cliOptions: { userconfig: path.resolve('user.npmrc') }, + env: { + HTTPS_PROXY: 'http://env-proxy.example.com:8080', + }, + packageManager: { + name: 'pnpm', + version: '1.0.0', + }, + workspaceDir: process.cwd(), + }) + + expect(config.packageManagerNetworkConfig?.httpsProxy).toBe('http://env-proxy.example.com:8080') +}) + +test('package manager downloads use the https proxy when http-proxy is false', async () => { + prepareEmpty() + + fs.writeFileSync('user.npmrc', 'https-proxy=http://https-proxy.example.com:8080\nhttp-proxy=false', 'utf8') + + const { config } = await getConfig({ + cliOptions: { userconfig: path.resolve('user.npmrc') }, + env: {}, + packageManager: { + name: 'pnpm', + version: '1.0.0', + }, + workspaceDir: process.cwd(), + }) + + expect(config.packageManagerNetworkConfig?.httpProxy).toBe('http://https-proxy.example.com:8080') +}) + test('an empty legacy proxy setting in .npmrc falls through to the environment', async () => { prepareEmpty() diff --git a/pnpm11/pnpm/src/switchCliVersion.test.ts b/pnpm11/pnpm/src/switchCliVersion.test.ts index b4a8c0ae51..1c42692d88 100644 --- a/pnpm11/pnpm/src/switchCliVersion.test.ts +++ b/pnpm11/pnpm/src/switchCliVersion.test.ts @@ -698,6 +698,28 @@ test('ends with the exit status of the pnpm it switched to', async () => { } }) +test('switchCliVersion verifies the pinned pnpm through the trusted proxy settings', async () => { + const config = { + httpsProxy: 'http://project-proxy.example.com:8080', + packageManagerNetworkConfig: { configByUri: {} }, + rawConfig: {}, + } as unknown as Config + const context = { + rootProjectManifestDir: '/project', + wantedPackageManager: { name: 'pnpm', version: '11.13.1', fromDevEngines: true, onFail: 'download' }, + } as unknown as ConfigContext + readEnvLockfile.mockResolvedValue(envLockfileFor('11.13.1')) + + const exit = jest.spyOn(process, 'exit').mockImplementation((() => undefined) as never) + try { + await switchCliVersion(config, context) + } finally { + exit.mockRestore() + } + + expect(installPnpmToStore).toHaveBeenCalledWith('11.13.1', expect.objectContaining({ httpsProxy: undefined })) +}) + test('reports a pnpm that cannot be started as a failed version switch', async () => { const config = { rawConfig: {} } as unknown as Config const context = { diff --git a/pnpm11/pnpm/src/switchCliVersion.ts b/pnpm11/pnpm/src/switchCliVersion.ts index a0cf5ca04e..b334e2f5a2 100644 --- a/pnpm11/pnpm/src/switchCliVersion.ts +++ b/pnpm11/pnpm/src/switchCliVersion.ts @@ -346,21 +346,12 @@ function installPnpmToStoreOptions ( envLockfile, storeController: store.ctrl, storeDir: store.dir, - registriesByScope: getPackageManagerBootstrapConfig(config).registriesByScope, virtualStoreDirMaxLength: config.virtualStoreDirMaxLength, packageManager: { name: packageManager.name, version: packageManager.version }, - // Network settings so the engine identity check can reach the canonical - // npm registry through the user's proxy / TLS configuration. - ca: config.ca, - cert: config.cert, - key: config.key, - httpProxy: config.httpProxy, - httpsProxy: config.httpsProxy, - noProxy: config.noProxy, - strictSsl: config.strictSsl, - localAddress: config.localAddress, + // The engine identity check reaches the registry with the same trusted + // network settings as the store controller that downloads pnpm. + ...getPackageManagerBootstrapConfig(config), maxSockets: config.maxSockets, - configByUri: config.configByUri, timeout: config.fetchTimeout, } }