mirror of
https://github.com/pnpm/pnpm.git
synced 2026-04-26 18:09:06 -04:00
feat(config): make dlx inherit security and trust policy settings from local config
Previously, `pnpm dlx` and `pnpm create` only inherited auth/registry settings from the local project config, ignoring all other settings. This meant security policy settings like `minimumReleaseAge` and `trustPolicy` configured in a project's `pnpm-workspace.yaml` were silently dropped. Now these commands inherit two categories of local settings: 1. Registry & auth (existing) — needed to reach the same package sources 2. Security & trust policy (new) — settings that gate what is allowed to be downloaded, reflecting the org's security posture Project-structural settings (hoisting, linking, workspace layout, etc.) remain correctly excluded. Closes #11183 https://claude.ai/code/session_01NumMLsTvswMVJpbWp3YJrH
This commit is contained in:
6
.changeset/dlx-inherit-policy-config.md
Normal file
6
.changeset/dlx-inherit-policy-config.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@pnpm/config.reader": minor
|
||||
"pnpm": minor
|
||||
---
|
||||
|
||||
`pnpm dlx` and `pnpm create` now respect security and trust policy settings (`minimumReleaseAge`, `minimumReleaseAgeExclude`, `minimumReleaseAgeStrict`, `trustPolicy`, `trustPolicyExclude`, `trustPolicyIgnoreAfter`) from project-level configuration [#11183](https://github.com/pnpm/pnpm/issues/11183).
|
||||
@@ -1,4 +1,4 @@
|
||||
import { inheritAuthConfig } from './auth.js'
|
||||
import { inheritAuthConfig, inheritDlxConfig } from './auth.js'
|
||||
import type { InheritableConfigPair } from './inheritPickedConfig.js'
|
||||
|
||||
test('inheritAuthConfig copies only auth keys from source to target', () => {
|
||||
@@ -38,3 +38,62 @@ test('inheritAuthConfig copies only auth keys from source to target', () => {
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test('inheritDlxConfig copies auth and security policy keys from source to target', () => {
|
||||
const target: InheritableConfigPair = {
|
||||
config: {
|
||||
bin: 'foo',
|
||||
cacheDir: '/path/to/cache/dir',
|
||||
registry: 'https://npmjs.com/registry/',
|
||||
shamefullyHoist: true,
|
||||
authConfig: {
|
||||
registry: 'https://npmjs.com/registry/',
|
||||
},
|
||||
} as any, // eslint-disable-line
|
||||
}
|
||||
|
||||
inheritDlxConfig(target, {
|
||||
config: {
|
||||
bin: 'bar',
|
||||
cacheDir: '/path/to/another/cache/dir',
|
||||
storeDir: '/path/to/custom/store/dir',
|
||||
registry: 'https://example.com/local-registry/',
|
||||
shamefullyHoist: false,
|
||||
minimumReleaseAge: 1440,
|
||||
minimumReleaseAgeExclude: ['trusted-pkg'],
|
||||
minimumReleaseAgeStrict: true,
|
||||
trustPolicy: 'no-downgrade',
|
||||
trustPolicyExclude: ['legacy-pkg'],
|
||||
trustPolicyIgnoreAfter: 525600,
|
||||
authConfig: {
|
||||
registry: 'https://example.com/local-registry/',
|
||||
'//example.com/local-registry/:_authToken': 'SECRET_TOKEN',
|
||||
'minimum-release-age': '1440',
|
||||
},
|
||||
} as any, // eslint-disable-line
|
||||
})
|
||||
|
||||
// Auth keys are inherited
|
||||
expect(target.config.registry).toBe('https://example.com/local-registry/')
|
||||
|
||||
// Security/trust policy keys are inherited
|
||||
expect(target.config.minimumReleaseAge).toBe(1440)
|
||||
expect(target.config.minimumReleaseAgeExclude).toEqual(['trusted-pkg'])
|
||||
expect(target.config.minimumReleaseAgeStrict).toBe(true)
|
||||
expect(target.config.trustPolicy).toBe('no-downgrade')
|
||||
expect(target.config.trustPolicyExclude).toEqual(['legacy-pkg'])
|
||||
expect(target.config.trustPolicyIgnoreAfter).toBe(525600)
|
||||
|
||||
// Project-structural keys are NOT inherited
|
||||
expect(target.config.bin).toBe('foo')
|
||||
expect(target.config.cacheDir).toBe('/path/to/cache/dir')
|
||||
expect(target.config.shamefullyHoist).toBe(true)
|
||||
expect(target.config.storeDir).toBeUndefined()
|
||||
|
||||
// Raw auth keys are inherited, raw policy keys are inherited
|
||||
expect(target.config.authConfig).toMatchObject({
|
||||
registry: 'https://example.com/local-registry/',
|
||||
'//example.com/local-registry/:_authToken': 'SECRET_TOKEN',
|
||||
'minimum-release-age': '1440',
|
||||
})
|
||||
})
|
||||
|
||||
@@ -45,6 +45,56 @@ const AUTH_CFG_KEYS = [
|
||||
'registries',
|
||||
] satisfies Array<keyof Config>
|
||||
|
||||
/**
|
||||
* Security and trust policy config keys (camelCase, from Config type).
|
||||
*
|
||||
* ## Principle
|
||||
*
|
||||
* `pnpm dlx` runs packages in isolation from the current project. It must not
|
||||
* read project-structural settings (hoisting, linking, workspace layout, etc.)
|
||||
* from local config. However, two categories of local settings DO apply:
|
||||
*
|
||||
* 1. **Registry & auth** — needed to reach the same package sources
|
||||
* (registries, tokens, certificates, proxies).
|
||||
* 2. **Security & trust policy** — these reflect the user's or organization's
|
||||
* security posture and must apply regardless of how a package is installed.
|
||||
* A setting that answers "what am I allowed to download?" belongs here.
|
||||
*
|
||||
* Everything else — settings that answer "how should I arrange what I
|
||||
* downloaded?" — is intentionally excluded.
|
||||
*
|
||||
* ## Rules
|
||||
*
|
||||
* | Category | Inherited by dlx? | Examples |
|
||||
* |--------------------------------|--------------------|--------------------------------------------------|
|
||||
* | Registry & auth | Yes | registry, _authToken, ca, proxy |
|
||||
* | Security & trust policy | Yes | minimumReleaseAge, trustPolicy |
|
||||
* | Installation structure | No | shamefully-hoist, node-linker, hoist-pattern |
|
||||
* | Workspace settings | No | link-workspace-packages, shared-workspace-lockfile|
|
||||
* | Resolution strategy | No | resolution-mode, dedupe-peers |
|
||||
*/
|
||||
const POLICY_CFG_KEYS = [
|
||||
'minimumReleaseAge',
|
||||
'minimumReleaseAgeExclude',
|
||||
'minimumReleaseAgeStrict',
|
||||
'trustPolicy',
|
||||
'trustPolicyExclude',
|
||||
'trustPolicyIgnoreAfter',
|
||||
] satisfies Array<keyof Config>
|
||||
|
||||
/**
|
||||
* Raw (kebab-case) config keys for security and trust policy settings.
|
||||
* These are the keys as they appear in .npmrc or pnpm-workspace.yaml.
|
||||
*/
|
||||
const RAW_POLICY_CFG_KEYS = [
|
||||
'minimum-release-age',
|
||||
'minimum-release-age-exclude',
|
||||
'minimum-release-age-strict',
|
||||
'trust-policy',
|
||||
'trust-policy-exclude',
|
||||
'trust-policy-ignore-after',
|
||||
]
|
||||
|
||||
const NPM_AUTH_SETTINGS = [
|
||||
...RAW_AUTH_CFG_KEYS,
|
||||
'_auth',
|
||||
@@ -65,6 +115,14 @@ function isAuthCfgKey (cfgKey: keyof Config): cfgKey is typeof AUTH_CFG_KEYS[num
|
||||
return (AUTH_CFG_KEYS as Array<keyof Config>).includes(cfgKey)
|
||||
}
|
||||
|
||||
function isPolicyCfgKey (cfgKey: keyof Config): cfgKey is typeof POLICY_CFG_KEYS[number] {
|
||||
return (POLICY_CFG_KEYS as Array<keyof Config>).includes(cfgKey)
|
||||
}
|
||||
|
||||
function isRawPolicyCfgKey (rawCfgKey: string): boolean {
|
||||
return (RAW_POLICY_CFG_KEYS as string[]).includes(rawCfgKey)
|
||||
}
|
||||
|
||||
function pickRawAuthConfig<RawLocalCfg extends Record<string, unknown>> (rawLocalCfg: RawLocalCfg): Partial<RawLocalCfg> {
|
||||
const result: Partial<RawLocalCfg> = {}
|
||||
for (const key in rawLocalCfg) {
|
||||
@@ -85,10 +143,42 @@ function pickAuthConfig (localCfg: Partial<Config>): Partial<Config> {
|
||||
return result as Partial<Config>
|
||||
}
|
||||
|
||||
function pickRawDlxConfig<RawLocalCfg extends Record<string, unknown>> (rawLocalCfg: RawLocalCfg): Partial<RawLocalCfg> {
|
||||
const result: Partial<RawLocalCfg> = {}
|
||||
for (const key in rawLocalCfg) {
|
||||
if (isRawAuthCfgKey(key) || isRawPolicyCfgKey(key)) {
|
||||
result[key] = rawLocalCfg[key]
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function pickDlxConfig (localCfg: Partial<Config>): Partial<Config> {
|
||||
const result: Record<string, unknown> = {}
|
||||
for (const key in localCfg) {
|
||||
if (isAuthCfgKey(key as keyof Config) || isPolicyCfgKey(key as keyof Config)) {
|
||||
result[key] = localCfg[key as keyof Config]
|
||||
}
|
||||
}
|
||||
return result as Partial<Config>
|
||||
}
|
||||
|
||||
export function inheritAuthConfig (target: InheritableConfigPair, src: InheritableConfigPair): void {
|
||||
inheritPickedConfig(target, src, pickAuthConfig, pickRawAuthConfig)
|
||||
}
|
||||
|
||||
/**
|
||||
* Inherits both auth/registry settings and security/trust policy settings
|
||||
* from a local config source into the target config.
|
||||
*
|
||||
* Used by `pnpm dlx` and `pnpm create` so that these commands respect
|
||||
* the local project's registry authentication and security policies
|
||||
* while ignoring project-structural settings.
|
||||
*/
|
||||
export function inheritDlxConfig (target: InheritableConfigPair, src: InheritableConfigPair): void {
|
||||
inheritPickedConfig(target, src, pickDlxConfig, pickRawDlxConfig)
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the config key would be read from an INI config file.
|
||||
*/
|
||||
|
||||
@@ -22,7 +22,7 @@ import { omit } from 'ramda'
|
||||
import { realpathMissing } from 'realpath-missing'
|
||||
import semver from 'semver'
|
||||
|
||||
import { inheritAuthConfig, pickIniConfig } from './auth.js'
|
||||
import { inheritDlxConfig, pickIniConfig } from './auth.js'
|
||||
import { checkGlobalBinDir } from './checkGlobalBinDir.js'
|
||||
import { getDefaultWorkspaceConcurrency, getWorkspaceConcurrency } from './concurrency.js'
|
||||
import type {
|
||||
@@ -88,22 +88,22 @@ export async function getConfig (opts: {
|
||||
}
|
||||
workspaceDir?: string | undefined
|
||||
env?: Record<string, string | undefined>
|
||||
ignoreNonAuthSettingsFromLocal?: boolean
|
||||
onlyInheritDlxSettingsFromLocal?: boolean
|
||||
ignoreLocalSettings?: boolean
|
||||
}): Promise<{ config: Config, context: ConfigContext, warnings: string[] }> {
|
||||
if (opts.ignoreNonAuthSettingsFromLocal) {
|
||||
const { ignoreNonAuthSettingsFromLocal: _, ...authOpts } = opts
|
||||
const globalCfgOpts: typeof authOpts = {
|
||||
...authOpts,
|
||||
if (opts.onlyInheritDlxSettingsFromLocal) {
|
||||
const { onlyInheritDlxSettingsFromLocal: _, ...localOpts } = opts
|
||||
const globalCfgOpts: typeof localOpts = {
|
||||
...localOpts,
|
||||
ignoreLocalSettings: true,
|
||||
cliOptions: {
|
||||
...authOpts.cliOptions,
|
||||
...localOpts.cliOptions,
|
||||
dir: os.homedir(),
|
||||
},
|
||||
}
|
||||
const [final, authSrc] = await Promise.all([getConfig(globalCfgOpts), getConfig(authOpts)])
|
||||
inheritAuthConfig(final, authSrc)
|
||||
final.warnings.push(...authSrc.warnings)
|
||||
const [final, localSrc] = await Promise.all([getConfig(globalCfgOpts), getConfig(localOpts)])
|
||||
inheritDlxConfig(final, localSrc)
|
||||
final.warnings.push(...localSrc.warnings)
|
||||
return final
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ export async function getConfig (
|
||||
excludeReporter: boolean
|
||||
globalDirShouldAllowWrite?: boolean
|
||||
workspaceDir: string | undefined
|
||||
ignoreNonAuthSettingsFromLocal?: boolean
|
||||
onlyInheritDlxSettingsFromLocal?: boolean
|
||||
}
|
||||
): Promise<{ config: Config, context: ConfigContext }> {
|
||||
const { config, context, warnings } = await _getConfig({
|
||||
@@ -24,7 +24,7 @@ export async function getConfig (
|
||||
globalDirShouldAllowWrite: opts.globalDirShouldAllowWrite,
|
||||
packageManager,
|
||||
workspaceDir: opts.workspaceDir,
|
||||
ignoreNonAuthSettingsFromLocal: opts.ignoreNonAuthSettingsFromLocal,
|
||||
onlyInheritDlxSettingsFromLocal: opts.onlyInheritDlxSettingsFromLocal,
|
||||
})
|
||||
context.cliOptions = cliOptions
|
||||
applyDerivedConfig(config)
|
||||
|
||||
@@ -100,7 +100,7 @@ export async function main (inputArgv: string[]): Promise<void> {
|
||||
excludeReporter: false,
|
||||
globalDirShouldAllowWrite,
|
||||
workspaceDir,
|
||||
ignoreNonAuthSettingsFromLocal: isDlxOrCreateCommand,
|
||||
onlyInheritDlxSettingsFromLocal: isDlxOrCreateCommand,
|
||||
}) as { config: typeof config, context: ConfigContext })
|
||||
if (!isExecutedByCorepack() && cmd !== 'setup' && context.wantedPackageManager != null) {
|
||||
const pm = context.wantedPackageManager
|
||||
|
||||
Reference in New Issue
Block a user