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:
Claude
2026-04-10 19:20:25 +00:00
parent 673c09240d
commit 216007ec7d
6 changed files with 169 additions and 14 deletions

View 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).

View File

@@ -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',
})
})

View File

@@ -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.
*/

View 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
}

View File

@@ -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)

View File

@@ -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