feat(npm): publishing-trust ranking and no-downgrade trust policy (#34927)
This teaches Deno's npm resolver about how a package version was
published and adds an opt-in `no-downgrade` trust policy, following
pnpm's implementation (`resolving/npm-resolver/src/trustChecks.ts`)
closely.
npm's full packument exposes whether a version was published via trusted
publishing (OIDC, `_npmUser.trustedPublisher`), carries a provenance
attestation (`dist.attestations.provenance`), or went through staged
publishing (`_npmUser.approver`, a maintainer approving with a live 2FA
challenge). Each version's strongest signal is ranked into a single
trust level. The tiers are mutually exclusive and mirror pnpm: a staged
publish is strongest, then trusted publishing *backed by* a provenance
attestation, then a provenance attestation on its own. A
`trustedPublisher` flag without provenance is deliberately not counted,
since on its own it is just metadata a future staged-publish flow could
mint.
The `no-downgrade` policy, enabled with `trust-policy=no-downgrade` in
`.npmrc`, refuses to resolve a version whose trust evidence is weaker
than the strongest evidence on any earlier-published version of the same
package. The comparison is by publish date, not semver, exactly as
pnpm does it: if a package has been published through trusted publishing
or with provenance and a later version suddenly appears as a plain token
publish, that is a strong signal of a stolen maintainer token, and the
policy turns it into a hard error instead of a silent install. When a
downgrade is the only thing blocking resolution, the resolver returns a
dedicated error explaining the rejection and how to override it, rather
than silently falling back to an older version.
The motivation is supply-chain safety. This is the same defense that
would have caught the August 2025 s1ngularity incident, where a package
consistently published through CI was suddenly published from a laptop
with basic credentials.
The trust signals only exist in the full packument. Enabling the policy
fetches the full packument, but `min-release-age` already makes Deno do
that by default, so in the common case there is no extra fetch. To keep
the cached `registry.json` small, the never-read signal objects
(`_npmUser.approver`, `_npmUser.trustedPublisher`,
`dist.attestations.provenance`) are reduced to a one-byte presence
marker before the packument is cached.
A `trust-policy-ignore-after` setting (in minutes, mirroring pnpm's
`trustPolicyIgnoreAfter`) skips the check for versions published more
than that long ago, so genuinely pre-provenance releases still install.
It is turned into an absolute cutoff in the resolver factory so the
resolver stays free of a wall clock.
Coverage: tiered ranking, the publish-date downgrade scan (including
prerelease exclusion and the ignore-after cutoff), the targeted
downgrade error, and compact cache serialization are covered by resolver
unit tests. An end-to-end spec test installs a package whose newer
version downgrades from a staged publish to a plain publish and asserts
the install is rejected, plus a spec test that the cache keeps compact
markers. The test npm registry now preserves `dist.attestations` from
fixtures so provenance is exercisable end to end.
Unlike pnpm, the trust level is not persisted to the lockfile: the
policy recomputes the baseline from the packument's version history on
each resolution, so existing lockfiles are unaffected.
A `trust-policy-exclude[]` setting (mirroring pnpm's
`trustPolicyExclude`) exempts named packages from the policy: repeated
`trust-policy-exclude[]=<package>` entries in `.npmrc` list packages
that resolve as if the policy were off. Home and project excludes are
unioned.
The policy is opt-in and stays off by default. Provenance, trusted
publishing and staged publishing are still unevenly adopted across the
registry, so enabling `no-downgrade` by default would turn legitimate
trust-evidence gaps into hard install failures; it can be revisited
once adoption is high.