mirror of
https://github.com/pnpm/pnpm.git
synced 2026-04-27 10:30:58 -04:00
22
.changeset/tiny-rivers-crash.md
Normal file
22
.changeset/tiny-rivers-crash.md
Normal file
@@ -0,0 +1,22 @@
|
||||
---
|
||||
"@pnpm/plugin-commands-installation": minor
|
||||
"@pnpm/resolve-dependencies": minor
|
||||
"@pnpm/package-requester": minor
|
||||
"@pnpm/store-controller-types": minor
|
||||
"@pnpm/resolver-base": minor
|
||||
"@pnpm/npm-resolver": minor
|
||||
"@pnpm/core": minor
|
||||
"@pnpm/config": minor
|
||||
"pnpm": minor
|
||||
---
|
||||
|
||||
Added support for `trustPolicyExclude` [#10164](https://github.com/pnpm/pnpm/issues/10164).
|
||||
|
||||
You can now list one or more specific packages or versions that pnpm should allow to install, even if those packages don't satisfy the trust policy requirement. For example:
|
||||
|
||||
```yaml
|
||||
trustPolicy: no-downgrade
|
||||
trustPolicyExclude:
|
||||
- chokidar@4.0.3
|
||||
- webpack@4.47.0 || 5.102.1
|
||||
```
|
||||
@@ -234,6 +234,7 @@ export interface Config extends OptionsFromRootManifest {
|
||||
fetchWarnTimeoutMs?: number
|
||||
fetchMinSpeedKiBps?: number
|
||||
trustPolicy?: TrustPolicy
|
||||
trustPolicyExclude?: string[]
|
||||
}
|
||||
|
||||
export interface ConfigWithDeprecatedSettings extends Config {
|
||||
|
||||
@@ -115,6 +115,7 @@ export const pnpmTypes = {
|
||||
'strict-store-pkg-content-check': Boolean,
|
||||
'strict-peer-dependencies': Boolean,
|
||||
'trust-policy': ['off', 'no-downgrade'] satisfies TrustPolicy[],
|
||||
'trust-policy-exclude': [String, Array],
|
||||
'use-beta-cli': Boolean,
|
||||
'use-node-version': String,
|
||||
'use-running-store-server': Boolean,
|
||||
|
||||
@@ -169,6 +169,7 @@ export interface StrictInstallOptions {
|
||||
minimumReleaseAge?: number
|
||||
minimumReleaseAgeExclude?: string[]
|
||||
trustPolicy?: TrustPolicy
|
||||
trustPolicyExclude?: string[]
|
||||
}
|
||||
|
||||
export type InstallOptions =
|
||||
|
||||
@@ -1178,6 +1178,7 @@ const _installInContext: InstallFunction = async (projects, ctx, opts) => {
|
||||
minimumReleaseAge: opts.minimumReleaseAge,
|
||||
minimumReleaseAgeExclude: opts.minimumReleaseAgeExclude,
|
||||
trustPolicy: opts.trustPolicy,
|
||||
trustPolicyExclude: opts.trustPolicyExclude,
|
||||
}
|
||||
)
|
||||
if (!opts.include.optionalDependencies || !opts.include.devDependencies || !opts.include.dependencies) {
|
||||
|
||||
@@ -210,6 +210,7 @@ async function resolveAndFetch (
|
||||
alwaysTryWorkspacePackages: options.alwaysTryWorkspacePackages,
|
||||
defaultTag: options.defaultTag,
|
||||
trustPolicy: options.trustPolicy,
|
||||
trustPolicyExclude: options.trustPolicyExclude,
|
||||
publishedBy: options.publishedBy,
|
||||
publishedByExclude: options.publishedByExclude,
|
||||
pickLowestVersion: options.pickLowestVersion,
|
||||
|
||||
@@ -75,6 +75,7 @@ export function rcOptionsTypes (): Record<string, unknown> {
|
||||
'store-dir',
|
||||
'strict-peer-dependencies',
|
||||
'trust-policy',
|
||||
'trust-policy-exclude',
|
||||
'unsafe-perm',
|
||||
'offline',
|
||||
'only',
|
||||
|
||||
@@ -64,6 +64,7 @@ export function rcOptionsTypes (): Record<string, unknown> {
|
||||
'store-dir',
|
||||
'strict-peer-dependencies',
|
||||
'trust-policy',
|
||||
'trust-policy-exclude',
|
||||
'offline',
|
||||
'only',
|
||||
'optional',
|
||||
@@ -208,6 +209,10 @@ by any dependencies, so it is an emulation of a flat node_modules',
|
||||
description: "Fail when a package's trust level is downgraded (e.g., from a trusted publisher to provenance only or no trust evidence)",
|
||||
name: '--trust-policy no-downgrade',
|
||||
},
|
||||
{
|
||||
description: 'Exclude specific packages from trust policy checks',
|
||||
name: '--trust-policy-exclude <package-spec>',
|
||||
},
|
||||
{
|
||||
description: 'Starts a store server in the background. The store server will keep running after installation is done. To stop the store server, run `pnpm server stop`',
|
||||
name: '--use-store-server',
|
||||
|
||||
@@ -182,6 +182,7 @@ export interface ResolutionContext {
|
||||
maximumPublishedBy?: Date
|
||||
publishedByExclude?: PackageVersionPolicy
|
||||
trustPolicy?: TrustPolicy
|
||||
trustPolicyExclude?: PackageVersionPolicy
|
||||
}
|
||||
|
||||
export interface MissingPeerInfo {
|
||||
@@ -1337,6 +1338,7 @@ async function resolveDependency (
|
||||
: options.parentPkg.rootDir,
|
||||
skipFetch: ctx.dryRun,
|
||||
trustPolicy: ctx.trustPolicy,
|
||||
trustPolicyExclude: ctx.trustPolicyExclude,
|
||||
update: options.update,
|
||||
workspacePackages: ctx.workspacePackages,
|
||||
supportedArchitectures: options.supportedArchitectures,
|
||||
|
||||
@@ -139,6 +139,7 @@ export interface ResolveDependenciesOptions {
|
||||
minimumReleaseAge?: number
|
||||
minimumReleaseAgeExclude?: string[]
|
||||
trustPolicy?: TrustPolicy
|
||||
trustPolicyExclude?: string[]
|
||||
}
|
||||
|
||||
export interface ResolveDependencyTreeResult {
|
||||
@@ -202,6 +203,7 @@ export async function resolveDependencyTree<T> (
|
||||
maximumPublishedBy: opts.minimumReleaseAge ? new Date(Date.now() - opts.minimumReleaseAge * 60 * 1000) : undefined,
|
||||
publishedByExclude: opts.minimumReleaseAgeExclude ? createPublishedByExclude(opts.minimumReleaseAgeExclude) : undefined,
|
||||
trustPolicy: opts.trustPolicy,
|
||||
trustPolicyExclude: opts.trustPolicyExclude ? createTrustPolicyExclude(opts.trustPolicyExclude) : undefined,
|
||||
}
|
||||
|
||||
function createPublishedByExclude (patterns: string[]): PackageVersionPolicy {
|
||||
@@ -213,6 +215,15 @@ export async function resolveDependencyTree<T> (
|
||||
}
|
||||
}
|
||||
|
||||
function createTrustPolicyExclude (patterns: string[]): PackageVersionPolicy {
|
||||
try {
|
||||
return createPackageVersionPolicy(patterns)
|
||||
} catch (err) {
|
||||
if (!err || typeof err !== 'object' || !('message' in err)) throw err
|
||||
throw new PnpmError('INVALID_TRUST_POLICY_EXCLUDE', `Invalid value in trustPolicyExclude: ${err.message as string}`)
|
||||
}
|
||||
}
|
||||
|
||||
const resolveArgs: ImporterToResolve[] = importers.map((importer) => {
|
||||
const projectSnapshot = opts.wantedLockfile.importers[importer.id]
|
||||
// This may be optimized.
|
||||
|
||||
17
pnpm-lock.yaml
generated
17
pnpm-lock.yaml
generated
@@ -7427,6 +7427,9 @@ importers:
|
||||
specifier: 'catalog:'
|
||||
version: 3.0.0
|
||||
devDependencies:
|
||||
'@pnpm/config.version-policy':
|
||||
specifier: workspace:*
|
||||
version: link:../../config/version-policy
|
||||
'@pnpm/fetch':
|
||||
specifier: workspace:*
|
||||
version: link:../../network/fetch
|
||||
@@ -18253,7 +18256,7 @@ snapshots:
|
||||
'@pnpm/fs.packlist': 2.0.0
|
||||
'@pnpm/logger': 1001.0.0
|
||||
'@pnpm/prepare-package': 1000.0.16(@pnpm/logger@1001.0.0)(typanion@3.14.0)
|
||||
'@pnpm/worker': 1000.1.7(@pnpm/logger@packages+logger)(@types/node@22.15.30)
|
||||
'@pnpm/worker': 1000.1.7(@pnpm/logger@1001.0.0)(@types/node@22.15.30)
|
||||
'@zkochan/rimraf': 3.0.2
|
||||
execa: safe-execa@0.1.2
|
||||
transitivePeerDependencies:
|
||||
@@ -18388,7 +18391,7 @@ snapshots:
|
||||
'@pnpm/find-workspace-dir': 1000.1.0
|
||||
'@pnpm/logger': 1001.0.0
|
||||
'@pnpm/types': 1000.6.0
|
||||
'@pnpm/worker': 1000.1.7(@pnpm/logger@packages+logger)(@types/node@22.15.30)
|
||||
'@pnpm/worker': 1000.1.7(@pnpm/logger@1001.0.0)(@types/node@22.15.30)
|
||||
'@pnpm/workspace.find-packages': 1000.0.25(@pnpm/logger@1001.0.0)(@pnpm/worker@1000.1.7(@pnpm/logger@1001.0.0)(@types/node@22.15.30))(typanion@3.14.0)
|
||||
'@pnpm/workspace.read-manifest': 1000.1.5
|
||||
load-json-file: 7.0.1
|
||||
@@ -18588,7 +18591,7 @@ snapshots:
|
||||
'@pnpm/store-controller-types': 1003.0.2
|
||||
'@pnpm/store.cafs': 1000.0.13
|
||||
'@pnpm/types': 1000.6.0
|
||||
'@pnpm/worker': 1000.1.7(@pnpm/logger@packages+logger)(@types/node@22.15.30)
|
||||
'@pnpm/worker': 1000.1.7(@pnpm/logger@1001.0.0)(@types/node@22.15.30)
|
||||
p-defer: 3.0.0
|
||||
p-limit: 3.1.0
|
||||
p-queue: 6.6.2
|
||||
@@ -18607,7 +18610,7 @@ snapshots:
|
||||
'@pnpm/store-controller-types': 1003.0.2
|
||||
'@pnpm/store.cafs': 1000.0.13
|
||||
'@pnpm/types': 1000.6.0
|
||||
'@pnpm/worker': 1000.1.7(@pnpm/logger@packages+logger)(@types/node@22.15.30)
|
||||
'@pnpm/worker': 1000.1.7(@pnpm/logger@1001.0.0)(@types/node@22.15.30)
|
||||
'@zkochan/rimraf': 3.0.2
|
||||
load-json-file: 6.2.0
|
||||
ramda: '@pnpm/ramda@0.28.1'
|
||||
@@ -18886,7 +18889,7 @@ snapshots:
|
||||
'@pnpm/graceful-fs': 1000.0.0
|
||||
'@pnpm/logger': 1001.0.0
|
||||
'@pnpm/prepare-package': 1000.0.16(@pnpm/logger@1001.0.0)(typanion@3.14.0)
|
||||
'@pnpm/worker': 1000.1.7(@pnpm/logger@packages+logger)(@types/node@22.15.30)
|
||||
'@pnpm/worker': 1000.1.7(@pnpm/logger@1001.0.0)(@types/node@22.15.30)
|
||||
'@zkochan/retry': 0.2.0
|
||||
lodash.throttle: 4.1.1
|
||||
p-map-values: 1.0.0
|
||||
@@ -18925,7 +18928,7 @@ snapshots:
|
||||
dependencies:
|
||||
isexe: 2.0.0
|
||||
|
||||
'@pnpm/worker@1000.1.7(@pnpm/logger@packages+logger)(@types/node@22.15.30)':
|
||||
'@pnpm/worker@1000.1.7(@pnpm/logger@1001.0.0)(@types/node@22.15.30)':
|
||||
dependencies:
|
||||
'@pnpm/cafs-types': 1000.0.0
|
||||
'@pnpm/create-cafs-store': 1000.0.14(@pnpm/logger@1001.0.0)
|
||||
@@ -18934,7 +18937,7 @@ snapshots:
|
||||
'@pnpm/exec.pkg-requires-build': 1000.0.8
|
||||
'@pnpm/fs.hard-link-dir': 1000.0.1(@pnpm/logger@1001.0.0)
|
||||
'@pnpm/graceful-fs': 1000.0.0
|
||||
'@pnpm/logger': link:packages/logger
|
||||
'@pnpm/logger': 1001.0.0
|
||||
'@pnpm/store.cafs': 1000.0.13
|
||||
'@pnpm/symlink-dependency': 1000.0.9(@pnpm/logger@1001.0.0)
|
||||
'@rushstack/worker-pool': 0.4.9(@types/node@22.15.30)
|
||||
|
||||
@@ -527,3 +527,27 @@ test('install does not fail when the trust evidence of a package is downgraded b
|
||||
expect(result.status).toBe(0)
|
||||
project.has('@pnpm/e2e.test-provenance')
|
||||
})
|
||||
|
||||
test('install does not fail when the trust evidence of a package is downgraded but it is in trust-policy-exclude', async () => {
|
||||
const project = prepare()
|
||||
const result = execPnpmSync([
|
||||
'add',
|
||||
'@pnpm/e2e.test-provenance@0.0.5',
|
||||
'--trust-policy=no-downgrade',
|
||||
'--trust-policy-exclude=@pnpm/e2e.test-provenance@0.0.5',
|
||||
])
|
||||
expect(result.status).toBe(0)
|
||||
project.has('@pnpm/e2e.test-provenance')
|
||||
})
|
||||
|
||||
test('install does not fail when the trust evidence of a package is downgraded but the package name is in trust-policy-exclude', async () => {
|
||||
const project = prepare()
|
||||
const result = execPnpmSync([
|
||||
'add',
|
||||
'@pnpm/e2e.test-provenance@0.0.5',
|
||||
'--trust-policy=no-downgrade',
|
||||
'--trust-policy-exclude=@pnpm/e2e.test-provenance',
|
||||
])
|
||||
expect(result.status).toBe(0)
|
||||
project.has('@pnpm/e2e.test-provenance')
|
||||
})
|
||||
|
||||
@@ -67,6 +67,7 @@
|
||||
"@pnpm/logger": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@pnpm/config.version-policy": "workspace:*",
|
||||
"@pnpm/fetch": "workspace:*",
|
||||
"@pnpm/logger": "workspace:*",
|
||||
"@pnpm/npm-resolver": "workspace:*",
|
||||
|
||||
@@ -188,6 +188,7 @@ export type ResolveFromNpmOptions = {
|
||||
publishedByExclude?: PackageVersionPolicy
|
||||
pickLowestVersion?: boolean
|
||||
trustPolicy?: TrustPolicy
|
||||
trustPolicyExclude?: PackageVersionPolicy
|
||||
dryRun?: boolean
|
||||
lockfileDir?: string
|
||||
preferredVersions?: PreferredVersions
|
||||
@@ -308,7 +309,7 @@ async function resolveNpm (
|
||||
throw new NoMatchingVersionError({ wantedDependency, packageMeta: meta, registry })
|
||||
} else if (opts.trustPolicy === 'no-downgrade') {
|
||||
assertMetaHasTime(meta)
|
||||
failIfTrustDowngraded(meta, pickedPackage.version)
|
||||
failIfTrustDowngraded(meta, pickedPackage.version, opts.trustPolicyExclude)
|
||||
}
|
||||
|
||||
const workspacePkgsMatchingName = workspacePackages?.get(pickedPackage.name)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { PnpmError } from '@pnpm/error'
|
||||
import { type PackageInRegistry, type PackageMetaWithTime } from '@pnpm/registry.types'
|
||||
import { type PackageVersionPolicy } from '@pnpm/types'
|
||||
|
||||
type TrustEvidence = 'provenance' | 'trustedPublisher'
|
||||
|
||||
@@ -10,8 +11,19 @@ const TRUST_RANK = {
|
||||
|
||||
export function failIfTrustDowngraded (
|
||||
meta: PackageMetaWithTime,
|
||||
version: string
|
||||
version: string,
|
||||
trustPolicyExclude?: PackageVersionPolicy
|
||||
): void {
|
||||
if (trustPolicyExclude) {
|
||||
const excludeResult = trustPolicyExclude(meta.name)
|
||||
if (excludeResult === true) {
|
||||
return
|
||||
}
|
||||
if (Array.isArray(excludeResult) && excludeResult.includes(version)) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const versionPublishedAt = meta.time[version]
|
||||
if (!versionPublishedAt) {
|
||||
throw new PnpmError(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { type PackageInRegistry, type PackageMetaWithTime } from '@pnpm/registry.types'
|
||||
import { createPackageVersionPolicy } from '@pnpm/config.version-policy'
|
||||
import { getTrustEvidence, failIfTrustDowngraded } from '../src/trustChecks.js'
|
||||
|
||||
describe('getTrustEvidence', () => {
|
||||
@@ -405,3 +406,88 @@ describe('failIfTrustDowngraded', () => {
|
||||
}).toThrow('Missing time')
|
||||
})
|
||||
})
|
||||
|
||||
describe('failIfTrustDowngraded with trustPolicyExclude', () => {
|
||||
test('allows downgrade when package@version is in exclude list', () => {
|
||||
const meta: PackageMetaWithTime = {
|
||||
name: 'foo',
|
||||
'dist-tags': { latest: '3.0.0' },
|
||||
versions: {
|
||||
'2.0.0': {
|
||||
name: 'foo',
|
||||
version: '2.0.0',
|
||||
dist: {
|
||||
shasum: 'def456',
|
||||
tarball: 'https://registry.example.com/foo/-/foo-2.0.0.tgz',
|
||||
attestations: {
|
||||
provenance: {
|
||||
predicateType: 'https://slsa.dev/provenance/v1',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
'3.0.0': {
|
||||
name: 'foo',
|
||||
version: '3.0.0',
|
||||
dist: {
|
||||
shasum: 'ghi789',
|
||||
tarball: 'https://registry.example.com/foo/-/foo-3.0.0.tgz',
|
||||
},
|
||||
},
|
||||
},
|
||||
time: {
|
||||
'2.0.0': '2025-02-01T00:00:00.000Z',
|
||||
'3.0.0': '2025-03-01T00:00:00.000Z',
|
||||
},
|
||||
}
|
||||
|
||||
expect(() => {
|
||||
failIfTrustDowngraded(meta, '3.0.0', createPackageVersionPolicy(['foo@3.0.0']))
|
||||
}).not.toThrow()
|
||||
|
||||
expect(() => {
|
||||
failIfTrustDowngraded(meta, '3.0.0')
|
||||
}).toThrow('High-risk trust downgrade')
|
||||
})
|
||||
|
||||
test('allows downgrade when package name is in exclude list (all versions)', () => {
|
||||
const meta: PackageMetaWithTime = {
|
||||
name: 'bar',
|
||||
'dist-tags': { latest: '3.0.0' },
|
||||
versions: {
|
||||
'2.0.0': {
|
||||
name: 'bar',
|
||||
version: '2.0.0',
|
||||
_npmUser: {
|
||||
name: 'test-publisher',
|
||||
email: 'publisher@example.com',
|
||||
trustedPublisher: {
|
||||
id: 'test-provider',
|
||||
oidcConfigId: 'oidc:test-config-123',
|
||||
},
|
||||
},
|
||||
dist: {
|
||||
shasum: 'def456',
|
||||
tarball: 'https://registry.example.com/bar/-/bar-2.0.0.tgz',
|
||||
},
|
||||
},
|
||||
'3.0.0': {
|
||||
name: 'bar',
|
||||
version: '3.0.0',
|
||||
dist: {
|
||||
shasum: 'ghi789',
|
||||
tarball: 'https://registry.example.com/bar/-/bar-3.0.0.tgz',
|
||||
},
|
||||
},
|
||||
},
|
||||
time: {
|
||||
'2.0.0': '2025-02-01T00:00:00.000Z',
|
||||
'3.0.0': '2025-03-01T00:00:00.000Z',
|
||||
},
|
||||
}
|
||||
|
||||
expect(() => {
|
||||
failIfTrustDowngraded(meta, '3.0.0', createPackageVersionPolicy(['bar']))
|
||||
}).not.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -15,6 +15,9 @@
|
||||
{
|
||||
"path": "../../config/pick-registry-for-package"
|
||||
},
|
||||
{
|
||||
"path": "../../config/version-policy"
|
||||
},
|
||||
{
|
||||
"path": "../../crypto/hash"
|
||||
},
|
||||
|
||||
@@ -109,6 +109,7 @@ export interface PreferredVersions {
|
||||
export interface ResolveOptions {
|
||||
alwaysTryWorkspacePackages?: boolean
|
||||
trustPolicy?: TrustPolicy
|
||||
trustPolicyExclude?: PackageVersionPolicy
|
||||
defaultTag?: string
|
||||
pickLowestVersion?: boolean
|
||||
publishedBy?: Date
|
||||
|
||||
@@ -138,6 +138,7 @@ export interface RequestPackageOptions {
|
||||
calcSpecifier?: boolean
|
||||
pinnedVersion?: PinnedVersion
|
||||
trustPolicy?: TrustPolicy
|
||||
trustPolicyExclude?: PackageVersionPolicy
|
||||
}
|
||||
|
||||
export type BundledManifestFunction = () => Promise<BundledManifest | undefined>
|
||||
|
||||
Reference in New Issue
Block a user