feat: add trustPolicyIgnoreAfter (#10359)

* feat: add `trustPolicyIgnoreAfter`

* Update .changeset/big-lies-pump.md

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* refactor: npm-resolver

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Zoltan Kochan <z@kochan.io>
This commit is contained in:
btea
2025-12-28 09:01:09 +08:00
committed by GitHub
parent 71de2b3f2b
commit facdd717bf
17 changed files with 106 additions and 8 deletions

View File

@@ -0,0 +1,13 @@
---
"@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
---
Adding `trustPolicyIgnoreAfter` allows you to ignore trust policy checks for packages published more than a specified time ago[#10352](https://github.com/pnpm/pnpm/issues/10352).

View File

@@ -236,6 +236,7 @@ export interface Config extends OptionsFromRootManifest {
fetchMinSpeedKiBps?: number fetchMinSpeedKiBps?: number
trustPolicy?: TrustPolicy trustPolicy?: TrustPolicy
trustPolicyExclude?: string[] trustPolicyExclude?: string[]
trustPolicyIgnoreAfter?: number
packageConfigs?: ProjectConfigSet packageConfigs?: ProjectConfigSet
} }

View File

@@ -134,6 +134,7 @@ export const excludedPnpmKeys = [
'strict-peer-dependencies', 'strict-peer-dependencies',
'trust-policy', 'trust-policy',
'trust-policy-exclude', 'trust-policy-exclude',
'trust-policy-ignore-after',
'use-node-version', 'use-node-version',
'use-stderr', 'use-stderr',
'verify-deps-before-run', 'verify-deps-before-run',

View File

@@ -117,6 +117,7 @@ export const pnpmTypes = {
'strict-peer-dependencies': Boolean, 'strict-peer-dependencies': Boolean,
'trust-policy': ['off', 'no-downgrade'] satisfies TrustPolicy[], 'trust-policy': ['off', 'no-downgrade'] satisfies TrustPolicy[],
'trust-policy-exclude': [String, Array], 'trust-policy-exclude': [String, Array],
'trust-policy-ignore-after': Number,
'use-beta-cli': Boolean, 'use-beta-cli': Boolean,
'use-node-version': String, 'use-node-version': String,
'use-running-store-server': Boolean, 'use-running-store-server': Boolean,

View File

@@ -171,6 +171,7 @@ export interface StrictInstallOptions {
minimumReleaseAgeExclude?: string[] minimumReleaseAgeExclude?: string[]
trustPolicy?: TrustPolicy trustPolicy?: TrustPolicy
trustPolicyExclude?: string[] trustPolicyExclude?: string[]
trustPolicyIgnoreAfter?: number
blockExoticSubdeps?: boolean blockExoticSubdeps?: boolean
} }

View File

@@ -1243,6 +1243,7 @@ const _installInContext: InstallFunction = async (projects, ctx, opts) => {
minimumReleaseAgeExclude: opts.minimumReleaseAgeExclude, minimumReleaseAgeExclude: opts.minimumReleaseAgeExclude,
trustPolicy: opts.trustPolicy, trustPolicy: opts.trustPolicy,
trustPolicyExclude: opts.trustPolicyExclude, trustPolicyExclude: opts.trustPolicyExclude,
trustPolicyIgnoreAfter: opts.trustPolicyIgnoreAfter,
blockExoticSubdeps: opts.blockExoticSubdeps, blockExoticSubdeps: opts.blockExoticSubdeps,
} }
) )

View File

@@ -209,6 +209,7 @@ async function resolveAndFetch (
defaultTag: options.defaultTag, defaultTag: options.defaultTag,
trustPolicy: options.trustPolicy, trustPolicy: options.trustPolicy,
trustPolicyExclude: options.trustPolicyExclude, trustPolicyExclude: options.trustPolicyExclude,
trustPolicyIgnoreAfter: options.trustPolicyIgnoreAfter,
publishedBy: options.publishedBy, publishedBy: options.publishedBy,
publishedByExclude: options.publishedByExclude, publishedByExclude: options.publishedByExclude,
pickLowestVersion: options.pickLowestVersion, pickLowestVersion: options.pickLowestVersion,

View File

@@ -79,6 +79,7 @@ export function rcOptionsTypes (): Record<string, unknown> {
'strict-peer-dependencies', 'strict-peer-dependencies',
'trust-policy', 'trust-policy',
'trust-policy-exclude', 'trust-policy-exclude',
'trust-policy-ignore-after',
'unsafe-perm', 'unsafe-perm',
'offline', 'offline',
'only', 'only',

View File

@@ -64,6 +64,7 @@ export function rcOptionsTypes (): Record<string, unknown> {
'strict-peer-dependencies', 'strict-peer-dependencies',
'trust-policy', 'trust-policy',
'trust-policy-exclude', 'trust-policy-exclude',
'trust-policy-ignore-after',
'offline', 'offline',
'only', 'only',
'optional', 'optional',
@@ -212,6 +213,10 @@ by any dependencies, so it is an emulation of a flat node_modules',
description: 'Exclude specific packages from trust policy checks', description: 'Exclude specific packages from trust policy checks',
name: '--trust-policy-exclude <package-spec>', name: '--trust-policy-exclude <package-spec>',
}, },
{
description: 'Ignore trust downgrades for packages published more than specified minutes ago',
name: '--trust-policy-ignore-after <minutes>',
},
{ {
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`', 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', name: '--use-store-server',

View File

@@ -185,6 +185,7 @@ export interface ResolutionContext {
publishedByExclude?: PackageVersionPolicy publishedByExclude?: PackageVersionPolicy
trustPolicy?: TrustPolicy trustPolicy?: TrustPolicy
trustPolicyExclude?: PackageVersionPolicy trustPolicyExclude?: PackageVersionPolicy
trustPolicyIgnoreAfter?: number
blockExoticSubdeps?: boolean blockExoticSubdeps?: boolean
} }
@@ -1343,6 +1344,7 @@ async function resolveDependency (
skipFetch: ctx.dryRun, skipFetch: ctx.dryRun,
trustPolicy: ctx.trustPolicy, trustPolicy: ctx.trustPolicy,
trustPolicyExclude: ctx.trustPolicyExclude, trustPolicyExclude: ctx.trustPolicyExclude,
trustPolicyIgnoreAfter: ctx.trustPolicyIgnoreAfter,
update: options.update, update: options.update,
workspacePackages: ctx.workspacePackages, workspacePackages: ctx.workspacePackages,
supportedArchitectures: options.supportedArchitectures, supportedArchitectures: options.supportedArchitectures,

View File

@@ -143,6 +143,7 @@ export interface ResolveDependenciesOptions {
minimumReleaseAgeExclude?: string[] minimumReleaseAgeExclude?: string[]
trustPolicy?: TrustPolicy trustPolicy?: TrustPolicy
trustPolicyExclude?: string[] trustPolicyExclude?: string[]
trustPolicyIgnoreAfter?: number
blockExoticSubdeps?: boolean blockExoticSubdeps?: boolean
} }
@@ -209,6 +210,7 @@ export async function resolveDependencyTree<T> (
publishedByExclude: opts.minimumReleaseAgeExclude ? createPackageVersionPolicyByExclude(opts.minimumReleaseAgeExclude, 'minimumReleaseAgeExclude') : undefined, publishedByExclude: opts.minimumReleaseAgeExclude ? createPackageVersionPolicyByExclude(opts.minimumReleaseAgeExclude, 'minimumReleaseAgeExclude') : undefined,
trustPolicy: opts.trustPolicy, trustPolicy: opts.trustPolicy,
trustPolicyExclude: opts.trustPolicyExclude ? createPackageVersionPolicyByExclude(opts.trustPolicyExclude, 'trustPolicyExclude') : undefined, trustPolicyExclude: opts.trustPolicyExclude ? createPackageVersionPolicyByExclude(opts.trustPolicyExclude, 'trustPolicyExclude') : undefined,
trustPolicyIgnoreAfter: opts.trustPolicyIgnoreAfter,
blockExoticSubdeps: opts.blockExoticSubdeps, blockExoticSubdeps: opts.blockExoticSubdeps,
} }

View File

@@ -563,3 +563,15 @@ test('install fails when trust evidence of an optional dependency is downgraded'
expect(result.stdout.toString()).toContain('ERR_PNPM_TRUST_DOWNGRADE') expect(result.stdout.toString()).toContain('ERR_PNPM_TRUST_DOWNGRADE')
expect(result.status).toBe(1) expect(result.status).toBe(1)
}) })
test('install does not fail when the trust evidence of a package is downgraded but the trust-policy-ignore-after is set', async () => {
const project = prepare()
const result = execPnpmSync([
'add',
'@pnpm/e2e.test-provenance@0.0.5',
'--trust-policy=no-downgrade',
'--trust-policy-ignore-after=1440', // 1 day
])
expect(result.status).toBe(0)
project.has('@pnpm/e2e.test-provenance')
})

View File

@@ -214,6 +214,7 @@ export type ResolveFromNpmOptions = {
pickLowestVersion?: boolean pickLowestVersion?: boolean
trustPolicy?: TrustPolicy trustPolicy?: TrustPolicy
trustPolicyExclude?: PackageVersionPolicy trustPolicyExclude?: PackageVersionPolicy
trustPolicyIgnoreAfter?: number
dryRun?: boolean dryRun?: boolean
lockfileDir?: string lockfileDir?: string
preferredVersions?: PreferredVersions preferredVersions?: PreferredVersions
@@ -333,7 +334,7 @@ async function resolveNpm (
} }
throw new NoMatchingVersionError({ wantedDependency, packageMeta: meta, registry }) throw new NoMatchingVersionError({ wantedDependency, packageMeta: meta, registry })
} else if (opts.trustPolicy === 'no-downgrade') { } else if (opts.trustPolicy === 'no-downgrade') {
failIfTrustDowngraded(meta, pickedPackage.version, opts.trustPolicyExclude) failIfTrustDowngraded(meta, pickedPackage.version, opts)
} }
const workspacePkgsMatchingName = workspacePackages?.get(pickedPackage.name) const workspacePkgsMatchingName = workspacePackages?.get(pickedPackage.name)

View File

@@ -14,10 +14,13 @@ const TRUST_RANK = {
export function failIfTrustDowngraded ( export function failIfTrustDowngraded (
meta: PackageMeta, meta: PackageMeta,
version: string, version: string,
trustPolicyExclude?: PackageVersionPolicy opts?: {
trustPolicyExclude?: PackageVersionPolicy
trustPolicyIgnoreAfter?: number
}
): void { ): void {
if (trustPolicyExclude) { if (opts?.trustPolicyExclude) {
const excludeResult = trustPolicyExclude(meta.name) const excludeResult = opts.trustPolicyExclude(meta.name)
if (excludeResult === true) { if (excludeResult === true) {
return return
} }
@@ -37,6 +40,13 @@ export function failIfTrustDowngraded (
} }
const versionDate = new Date(versionPublishedAt) const versionDate = new Date(versionPublishedAt)
if (opts?.trustPolicyIgnoreAfter) {
const now = new Date()
const minutesSincePublish = (now.getTime() - versionDate.getTime()) / (1000 * 60)
if (minutesSincePublish > opts.trustPolicyIgnoreAfter) {
return
}
}
const manifest = meta.versions[version] const manifest = meta.versions[version]
if (!manifest) { if (!manifest) {
throw new PnpmError( throw new PnpmError(

View File

@@ -488,7 +488,7 @@ describe('failIfTrustDowngraded with trustPolicyExclude', () => {
} }
expect(() => { expect(() => {
failIfTrustDowngraded(meta, '3.0.0', createPackageVersionPolicy(['foo@3.0.0'])) failIfTrustDowngraded(meta, '3.0.0', { trustPolicyExclude: createPackageVersionPolicy(['foo@3.0.0']) })
}).not.toThrow() }).not.toThrow()
expect(() => { expect(() => {
@@ -533,7 +533,7 @@ describe('failIfTrustDowngraded with trustPolicyExclude', () => {
} }
expect(() => { expect(() => {
failIfTrustDowngraded(meta, '3.0.0', createPackageVersionPolicy(['bar'])) failIfTrustDowngraded(meta, '3.0.0', { trustPolicyExclude: createPackageVersionPolicy(['bar']) })
}).not.toThrow() }).not.toThrow()
}) })
@@ -555,7 +555,7 @@ describe('failIfTrustDowngraded with trustPolicyExclude', () => {
} }
expect(() => { expect(() => {
failIfTrustDowngraded(meta, '1.0.0', createPackageVersionPolicy(['baz@1.0.0'])) failIfTrustDowngraded(meta, '1.0.0', { trustPolicyExclude: createPackageVersionPolicy(['baz@1.0.0']) })
}).not.toThrow() }).not.toThrow()
}) })
@@ -585,7 +585,51 @@ describe('failIfTrustDowngraded with trustPolicyExclude', () => {
} }
expect(() => { expect(() => {
failIfTrustDowngraded(meta, '2.0.0', createPackageVersionPolicy(['qux'])) failIfTrustDowngraded(meta, '2.0.0', { trustPolicyExclude: createPackageVersionPolicy(['qux']) })
}).not.toThrow() }).not.toThrow()
}) })
}) })
describe('failIfTrustDowngraded with trustPolicyIgnoreAfter', () => {
test('allows downgrade when version is older than ignoreAfter threshold', () => {
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', { trustPolicyIgnoreAfter: 60 * 24 * 30 }) // 30 days
}).not.toThrow()
expect(() => {
failIfTrustDowngraded(meta, '3.0.0')
}).toThrow('High-risk trust downgrade')
})
})

View File

@@ -116,6 +116,7 @@ export interface ResolveOptions {
alwaysTryWorkspacePackages?: boolean alwaysTryWorkspacePackages?: boolean
trustPolicy?: TrustPolicy trustPolicy?: TrustPolicy
trustPolicyExclude?: PackageVersionPolicy trustPolicyExclude?: PackageVersionPolicy
trustPolicyIgnoreAfter?: number
defaultTag?: string defaultTag?: string
pickLowestVersion?: boolean pickLowestVersion?: boolean
publishedBy?: Date publishedBy?: Date

View File

@@ -143,6 +143,7 @@ export interface RequestPackageOptions {
pinnedVersion?: PinnedVersion pinnedVersion?: PinnedVersion
trustPolicy?: TrustPolicy trustPolicy?: TrustPolicy
trustPolicyExclude?: PackageVersionPolicy trustPolicyExclude?: PackageVersionPolicy
trustPolicyIgnoreAfter?: number
} }
export type BundledManifestFunction = () => Promise<BundledManifest | undefined> export type BundledManifestFunction = () => Promise<BundledManifest | undefined>