feat: add support for npm package trust evidence check via a new trustPolicy setting (#10103)

close #8889

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
This commit is contained in:
Ryo Matsukawa
2025-11-10 07:23:58 +09:00
committed by Zoltan Kochan
parent a7cf08797e
commit 68ad0868b4
23 changed files with 616 additions and 5 deletions

View File

@@ -0,0 +1,16 @@
---
"@pnpm/plugin-commands-installation": minor
"@pnpm/plugin-commands-script-runners": minor
"@pnpm/resolve-dependencies": minor
"@pnpm/store-connection-manager": minor
"@pnpm/package-requester": minor
"@pnpm/store-controller-types": minor
"@pnpm/resolver-base": minor
"@pnpm/npm-resolver": minor
"@pnpm/core": minor
"@pnpm/types": minor
"@pnpm/registry.types": minor
"@pnpm/config": minor
---
Added a new setting: `trustPolicy`.

View File

@@ -0,0 +1,10 @@
---
"pnpm": minor
---
Added a new setting: `trustPolicy`.
When set to `no-downgrade`, pnpm will fail installation if a packages trust level has decreased compared to previous releases — for example, if it was previously published by a trusted publisher but now only has provenance or no trust evidence.
This helps prevent installing potentially compromised versions of a package.
Related issue: [#8889](https://github.com/pnpm/pnpm/issues/8889).

View File

@@ -6,6 +6,7 @@ import {
type ProjectsGraph,
type Registries,
type SslConfig,
type TrustPolicy,
} from '@pnpm/types'
import type { Hooks } from '@pnpm/pnpmfile'
import { type OptionsFromRootManifest } from './getOptionsFromRootManifest.js'
@@ -232,6 +233,7 @@ export interface Config extends OptionsFromRootManifest {
minimumReleaseAgeExclude?: string[]
fetchWarnTimeoutMs?: number
fetchMinSpeedKiBps?: number
trustPolicy?: TrustPolicy
}
export interface ConfigWithDeprecatedSettings extends Config {

View File

@@ -1,4 +1,5 @@
import npmTypes from '@pnpm/npm-conf/lib/types'
import { type TrustPolicy } from '@pnpm/types'
export const types = Object.assign({
'auto-install-peers': Boolean,
@@ -113,6 +114,7 @@ export const types = Object.assign({
'strict-dep-builds': Boolean,
'strict-store-pkg-content-check': Boolean,
'strict-peer-dependencies': Boolean,
'trust-policy': ['off', 'no-downgrade'] satisfies TrustPolicy[],
'use-beta-cli': Boolean,
'use-node-version': String,
'use-running-store-server': Boolean,

View File

@@ -85,7 +85,13 @@ export async function handler (
[command, ...args]: string[]
): Promise<{ exitCode: number }> {
const pkgs = opts.package ?? [command]
const fullMetadata = ((opts.resolutionMode === 'time-based' || Boolean(opts.minimumReleaseAge)) && !opts.registrySupportsTimeField)
const fullMetadata = (
(
opts.resolutionMode === 'time-based' ||
Boolean(opts.minimumReleaseAge) ||
opts.trustPolicy === 'no-downgrade'
) && !opts.registrySupportsTimeField
)
const { resolve } = createResolver({
...opts,
authConfig: opts.rawConfig,

View File

@@ -1,3 +1,5 @@
export type PackageVersionPolicy = (pkgName: string) => boolean | string[]
export type AllowBuild = (pkgName: string, pkgVersion: string) => boolean
export type TrustPolicy = 'no-downgrade' | 'off'

View File

@@ -18,6 +18,7 @@ import {
type ReadPackageHook,
type Registries,
type PrepareExecutionEnv,
type TrustPolicy,
} from '@pnpm/types'
import { parseOverrides, type VersionOverride } from '@pnpm/parse-overrides'
import { pnpmPkgJson } from '../pnpmPkgJson.js'
@@ -167,6 +168,7 @@ export interface StrictInstallOptions {
ci?: boolean
minimumReleaseAge?: number
minimumReleaseAgeExclude?: string[]
trustPolicy?: TrustPolicy
}
export type InstallOptions =

View File

@@ -1181,6 +1181,7 @@ const _installInContext: InstallFunction = async (projects, ctx, opts) => {
injectWorkspacePackages: opts.injectWorkspacePackages,
minimumReleaseAge: opts.minimumReleaseAge,
minimumReleaseAgeExclude: opts.minimumReleaseAgeExclude,
trustPolicy: opts.trustPolicy,
}
)
if (!opts.include.optionalDependencies || !opts.include.devDependencies || !opts.include.dependencies) {

View File

@@ -209,6 +209,7 @@ async function resolveAndFetch (
const resolveResult = await ctx.requestsQueue.add<ResolveResult>(async () => ctx.resolve(wantedDependency, {
alwaysTryWorkspacePackages: options.alwaysTryWorkspacePackages,
defaultTag: options.defaultTag,
trustPolicy: options.trustPolicy,
publishedBy: options.publishedBy,
publishedByExclude: options.publishedByExclude,
pickLowestVersion: options.pickLowestVersion,

View File

@@ -74,6 +74,7 @@ export function rcOptionsTypes (): Record<string, unknown> {
'side-effects-cache',
'store-dir',
'strict-peer-dependencies',
'trust-policy',
'unsafe-perm',
'offline',
'only',

View File

@@ -63,6 +63,7 @@ export function rcOptionsTypes (): Record<string, unknown> {
'side-effects-cache',
'store-dir',
'strict-peer-dependencies',
'trust-policy',
'offline',
'only',
'optional',
@@ -203,6 +204,10 @@ by any dependencies, so it is an emulation of a flat node_modules',
description: 'Fail on missing or invalid peer dependencies',
name: '--strict-peer-dependencies',
},
{
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: '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

@@ -40,6 +40,7 @@ import {
type PkgIdWithPatchHash,
type PinnedVersion,
type PackageVersionPolicy,
type TrustPolicy,
} from '@pnpm/types'
import * as dp from '@pnpm/dependency-path'
import { getPreferredVersionsFromLockfileAndManifests } from '@pnpm/lockfile.preferred-versions'
@@ -182,6 +183,7 @@ export interface ResolutionContext {
hoistPeers?: boolean
maximumPublishedBy?: Date
publishedByExclude?: PackageVersionPolicy
trustPolicy?: TrustPolicy
}
export interface MissingPeerInfo {
@@ -1336,6 +1338,7 @@ async function resolveDependency (
? ctx.lockfileDir
: options.parentPkg.rootDir,
skipFetch: ctx.dryRun,
trustPolicy: ctx.trustPolicy,
update: options.update,
workspacePackages: ctx.workspacePackages,
supportedArchitectures: options.supportedArchitectures,

View File

@@ -18,6 +18,7 @@ import {
type Registries,
type ProjectRootDir,
type PackageVersionPolicy,
type TrustPolicy,
} from '@pnpm/types'
import partition from 'ramda/src/partition'
import zipObj from 'ramda/src/zipObj'
@@ -138,6 +139,7 @@ export interface ResolveDependenciesOptions {
peersSuffixMaxLength: number
minimumReleaseAge?: number
minimumReleaseAgeExclude?: string[]
trustPolicy?: TrustPolicy
}
export interface ResolveDependencyTreeResult {
@@ -200,6 +202,7 @@ export async function resolveDependencyTree<T> (
allPeerDepNames: new Set(),
maximumPublishedBy: opts.minimumReleaseAge ? new Date(Date.now() - opts.minimumReleaseAge * 60 * 1000) : undefined,
publishedByExclude: opts.minimumReleaseAgeExclude ? createPublishedByExclude(opts.minimumReleaseAgeExclude) : undefined,
trustPolicy: opts.trustPolicy,
}
function createPublishedByExclude (patterns: string[]): PackageVersionPolicy {

View File

@@ -502,3 +502,25 @@ test('install success even though the url\'s hash contains slash', async () => {
])
expect(result.status).toBe(0)
})
test('install fails when the trust evidence of a package is downgraded', async () => {
const project = prepare()
const result = execPnpmSync([
'add',
'@pnpm/e2e.test-provenance@0.0.5',
'--trust-policy=no-downgrade',
])
expect(result.status).toBe(1)
project.hasNot('@pnpm/e2e.test-provenance')
})
test('install does not fail when the trust evidence of a package is downgraded but trust-policy is turned off', async () => {
const project = prepare()
const result = execPnpmSync([
'add',
'@pnpm/e2e.test-provenance@0.0.5',
'--trust-policy=off',
])
expect(result.status).toBe(0)
project.has('@pnpm/e2e.test-provenance')
})

View File

@@ -25,9 +25,22 @@ export type PackageMetaTime = Record<string, string> & {
export interface PackageInRegistry extends PackageManifest {
hasInstallScript?: boolean
_npmUser?: {
name?: string
email?: string
trustedPublisher?: {
id: string
oidcConfigId: string
}
}
dist: {
integrity?: string
shasum: string
tarball: string
attestations?: {
provenance?: {
predicateType: string
}
}
}
}

View File

@@ -20,7 +20,13 @@ import {
type WorkspacePackages,
type WorkspacePackagesByVersion,
} from '@pnpm/resolver-base'
import { type DependencyManifest, type Registries, type PinnedVersion, type PackageVersionPolicy } from '@pnpm/types'
import {
type DependencyManifest,
type PackageVersionPolicy,
type PinnedVersion,
type Registries,
type TrustPolicy,
} from '@pnpm/types'
import { LRUCache } from 'lru-cache'
import normalize from 'normalize-path'
import pMemoize from 'p-memoize'
@@ -42,7 +48,8 @@ import {
import { fetchMetadataFromFromRegistry, type FetchMetadataFromFromRegistryOptions, RegistryResponseError } from './fetch.js'
import { workspacePrefToNpm } from './workspacePrefToNpm.js'
import { whichVersionIsPinned } from './whichVersionIsPinned.js'
import { pickVersionByVersionRange } from './pickPackageFromMeta.js'
import { pickVersionByVersionRange, assertMetaHasTime } from './pickPackageFromMeta.js'
import { failIfTrustDowngraded } from './trustChecks.js'
export interface NoMatchingVersionErrorOptions {
wantedDependency: WantedDependency
@@ -180,6 +187,7 @@ export type ResolveFromNpmOptions = {
publishedBy?: Date
publishedByExclude?: PackageVersionPolicy
pickLowestVersion?: boolean
trustPolicy?: TrustPolicy
dryRun?: boolean
lockfileDir?: string
preferredVersions?: PreferredVersions
@@ -298,6 +306,9 @@ async function resolveNpm (
}
}
throw new NoMatchingVersionError({ wantedDependency, packageMeta: meta, registry })
} else if (opts.trustPolicy === 'no-downgrade') {
assertMetaHasTime(meta)
failIfTrustDowngraded(meta, pickedPackage.version)
}
const workspacePkgsMatchingName = workspacePackages?.get(pickedPackage.name)

View File

@@ -262,6 +262,7 @@ function clearMeta (pkg: PackageMeta): PackageMeta {
'bundleDependencies',
'bundledDependencies',
'hasInstallScript',
'_npmUser',
], info)
}

View File

@@ -93,7 +93,7 @@ export function pickPackageFromMeta (
}
}
function assertMetaHasTime (meta: PackageMeta): asserts meta is PackageMetaWithTime {
export function assertMetaHasTime (meta: PackageMeta): asserts meta is PackageMetaWithTime {
if (meta.time == null) {
throw new PnpmError('MISSING_TIME', `The metadata of ${meta.name} is missing the "time" field`)
}

View File

@@ -0,0 +1,92 @@
import { PnpmError } from '@pnpm/error'
import { type PackageInRegistry, type PackageMetaWithTime } from '@pnpm/registry.types'
type TrustEvidence = 'provenance' | 'trustedPublisher'
const TRUST_RANK = {
trustedPublisher: 2,
provenance: 1,
} as const satisfies Record<TrustEvidence, number>
export function failIfTrustDowngraded (
meta: PackageMetaWithTime,
version: string
): void {
const versionPublishedAt = meta.time[version]
if (!versionPublishedAt) {
throw new PnpmError(
'TRUST_CHECK_FAIL',
`Missing time for version ${version} of ${meta.name} in metadata`
)
}
const versionDate = new Date(versionPublishedAt)
const manifest = meta.versions[version]
if (!manifest) {
throw new PnpmError(
'TRUST_CHECK_FAIL',
`Missing version object for version ${version} of ${meta.name} in metadata`
)
}
const strongestEvidencePriorToRequestedVersion = detectStrongestTrustEvidenceBeforeDate(meta, versionDate)
if (strongestEvidencePriorToRequestedVersion == null) {
return
}
const currentTrustEvidence = getTrustEvidence(manifest)
if (currentTrustEvidence == null || TRUST_RANK[strongestEvidencePriorToRequestedVersion] > TRUST_RANK[currentTrustEvidence]) {
throw new PnpmError(
'TRUST_DOWNGRADE',
`High-risk trust downgrade for "${meta.name}@${version}" (possible package takeover)`,
{
hint: `Earlier versions had ${prettyPrintTrustEvidence(strongestEvidencePriorToRequestedVersion)}, ` +
`but this version has ${prettyPrintTrustEvidence(currentTrustEvidence)}. ` +
'A trust downgrade may indicate a supply chain incident.',
}
)
}
}
function prettyPrintTrustEvidence (trustEvidence: TrustEvidence | undefined): string {
switch (trustEvidence) {
case 'trustedPublisher': return 'trusted publisher'
case 'provenance': return 'provenance attestation'
default: return 'no trust evidence'
}
}
function detectStrongestTrustEvidenceBeforeDate (
meta: PackageMetaWithTime,
beforeDate: Date
): TrustEvidence | undefined {
let best: TrustEvidence | undefined
for (const [version, manifest] of Object.entries(meta.versions)) {
const ts = meta.time[version]
if (!ts) continue
const publishedAt = new Date(ts)
if (!(publishedAt < beforeDate)) continue
const trustEvidence = getTrustEvidence(manifest)
if (!trustEvidence) continue
if (trustEvidence === 'trustedPublisher') {
return 'trustedPublisher'
}
best ||= 'provenance'
}
return best
}
export function getTrustEvidence (manifest: PackageInRegistry): TrustEvidence | undefined {
if (manifest._npmUser?.trustedPublisher) {
return 'trustedPublisher'
}
if (manifest.dist?.attestations?.provenance) {
return 'provenance'
}
return undefined
}

View File

@@ -0,0 +1,407 @@
import { type PackageInRegistry, type PackageMetaWithTime } from '@pnpm/registry.types'
import { getTrustEvidence, failIfTrustDowngraded } from '../src/trustChecks.js'
describe('getTrustEvidence', () => {
test('returns "trustedPublisher" when _npmUser.trustedPublisher exists', () => {
const manifest: PackageInRegistry = {
name: 'foo',
version: '1.0.0',
_npmUser: {
name: 'test-publisher',
email: 'publisher@example.com',
trustedPublisher: {
id: 'test-provider',
oidcConfigId: 'oidc:test-config-123',
},
},
dist: {
shasum: 'abc123',
tarball: 'https://registry.example.com/foo/-/foo-1.0.0.tgz',
},
}
expect(getTrustEvidence(manifest)).toBe('trustedPublisher')
})
test('returns "trustedPublisher" even when attestations.provenance exists', () => {
const manifest: PackageInRegistry = {
name: 'foo',
version: '1.0.0',
_npmUser: {
name: 'test-publisher',
email: 'publisher@example.com',
trustedPublisher: {
id: 'test-provider',
oidcConfigId: 'oidc:test-config-123',
},
},
dist: {
shasum: 'abc123',
tarball: 'https://registry.example.com/foo/-/foo-2.0.0.tgz',
attestations: {
provenance: {
predicateType: 'https://slsa.dev/provenance/v1',
},
},
},
}
expect(getTrustEvidence(manifest)).toBe('trustedPublisher')
})
test('returns true when provenance exists', () => {
const manifest: PackageInRegistry = {
name: 'foo',
version: '1.0.0',
dist: {
shasum: 'abc123',
tarball: 'https://registry.example.com/foo/-/foo-1.0.0.tgz',
attestations: {
provenance: {
predicateType: 'https://slsa.dev/provenance/v1',
},
},
},
}
expect(getTrustEvidence(manifest)).toBe('provenance')
})
test('returns undefined when provenance and attestations are undefined', () => {
const manifest: PackageInRegistry = {
name: 'foo',
version: '1.0.0',
dist: {
shasum: 'abc123',
tarball: 'https://registry.example.com/foo/-/foo-1.0.0.tgz',
},
}
expect(getTrustEvidence(manifest)).toBeUndefined()
})
test('returns undefined when _npmUser exists but trustedPublisher is undefined', () => {
const manifest: PackageInRegistry = {
name: 'foo',
version: '1.0.0',
_npmUser: {
name: 'test-user',
email: 'user@example.com',
},
dist: {
shasum: 'abc123',
tarball: 'https://registry.example.com/foo/-/foo-1.0.0.tgz',
},
}
expect(getTrustEvidence(manifest)).toBeUndefined()
})
})
describe('failIfTrustDowngraded', () => {
test('succeeds when no versions have attestation', () => {
const meta: PackageMetaWithTime = {
name: 'foo',
'dist-tags': { latest: '2.0.0' },
versions: {
'1.0.0': {
name: 'foo',
version: '1.0.0',
dist: {
shasum: 'abc123',
tarball: 'https://registry.example.com/foo/-/foo-1.0.0.tgz',
},
},
'2.0.0': {
name: 'foo',
version: '2.0.0',
dist: {
shasum: 'def456',
tarball: 'https://registry.example.com/foo/-/foo-2.0.0.tgz',
},
},
},
time: {
'1.0.0': '2025-01-01T00:00:00.000Z',
'2.0.0': '2025-02-01T00:00:00.000Z',
},
}
expect(() => {
failIfTrustDowngraded(meta, '2.0.0')
}).not.toThrow()
})
test('succeeds for version published before first attested version', () => {
const meta: PackageMetaWithTime = {
name: 'foo',
'dist-tags': { latest: '2.0.0' },
versions: {
'1.0.0': {
name: 'foo',
version: '1.0.0',
dist: {
shasum: 'abc123',
tarball: 'https://registry.example.com/foo/-/foo-1.0.0.tgz',
},
},
'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',
},
},
},
},
},
time: {
'1.0.0': '2025-01-01T00:00:00.000Z',
'2.0.0': '2025-02-01T00:00:00.000Z',
},
}
expect(() => {
failIfTrustDowngraded(meta, '1.0.0')
}).not.toThrow()
})
test('throws an error when downgrading from provenance to none', () => {
const meta: PackageMetaWithTime = {
name: 'foo',
'dist-tags': { latest: '3.0.0' },
versions: {
'1.0.0': {
name: 'foo',
version: '1.0.0',
dist: {
shasum: 'abc123',
tarball: 'https://registry.example.com/foo/-/foo-1.0.0.tgz',
},
},
'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: {
'1.0.0': '2025-01-01T00:00:00.000Z',
'2.0.0': '2025-02-01T00:00:00.000Z',
'3.0.0': '2025-03-01T00:00:00.000Z',
},
}
expect(() => {
failIfTrustDowngraded(meta, '3.0.0')
}).toThrow('High-risk trust downgrade')
})
test('throws an error when downgrading from trustedPublisher to provenance', () => {
const meta: PackageMetaWithTime = {
name: 'foo',
'dist-tags': { latest: '3.0.0' },
versions: {
'1.0.0': {
name: 'foo',
version: '1.0.0',
dist: {
shasum: 'abc123',
tarball: 'https://registry.example.com/foo/-/foo-1.0.0.tgz',
},
},
'2.0.0': {
name: 'foo',
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/foo/-/foo-2.0.0.tgz',
},
},
'3.0.0': {
name: 'foo',
version: '3.0.0',
dist: {
shasum: 'ghi789',
tarball: 'https://registry.example.com/foo/-/foo-3.0.0.tgz',
attestations: {
provenance: {
predicateType: 'https://slsa.dev/provenance/v1',
},
},
},
},
},
time: {
'1.0.0': '2025-01-01T00:00:00.000Z',
'2.0.0': '2025-02-01T00:00:00.000Z',
'3.0.0': '2025-03-01T00:00:00.000Z',
},
}
expect(() => {
failIfTrustDowngraded(meta, '3.0.0')
}).toThrow('High-risk trust downgrade')
})
test('throws an error when downgrading from trustedPublisher to none', () => {
const meta: PackageMetaWithTime = {
name: 'foo',
'dist-tags': { latest: '3.0.0' },
versions: {
'1.0.0': {
name: 'foo',
version: '1.0.0',
dist: {
shasum: 'abc123',
tarball: 'https://registry.example.com/foo/-/foo-1.0.0.tgz',
},
},
'2.0.0': {
name: 'foo',
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/foo/-/foo-2.0.0.tgz',
},
},
'3.0.0': {
name: 'foo',
version: '3.0.0',
dist: {
shasum: 'ghi789',
tarball: 'https://registry.example.com/foo/-/foo-3.0.0.tgz',
},
},
},
time: {
'1.0.0': '2025-01-01T00:00:00.000Z',
'2.0.0': '2025-02-01T00:00:00.000Z',
'3.0.0': '2025-03-01T00:00:00.000Z',
},
}
expect(() => {
failIfTrustDowngraded(meta, '3.0.0')
}).toThrow('High-risk trust downgrade')
})
test('succeeds when maintaining same trust level', () => {
const meta: PackageMetaWithTime = {
name: 'foo',
'dist-tags': { latest: '3.0.0' },
versions: {
'1.0.0': {
name: 'foo',
version: '1.0.0',
dist: {
shasum: 'abc123',
tarball: 'https://registry.example.com/foo/-/foo-1.0.0.tgz',
},
},
'2.0.0': {
name: 'foo',
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/foo/-/foo-2.0.0.tgz',
},
},
'3.0.0': {
name: 'foo',
version: '3.0.0',
_npmUser: {
name: 'test-publisher',
email: 'publisher@example.com',
trustedPublisher: {
id: 'test-provider',
oidcConfigId: 'oidc:test-config-123',
},
},
dist: {
shasum: 'ghi789',
tarball: 'https://registry.example.com/foo/-/foo-3.0.0.tgz',
},
},
},
time: {
'1.0.0': '2025-01-01T00:00:00.000Z',
'2.0.0': '2025-02-01T00:00:00.000Z',
'3.0.0': '2025-03-01T00:00:00.000Z',
},
}
expect(() => {
failIfTrustDowngraded(meta, '3.0.0')
}).not.toThrow()
})
test('throws an error when version time is missing', () => {
const meta: PackageMetaWithTime = {
name: 'foo',
'dist-tags': { latest: '2.0.0' },
versions: {
'1.0.0': {
name: 'foo',
version: '1.0.0',
dist: {
shasum: 'abc123',
tarball: 'https://registry.example.com/foo/-/foo-1.0.0.tgz',
attestations: {
provenance: {
predicateType: 'https://slsa.dev/provenance/v1',
},
},
},
},
'2.0.0': {
name: 'foo',
version: '2.0.0',
dist: {
shasum: 'def456',
tarball: 'https://registry.example.com/foo/-/foo-2.0.0.tgz',
},
},
},
time: {
'1.0.0': '2025-01-01T00:00:00.000Z',
},
}
expect(() => {
failIfTrustDowngraded(meta, '2.0.0')
}).toThrow('Missing time')
})
})

View File

@@ -4,6 +4,7 @@ import {
type PkgResolutionId,
type PinnedVersion,
type PackageVersionPolicy,
type TrustPolicy,
} from '@pnpm/types'
export { type PkgResolutionId }
@@ -107,6 +108,7 @@ export interface PreferredVersions {
export interface ResolveOptions {
alwaysTryWorkspacePackages?: boolean
trustPolicy?: TrustPolicy
defaultTag?: string
pickLowestVersion?: boolean
publishedBy?: Date

View File

@@ -43,6 +43,7 @@ export type CreateNewStoreControllerOptions = CreateResolverOptions & Pick<Confi
| 'resolutionMode'
| 'saveWorkspaceProtocol'
| 'strictSsl'
| 'trustPolicy'
| 'unsafePerm'
| 'userAgent'
| 'verifyStoreIntegrity'
@@ -56,7 +57,13 @@ export type CreateNewStoreControllerOptions = CreateResolverOptions & Pick<Confi
export async function createNewStoreController (
opts: CreateNewStoreControllerOptions
): Promise<{ ctrl: StoreController, dir: string }> {
const fullMetadata = opts.fetchFullMetadata ?? ((opts.resolutionMode === 'time-based' || Boolean(opts.minimumReleaseAge)) && !opts.registrySupportsTimeField)
const fullMetadata = opts.fetchFullMetadata ?? (
(
opts.resolutionMode === 'time-based' ||
Boolean(opts.minimumReleaseAge) ||
opts.trustPolicy === 'no-downgrade'
) && !opts.registrySupportsTimeField
)
const { resolve, fetchers, clearResolutionCache } = createClient({
customFetchers: opts.hooks?.fetchers,
userConfig: opts.userConfig,

View File

@@ -19,6 +19,7 @@ import {
type PackageManifest,
type PinnedVersion,
type PackageVersionPolicy,
type TrustPolicy,
} from '@pnpm/types'
export type { PackageFileInfo, PackageFilesResponse, ImportPackageFunction, ImportPackageFunctionAsync }
@@ -136,6 +137,7 @@ export interface RequestPackageOptions {
injectWorkspacePackages?: boolean
calcSpecifier?: boolean
pinnedVersion?: PinnedVersion
trustPolicy?: TrustPolicy
}
export type BundledManifestFunction = () => Promise<BundledManifest | undefined>