From facdd717bfee5ae3d6832a30d2a54f26cc0411cc Mon Sep 17 00:00:00 2001 From: btea <2356281422@qq.com> Date: Sun, 28 Dec 2025 09:01:09 +0800 Subject: [PATCH] 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 --- .changeset/big-lies-pump.md | 13 +++++ config/config/src/Config.ts | 1 + config/config/src/configFileKey.ts | 1 + config/config/src/types.ts | 1 + .../core/src/install/extendInstallOptions.ts | 1 + pkg-manager/core/src/install/index.ts | 1 + .../package-requester/src/packageRequester.ts | 1 + .../plugin-commands-installation/src/add.ts | 1 + .../src/install.ts | 5 ++ .../src/resolveDependencies.ts | 2 + .../src/resolveDependencyTree.ts | 2 + pnpm/test/install/misc.ts | 12 +++++ resolving/npm-resolver/src/index.ts | 3 +- resolving/npm-resolver/src/trustChecks.ts | 16 ++++-- .../npm-resolver/test/trustChecks.test.ts | 52 +++++++++++++++++-- resolving/resolver-base/src/index.ts | 1 + store/store-controller-types/src/index.ts | 1 + 17 files changed, 106 insertions(+), 8 deletions(-) create mode 100644 .changeset/big-lies-pump.md diff --git a/.changeset/big-lies-pump.md b/.changeset/big-lies-pump.md new file mode 100644 index 0000000000..d298f28e87 --- /dev/null +++ b/.changeset/big-lies-pump.md @@ -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). diff --git a/config/config/src/Config.ts b/config/config/src/Config.ts index 8fb7a7e6ec..f564f6eb37 100644 --- a/config/config/src/Config.ts +++ b/config/config/src/Config.ts @@ -236,6 +236,7 @@ export interface Config extends OptionsFromRootManifest { fetchMinSpeedKiBps?: number trustPolicy?: TrustPolicy trustPolicyExclude?: string[] + trustPolicyIgnoreAfter?: number packageConfigs?: ProjectConfigSet } diff --git a/config/config/src/configFileKey.ts b/config/config/src/configFileKey.ts index ea2926a461..db9001fc57 100644 --- a/config/config/src/configFileKey.ts +++ b/config/config/src/configFileKey.ts @@ -134,6 +134,7 @@ export const excludedPnpmKeys = [ 'strict-peer-dependencies', 'trust-policy', 'trust-policy-exclude', + 'trust-policy-ignore-after', 'use-node-version', 'use-stderr', 'verify-deps-before-run', diff --git a/config/config/src/types.ts b/config/config/src/types.ts index e807f42904..c5ad9517cd 100644 --- a/config/config/src/types.ts +++ b/config/config/src/types.ts @@ -117,6 +117,7 @@ export const pnpmTypes = { '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, diff --git a/pkg-manager/core/src/install/extendInstallOptions.ts b/pkg-manager/core/src/install/extendInstallOptions.ts index 3688b678b6..4fcc71177e 100644 --- a/pkg-manager/core/src/install/extendInstallOptions.ts +++ b/pkg-manager/core/src/install/extendInstallOptions.ts @@ -171,6 +171,7 @@ export interface StrictInstallOptions { minimumReleaseAgeExclude?: string[] trustPolicy?: TrustPolicy trustPolicyExclude?: string[] + trustPolicyIgnoreAfter?: number blockExoticSubdeps?: boolean } diff --git a/pkg-manager/core/src/install/index.ts b/pkg-manager/core/src/install/index.ts index 4d800d3713..4dd988f321 100644 --- a/pkg-manager/core/src/install/index.ts +++ b/pkg-manager/core/src/install/index.ts @@ -1243,6 +1243,7 @@ const _installInContext: InstallFunction = async (projects, ctx, opts) => { minimumReleaseAgeExclude: opts.minimumReleaseAgeExclude, trustPolicy: opts.trustPolicy, trustPolicyExclude: opts.trustPolicyExclude, + trustPolicyIgnoreAfter: opts.trustPolicyIgnoreAfter, blockExoticSubdeps: opts.blockExoticSubdeps, } ) diff --git a/pkg-manager/package-requester/src/packageRequester.ts b/pkg-manager/package-requester/src/packageRequester.ts index 2f8dac9618..d8b3e25adc 100644 --- a/pkg-manager/package-requester/src/packageRequester.ts +++ b/pkg-manager/package-requester/src/packageRequester.ts @@ -209,6 +209,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, diff --git a/pkg-manager/plugin-commands-installation/src/add.ts b/pkg-manager/plugin-commands-installation/src/add.ts index f484871a8c..05af3875c0 100644 --- a/pkg-manager/plugin-commands-installation/src/add.ts +++ b/pkg-manager/plugin-commands-installation/src/add.ts @@ -79,6 +79,7 @@ export function rcOptionsTypes (): Record { 'strict-peer-dependencies', 'trust-policy', 'trust-policy-exclude', + 'trust-policy-ignore-after', 'unsafe-perm', 'offline', 'only', diff --git a/pkg-manager/plugin-commands-installation/src/install.ts b/pkg-manager/plugin-commands-installation/src/install.ts index 777e446ad8..3b2028862d 100644 --- a/pkg-manager/plugin-commands-installation/src/install.ts +++ b/pkg-manager/plugin-commands-installation/src/install.ts @@ -64,6 +64,7 @@ export function rcOptionsTypes (): Record { 'strict-peer-dependencies', 'trust-policy', 'trust-policy-exclude', + 'trust-policy-ignore-after', 'offline', 'only', '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', name: '--trust-policy-exclude ', }, + { + description: 'Ignore trust downgrades for packages published more than specified minutes ago', + name: '--trust-policy-ignore-after ', + }, { 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', diff --git a/pkg-manager/resolve-dependencies/src/resolveDependencies.ts b/pkg-manager/resolve-dependencies/src/resolveDependencies.ts index 6fc299aac4..3fa0f33698 100644 --- a/pkg-manager/resolve-dependencies/src/resolveDependencies.ts +++ b/pkg-manager/resolve-dependencies/src/resolveDependencies.ts @@ -185,6 +185,7 @@ export interface ResolutionContext { publishedByExclude?: PackageVersionPolicy trustPolicy?: TrustPolicy trustPolicyExclude?: PackageVersionPolicy + trustPolicyIgnoreAfter?: number blockExoticSubdeps?: boolean } @@ -1343,6 +1344,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, diff --git a/pkg-manager/resolve-dependencies/src/resolveDependencyTree.ts b/pkg-manager/resolve-dependencies/src/resolveDependencyTree.ts index 89cde2db80..407ac15612 100644 --- a/pkg-manager/resolve-dependencies/src/resolveDependencyTree.ts +++ b/pkg-manager/resolve-dependencies/src/resolveDependencyTree.ts @@ -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 ( 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, } diff --git a/pnpm/test/install/misc.ts b/pnpm/test/install/misc.ts index 0027667896..d72b3d0ac0 100644 --- a/pnpm/test/install/misc.ts +++ b/pnpm/test/install/misc.ts @@ -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.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') +}) diff --git a/resolving/npm-resolver/src/index.ts b/resolving/npm-resolver/src/index.ts index 3fd68d1eae..06a3aa89c4 100644 --- a/resolving/npm-resolver/src/index.ts +++ b/resolving/npm-resolver/src/index.ts @@ -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) diff --git a/resolving/npm-resolver/src/trustChecks.ts b/resolving/npm-resolver/src/trustChecks.ts index da5dc55bb6..2ec432d03f 100644 --- a/resolving/npm-resolver/src/trustChecks.ts +++ b/resolving/npm-resolver/src/trustChecks.ts @@ -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( diff --git a/resolving/npm-resolver/test/trustChecks.test.ts b/resolving/npm-resolver/test/trustChecks.test.ts index 3cb8e8844a..a496961395 100644 --- a/resolving/npm-resolver/test/trustChecks.test.ts +++ b/resolving/npm-resolver/test/trustChecks.test.ts @@ -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') + }) +}) diff --git a/resolving/resolver-base/src/index.ts b/resolving/resolver-base/src/index.ts index b526b2a5ca..21233dfebe 100644 --- a/resolving/resolver-base/src/index.ts +++ b/resolving/resolver-base/src/index.ts @@ -116,6 +116,7 @@ export interface ResolveOptions { alwaysTryWorkspacePackages?: boolean trustPolicy?: TrustPolicy trustPolicyExclude?: PackageVersionPolicy + trustPolicyIgnoreAfter?: number defaultTag?: string pickLowestVersion?: boolean publishedBy?: Date diff --git a/store/store-controller-types/src/index.ts b/store/store-controller-types/src/index.ts index 6fc5e12224..d47a6ca720 100644 --- a/store/store-controller-types/src/index.ts +++ b/store/store-controller-types/src/index.ts @@ -143,6 +143,7 @@ export interface RequestPackageOptions { pinnedVersion?: PinnedVersion trustPolicy?: TrustPolicy trustPolicyExclude?: PackageVersionPolicy + trustPolicyIgnoreAfter?: number } export type BundledManifestFunction = () => Promise