fix: skip lockfile minimumReleaseAge/trustPOlicy verification for non-registry tarball (#12122)

This commit is contained in:
btea
2026-06-02 16:59:41 +08:00
committed by GitHub
parent c0368f473f
commit 722b9cda24
5 changed files with 51 additions and 0 deletions

View File

@@ -0,0 +1,6 @@
---
"@pnpm/resolving.npm-resolver": patch
"pnpm": patch
---
Skip lockfile `minimumReleaseAge`/`trustPolicy` verification for non-registry tarball protocols (for example `file:`), so local tarball dependencies are not incorrectly checked against npm registry metadata.

View File

@@ -757,6 +757,12 @@ fn npm_registry_tarball(resolution: &LockfileResolution) -> Option<Option<&str>>
if t.git_hosted.unwrap_or(false) {
return None;
}
if let Ok(parsed) = reqwest::Url::parse(&t.tarball) {
let scheme = parsed.scheme();
if scheme != "http" && scheme != "https" {
return None;
}
}
Some(Some(t.tarball.as_str()))
}
LockfileResolution::Directory(_)

View File

@@ -237,6 +237,24 @@ async fn verify_short_circuits_non_semver_version() {
assert_eq!(result, ResolutionVerification::Ok);
}
/// `file:` tarball resolutions are local artifacts, not registry
/// entries, so the verifier must skip minimumReleaseAge/trust checks.
#[tokio::test]
async fn verify_short_circuits_file_tarball_resolution() {
let mut opts = default_opts("http://nonexistent.example.invalid/");
opts.minimum_release_age = Some(60 * 24 * 365);
let verifier = create_npm_resolution_verifier(opts).expect("verifier");
let resolution = LockfileResolution::Tarball(TarballResolution {
tarball: "file:vendor/types__my-cool-lib-v1.0.0.tgz".to_string(),
integrity: Some(fake_integrity()),
git_hosted: None,
path: None,
});
let name: PkgName = "@types/my-cool-lib".parse().expect("parse");
let result = verifier.verify(&resolution, ctx(&name, "1.0.0")).await;
assert_eq!(result, ResolutionVerification::Ok);
}
/// When the exclude policy covers the package, age check skips —
/// the version is treated as opted out regardless of its publish
/// timestamp.

View File

@@ -830,5 +830,12 @@ function isNpmRegistryResolution (resolution: Resolution | unknown): boolean {
// Git-hosted tarballs (codeload/gitlab/bitbucket) are special-cased in
// the resolver and aren't subject to release-age policy.
if ('gitHosted' in resolution && (resolution as { gitHosted?: boolean }).gitHosted) return false
const tarball = (resolution as { tarball?: unknown }).tarball
if (typeof tarball === 'string') {
// Local/non-registry tarballs (for example `file:`) have no packument
// metadata, so minimumReleaseAge/trustPolicy verification cannot apply.
const protocol = tryParseUrl(tarball)?.protocol
if (protocol != null && protocol !== 'http:' && protocol !== 'https:') return false
}
return 'tarball' in resolution || 'integrity' in resolution
}

View File

@@ -221,6 +221,20 @@ test('createNpmResolutionVerifier() ignoreMissingTimeField passes the entry when
expect(result).toEqual({ ok: true })
})
test('createNpmResolutionVerifier() skips file: tarball resolutions', async () => {
const verifier = createNpmResolutionVerifier(makeVerifierOpts({
minimumReleaseAge: 1440,
}))!
const result = await verifier.verify(
{
integrity: 'sha512-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==',
tarball: 'file:vendor/types__my-cool-lib-v1.0.0.tgz',
} as unknown as Resolution,
{ name: '@types/my-cool-lib', version: '1.0.0' }
)
expect(result).toEqual({ ok: true })
})
test('createNpmResolutionVerifier() canTrustPastCheck rejects when the trust-exclude list shrinks', () => {
const verifier = createNpmResolutionVerifier(makeVerifierOpts({
trustPolicy: 'no-downgrade',