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 Zoltan Kochan
parent b1ea7e09bd
commit 3f2c5f4d39
16 changed files with 105 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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -65,6 +65,7 @@ export function rcOptionsTypes (): Record<string, unknown> {
'strict-peer-dependencies',
'trust-policy',
'trust-policy-exclude',
'trust-policy-ignore-after',
'offline',
'only',
'optional',
@@ -213,6 +214,10 @@ by any dependencies, so it is an emulation of a flat node_modules',
description: 'Exclude specific packages from trust policy checks',
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`',
name: '--use-store-server',

View File

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

View File

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

View File

@@ -559,3 +559,15 @@ test('install fails when trust evidence of an optional dependency is downgraded'
expect(result.stdout.toString()).toContain('ERR_PNPM_TRUST_DOWNGRADE')
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
trustPolicy?: TrustPolicy
trustPolicyExclude?: PackageVersionPolicy
trustPolicyIgnoreAfter?: number
dryRun?: boolean
lockfileDir?: string
preferredVersions?: PreferredVersions
@@ -333,7 +334,7 @@ async function resolveNpm (
}
throw new NoMatchingVersionError({ wantedDependency, packageMeta: meta, registry })
} else if (opts.trustPolicy === 'no-downgrade') {
failIfTrustDowngraded(meta, pickedPackage.version, opts.trustPolicyExclude)
failIfTrustDowngraded(meta, pickedPackage.version, opts)
}
const workspacePkgsMatchingName = workspacePackages?.get(pickedPackage.name)

View File

@@ -14,10 +14,13 @@ const TRUST_RANK = {
export function failIfTrustDowngraded (
meta: PackageMeta,
version: string,
trustPolicyExclude?: PackageVersionPolicy
opts?: {
trustPolicyExclude?: PackageVersionPolicy
trustPolicyIgnoreAfter?: number
}
): void {
if (trustPolicyExclude) {
const excludeResult = trustPolicyExclude(meta.name)
if (opts?.trustPolicyExclude) {
const excludeResult = opts.trustPolicyExclude(meta.name)
if (excludeResult === true) {
return
}
@@ -37,6 +40,13 @@ export function failIfTrustDowngraded (
}
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]
if (!manifest) {
throw new PnpmError(

View File

@@ -488,7 +488,7 @@ describe('failIfTrustDowngraded with trustPolicyExclude', () => {
}
expect(() => {
failIfTrustDowngraded(meta, '3.0.0', createPackageVersionPolicy(['foo@3.0.0']))
failIfTrustDowngraded(meta, '3.0.0', { trustPolicyExclude: createPackageVersionPolicy(['foo@3.0.0']) })
}).not.toThrow()
expect(() => {
@@ -533,7 +533,7 @@ describe('failIfTrustDowngraded with trustPolicyExclude', () => {
}
expect(() => {
failIfTrustDowngraded(meta, '3.0.0', createPackageVersionPolicy(['bar']))
failIfTrustDowngraded(meta, '3.0.0', { trustPolicyExclude: createPackageVersionPolicy(['bar']) })
}).not.toThrow()
})
@@ -555,7 +555,7 @@ describe('failIfTrustDowngraded with trustPolicyExclude', () => {
}
expect(() => {
failIfTrustDowngraded(meta, '1.0.0', createPackageVersionPolicy(['baz@1.0.0']))
failIfTrustDowngraded(meta, '1.0.0', { trustPolicyExclude: createPackageVersionPolicy(['baz@1.0.0']) })
}).not.toThrow()
})
@@ -585,7 +585,51 @@ describe('failIfTrustDowngraded with trustPolicyExclude', () => {
}
expect(() => {
failIfTrustDowngraded(meta, '2.0.0', createPackageVersionPolicy(['qux']))
failIfTrustDowngraded(meta, '2.0.0', { trustPolicyExclude: createPackageVersionPolicy(['qux']) })
}).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

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

View File

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