mirror of
https://github.com/pnpm/pnpm.git
synced 2026-01-15 02:18:31 -05:00
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:
committed by
Zoltan Kochan
parent
a7cf08797e
commit
68ad0868b4
16
.changeset/smooth-llamas-pick.md
Normal file
16
.changeset/smooth-llamas-pick.md
Normal 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`.
|
||||
10
.changeset/violet-spiders-write.md
Normal file
10
.changeset/violet-spiders-write.md
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
"pnpm": minor
|
||||
---
|
||||
|
||||
Added a new setting: `trustPolicy`.
|
||||
|
||||
When set to `no-downgrade`, pnpm will fail installation if a package’s 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).
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -74,6 +74,7 @@ export function rcOptionsTypes (): Record<string, unknown> {
|
||||
'side-effects-cache',
|
||||
'store-dir',
|
||||
'strict-peer-dependencies',
|
||||
'trust-policy',
|
||||
'unsafe-perm',
|
||||
'offline',
|
||||
'only',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -262,6 +262,7 @@ function clearMeta (pkg: PackageMeta): PackageMeta {
|
||||
'bundleDependencies',
|
||||
'bundledDependencies',
|
||||
'hasInstallScript',
|
||||
'_npmUser',
|
||||
], info)
|
||||
}
|
||||
|
||||
|
||||
@@ -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`)
|
||||
}
|
||||
|
||||
92
resolving/npm-resolver/src/trustChecks.ts
Normal file
92
resolving/npm-resolver/src/trustChecks.ts
Normal 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
|
||||
}
|
||||
407
resolving/npm-resolver/test/trustChecks.test.ts
Normal file
407
resolving/npm-resolver/test/trustChecks.test.ts
Normal 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')
|
||||
})
|
||||
})
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user