From 722b9cda24bc4e1253881cbc4899f8103d9a89ff Mon Sep 17 00:00:00 2001 From: btea <2356281422@qq.com> Date: Tue, 2 Jun 2026 16:59:41 +0800 Subject: [PATCH] fix: skip lockfile `minimumReleaseAge`/`trustPOlicy` verification for non-registry tarball (#12122) --- .changeset/short-lamps-relax.md | 6 ++++++ .../src/create_npm_resolution_verifier.rs | 6 ++++++ .../create_npm_resolution_verifier/tests.rs | 18 ++++++++++++++++++ .../src/createNpmResolutionVerifier.ts | 7 +++++++ .../test/createNpmResolutionVerifier.test.ts | 14 ++++++++++++++ 5 files changed, 51 insertions(+) create mode 100644 .changeset/short-lamps-relax.md diff --git a/.changeset/short-lamps-relax.md b/.changeset/short-lamps-relax.md new file mode 100644 index 0000000000..1eadab06c4 --- /dev/null +++ b/.changeset/short-lamps-relax.md @@ -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. diff --git a/pacquet/crates/resolving-npm-resolver/src/create_npm_resolution_verifier.rs b/pacquet/crates/resolving-npm-resolver/src/create_npm_resolution_verifier.rs index 14b68605fd..7f25fb5346 100644 --- a/pacquet/crates/resolving-npm-resolver/src/create_npm_resolution_verifier.rs +++ b/pacquet/crates/resolving-npm-resolver/src/create_npm_resolution_verifier.rs @@ -757,6 +757,12 @@ fn npm_registry_tarball(resolution: &LockfileResolution) -> Option> 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(_) diff --git a/pacquet/crates/resolving-npm-resolver/src/create_npm_resolution_verifier/tests.rs b/pacquet/crates/resolving-npm-resolver/src/create_npm_resolution_verifier/tests.rs index 685aace539..17016a160b 100644 --- a/pacquet/crates/resolving-npm-resolver/src/create_npm_resolution_verifier/tests.rs +++ b/pacquet/crates/resolving-npm-resolver/src/create_npm_resolution_verifier/tests.rs @@ -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. diff --git a/resolving/npm-resolver/src/createNpmResolutionVerifier.ts b/resolving/npm-resolver/src/createNpmResolutionVerifier.ts index 9659f9eb97..fa619ea695 100644 --- a/resolving/npm-resolver/src/createNpmResolutionVerifier.ts +++ b/resolving/npm-resolver/src/createNpmResolutionVerifier.ts @@ -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 } diff --git a/resolving/npm-resolver/test/createNpmResolutionVerifier.test.ts b/resolving/npm-resolver/test/createNpmResolutionVerifier.test.ts index c5c95ae236..32c007899a 100644 --- a/resolving/npm-resolver/test/createNpmResolutionVerifier.test.ts +++ b/resolving/npm-resolver/test/createNpmResolutionVerifier.test.ts @@ -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',