@pnpm/resolving.npm-resolver 1102.0.1 → 1102.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/createNpmResolutionVerifier.d.ts +1 -1
- package/lib/createNpmResolutionVerifier.js +103 -75
- package/lib/fetch.js +14 -2
- package/lib/pickPackage.d.ts +30 -6
- package/lib/pickPackage.js +47 -3
- package/lib/violationCodes.d.ts +1 -0
- package/lib/violationCodes.js +1 -0
- package/package.json +21 -21
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { GetAuthHeader } from '@pnpm/fetching.types';
|
|
2
|
-
import type
|
|
2
|
+
import { type ResolutionVerifier } from '@pnpm/resolving.resolver-base';
|
|
3
3
|
import type { Registries, TrustPolicy } from '@pnpm/types';
|
|
4
4
|
import type { FetchMetadataFromFromRegistryOptions } from './fetch.js';
|
|
5
5
|
import { type FetchFullMetadataCachedOptions } from './fetchFullMetadataCached.js';
|
|
@@ -2,14 +2,15 @@ import { pickRegistryForPackage } from '@pnpm/config.pick-registry-for-package';
|
|
|
2
2
|
import { createPackageVersionPolicy } from '@pnpm/config.version-policy';
|
|
3
3
|
import { FULL_META_DIR } from '@pnpm/constants';
|
|
4
4
|
import { PnpmError } from '@pnpm/error';
|
|
5
|
+
import { isGitHostedTarballUrl, } from '@pnpm/resolving.resolver-base';
|
|
5
6
|
import semver from 'semver';
|
|
6
7
|
import { fetchAttestationPublishedAt } from './fetchAttestationPublishedAt.js';
|
|
7
8
|
import { fetchAbbreviatedMetadataCached, fetchFullMetadataCached, } from './fetchFullMetadataCached.js';
|
|
8
9
|
import { normalizeRegistryUrl } from './normalizeRegistryUrl.js';
|
|
9
10
|
import { BUILTIN_NAMED_REGISTRIES } from './parseBareSpecifier.js';
|
|
10
|
-
import { getPkgMirrorPath, loadMeta, warnMissingTimeFieldOnce } from './pickPackage.js';
|
|
11
|
+
import { getPkgMetaCacheKey, getPkgMirrorPath, loadMeta, warnMissingTimeFieldOnce } from './pickPackage.js';
|
|
11
12
|
import { failIfTrustDowngraded } from './trustChecks.js';
|
|
12
|
-
import { MINIMUM_RELEASE_AGE_VIOLATION_CODE, TARBALL_URL_MISMATCH_VIOLATION_CODE, TRUST_DOWNGRADE_VIOLATION_CODE, } from './violationCodes.js';
|
|
13
|
+
import { MINIMUM_RELEASE_AGE_VIOLATION_CODE, MISSING_TARBALL_INTEGRITY_VIOLATION_CODE, TARBALL_URL_MISMATCH_VIOLATION_CODE, TRUST_DOWNGRADE_VIOLATION_CODE, } from './violationCodes.js';
|
|
13
14
|
/**
|
|
14
15
|
* Returns a `ResolutionVerifier` for npm-registry-resolved lockfile
|
|
15
16
|
* entries. It always binds each entry's recorded tarball URL to the
|
|
@@ -85,8 +86,17 @@ export function createNpmResolutionVerifier(opts) {
|
|
|
85
86
|
const trustPolicy = opts.trustPolicy;
|
|
86
87
|
const trustPolicyIgnoreAfter = opts.trustPolicyIgnoreAfter;
|
|
87
88
|
const verify = async (resolution, { name, version, nonSemverVersion }) => {
|
|
88
|
-
if (!
|
|
89
|
+
if (!isRegistryTarballResolution(resolution))
|
|
89
90
|
return { ok: true };
|
|
91
|
+
// Network-free structural checks must run before registry metadata shortcuts.
|
|
92
|
+
const integrity = resolution.integrity;
|
|
93
|
+
if (typeof integrity !== 'string' || integrity.length === 0) {
|
|
94
|
+
return {
|
|
95
|
+
ok: false,
|
|
96
|
+
code: MISSING_TARBALL_INTEGRITY_VIOLATION_CODE,
|
|
97
|
+
reason: 'has no "integrity" field, so its downloaded tarball cannot be verified',
|
|
98
|
+
};
|
|
99
|
+
}
|
|
90
100
|
// URL/git-keyed entries are deliberate non-registry deps. They can still
|
|
91
101
|
// carry a semver `version` copied from the resolved manifest, so the
|
|
92
102
|
// semver guard below isn't enough on its own — the registry policies and
|
|
@@ -94,9 +104,22 @@ export function createNpmResolutionVerifier(opts) {
|
|
|
94
104
|
// would 404.
|
|
95
105
|
if (nonSemverVersion != null)
|
|
96
106
|
return { ok: true };
|
|
97
|
-
if (!semver.valid(version))
|
|
98
|
-
return {
|
|
99
|
-
|
|
107
|
+
if (!semver.valid(version)) {
|
|
108
|
+
return {
|
|
109
|
+
ok: false,
|
|
110
|
+
code: TARBALL_URL_MISMATCH_VIOLATION_CODE,
|
|
111
|
+
reason: `has a non-semver version ("${version}") and so cannot be verified against the registry's published metadata`,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
const rawTarball = resolution.tarball;
|
|
115
|
+
if (rawTarball != null && typeof rawTarball !== 'string') {
|
|
116
|
+
return {
|
|
117
|
+
ok: false,
|
|
118
|
+
code: TARBALL_URL_MISMATCH_VIOLATION_CODE,
|
|
119
|
+
reason: 'has a non-string "tarball" field, so its URL cannot be verified',
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
const tarballUrl = typeof rawTarball === 'string' ? rawTarball : undefined;
|
|
100
123
|
const registry = pickRegistryForVersion(opts.registries, namedRegistryPrefixes, name, tarballUrl);
|
|
101
124
|
// A registry entry that pins an explicit tarball URL must point at the
|
|
102
125
|
// artifact the registry's own metadata lists. Otherwise a trusted
|
|
@@ -150,6 +173,8 @@ export function createNpmResolutionVerifier(opts) {
|
|
|
150
173
|
// applies the binding — otherwise an upgrade could keep trusting a
|
|
151
174
|
// lockfile that was only ever age/trust-checked.
|
|
152
175
|
tarballUrlBinding: true,
|
|
176
|
+
// Same cache identity rule for the missing-integrity structural check.
|
|
177
|
+
integrityRequired: true,
|
|
153
178
|
minimumReleaseAge,
|
|
154
179
|
minimumReleaseAgeExclude: sortedMinAgeExcludes,
|
|
155
180
|
trustPolicy: trustPolicy ?? null,
|
|
@@ -161,6 +186,10 @@ export function createNpmResolutionVerifier(opts) {
|
|
|
161
186
|
// didn't record it can't be trusted to have enforced it.
|
|
162
187
|
if (cached.tarballUrlBinding !== true)
|
|
163
188
|
return false;
|
|
189
|
+
// The missing-integrity check is also unconditional; older cache records
|
|
190
|
+
// without the flag cannot prove they rejected unverifiable tarballs.
|
|
191
|
+
if (cached.integrityRequired !== true)
|
|
192
|
+
return false;
|
|
164
193
|
// Maturity: a previously cached run under a larger cutoff
|
|
165
194
|
// (stricter window) is trustworthy under a smaller current one —
|
|
166
195
|
// its set of accepted versions is a subset of today's. The
|
|
@@ -203,17 +232,11 @@ export function createNpmResolutionVerifier(opts) {
|
|
|
203
232
|
};
|
|
204
233
|
}
|
|
205
234
|
async function runAgeCheck(context, registry, name, version, cutoff, ignoreMissingTimeField) {
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
return {
|
|
212
|
-
ok: false,
|
|
213
|
-
code: MINIMUM_RELEASE_AGE_VIOLATION_CODE,
|
|
214
|
-
reason: uncheckable('minimumReleaseAge', err instanceof Error ? err.message : String(err)),
|
|
215
|
-
};
|
|
216
|
-
}
|
|
235
|
+
// A transport failure (auth/network/5xx) propagates the registry's own fetch
|
|
236
|
+
// error (e.g. ERR_PNPM_FETCH_403); the gate aborts the install with it rather
|
|
237
|
+
// than folding it into a policy violation. A successful fetch that simply
|
|
238
|
+
// lacks a publish timestamp for this version is handled below.
|
|
239
|
+
const published = await fetchPublishedAt(context, registry, name, version);
|
|
217
240
|
if (!published) {
|
|
218
241
|
// No source — attestation, local mirror, or full metadata —
|
|
219
242
|
// surfaced a publish timestamp for this version. The resolver's
|
|
@@ -262,7 +285,15 @@ async function runAgeCheck(context, registry, name, version, cutoff, ignoreMissi
|
|
|
262
285
|
* pointing it at a `name@version` the registry can't vouch for.
|
|
263
286
|
*/
|
|
264
287
|
async function runTarballUrlCheck(context, registry, name, version, lockfileTarball) {
|
|
265
|
-
const meta = await fetchAbbreviatedMeta(context, registry, name);
|
|
288
|
+
const { meta, error } = await fetchAbbreviatedMeta(context, registry, name);
|
|
289
|
+
if (error != null) {
|
|
290
|
+
// Couldn't reach the registry to verify (auth/network/5xx). Propagate the
|
|
291
|
+
// registry's own fetch error (e.g. ERR_PNPM_FETCH_403, which already
|
|
292
|
+
// explains the auth situation) instead of mislabeling a transport failure
|
|
293
|
+
// as a tampering-style URL mismatch. The gate aborts the install with that
|
|
294
|
+
// error — still fail-closed, the entry never reaches the filesystem.
|
|
295
|
+
throw error;
|
|
296
|
+
}
|
|
266
297
|
const registryTarball = meta?.versionTarballs?.get(version);
|
|
267
298
|
if (registryTarball != null && sameTarballUrl(lockfileTarball, registryTarball)) {
|
|
268
299
|
return undefined;
|
|
@@ -304,20 +335,11 @@ function canonicalTarballUrl(url) {
|
|
|
304
335
|
* attestation → pass" shortcut would silently miss it.
|
|
305
336
|
*/
|
|
306
337
|
async function runTrustCheck(context, registry, name, version, opts) {
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
// `fetchFullMetadataCached` rejects (network error, 404, etc.); the
|
|
313
|
-
// verifier fails closed so a missing manifest can't be mistaken
|
|
314
|
-
// for a passing trust check.
|
|
315
|
-
return {
|
|
316
|
-
ok: false,
|
|
317
|
-
code: TRUST_DOWNGRADE_VIOLATION_CODE,
|
|
318
|
-
reason: uncheckable('trustPolicy', err instanceof Error ? err.message : String(err)),
|
|
319
|
-
};
|
|
320
|
-
}
|
|
338
|
+
// A transport failure (auth/network/5xx) propagates the registry's own fetch
|
|
339
|
+
// error; the gate aborts the install with it rather than folding it into a
|
|
340
|
+
// policy violation. Still fail-closed: a missing manifest can't be mistaken
|
|
341
|
+
// for a passing trust check because the install never proceeds.
|
|
342
|
+
const meta = await fetchFullMetaForTrust(context, registry, name);
|
|
321
343
|
try {
|
|
322
344
|
failIfTrustDowngraded(meta, version, opts);
|
|
323
345
|
}
|
|
@@ -335,20 +357,13 @@ function fetchFullMetaForTrust(context, registry, name) {
|
|
|
335
357
|
let cachedPromise = context.fullMetaForTrustCache.get(cacheKey);
|
|
336
358
|
if (cachedPromise == null) {
|
|
337
359
|
// Fast path: if the resolver already upgraded to full meta for this
|
|
338
|
-
// name during the same install (e.g. minimumReleaseAge
|
|
339
|
-
// reuse that document. Abbreviated meta is rejected here —
|
|
340
|
-
// per-version `time` and per-version trust evidence, both
|
|
341
|
-
// by failIfTrustDowngraded.
|
|
342
|
-
//
|
|
343
|
-
//
|
|
344
|
-
|
|
345
|
-
// If two registries serve packages of the same name in one install
|
|
346
|
-
// the resolver itself silently keeps the first fetch; the verifier
|
|
347
|
-
// here inherits that scope. The name check below is a defensive
|
|
348
|
-
// guard against accidental cache mixups; tightening this to a
|
|
349
|
-
// registry-qualified read needs the resolver's `metaCache` key
|
|
350
|
-
// shape to change first.
|
|
351
|
-
const shared = readSharedMetaForTrust(context.sharedMetaCache, name);
|
|
360
|
+
// (registry, name) during the same install (e.g. minimumReleaseAge
|
|
361
|
+
// active), reuse that document. Abbreviated meta is rejected here —
|
|
362
|
+
// it lacks per-version `time` and per-version trust evidence, both
|
|
363
|
+
// required by failIfTrustDowngraded. The read is registry-qualified
|
|
364
|
+
// (see `getPkgMetaCacheKey`), so a package of the same name served by
|
|
365
|
+
// a different registry can't be returned here.
|
|
366
|
+
const shared = readSharedMetaForTrust(context.sharedMetaCache, registry, name);
|
|
352
367
|
if (shared != null) {
|
|
353
368
|
cachedPromise = Promise.resolve(projectTrustMeta(shared));
|
|
354
369
|
}
|
|
@@ -492,7 +507,9 @@ async function resolvePublishedAt(context, registry, name, version) {
|
|
|
492
507
|
* lookups.
|
|
493
508
|
*/
|
|
494
509
|
async function tryAbbreviatedModifiedShortcut(context, registry, name, version) {
|
|
495
|
-
|
|
510
|
+
// A fetch failure here is fine: ignore `error` and fall back to per-version
|
|
511
|
+
// lookups, the same as a successful-but-uninformative metadata response.
|
|
512
|
+
const { meta } = await fetchAbbreviatedMeta(context, registry, name);
|
|
496
513
|
const modified = meta?.modified;
|
|
497
514
|
if (typeof modified !== 'string')
|
|
498
515
|
return undefined;
|
|
@@ -516,49 +533,59 @@ function fetchAbbreviatedMeta(context, registry, name) {
|
|
|
516
533
|
// Fast path: the resolver's per-install LRU already holds this
|
|
517
534
|
// packument from its own pickPackage pass — abbreviated or full.
|
|
518
535
|
// Project it for the shortcut and skip the disk/network round-trip.
|
|
519
|
-
//
|
|
520
|
-
//
|
|
521
|
-
|
|
522
|
-
const shared = readSharedMeta(context.sharedMetaCache, name);
|
|
536
|
+
// The read is registry-qualified (see `getPkgMetaCacheKey`), so it
|
|
537
|
+
// can only return this registry's own packument.
|
|
538
|
+
const shared = readSharedMeta(context.sharedMetaCache, registry, name);
|
|
523
539
|
if (shared != null) {
|
|
524
|
-
cachedPromise = Promise.resolve(projectAbbreviatedMeta(shared));
|
|
540
|
+
cachedPromise = Promise.resolve({ meta: projectAbbreviatedMeta(shared) });
|
|
525
541
|
}
|
|
526
542
|
else {
|
|
543
|
+
// Carry a fetch failure (auth/network/5xx) as `error` instead of
|
|
544
|
+
// collapsing it to `undefined`: the tarball-URL check rethrows it (so the
|
|
545
|
+
// registry's own error surfaces, not a tampering-style mismatch) while
|
|
546
|
+
// the age shortcut ignores it and falls back to per-version lookups.
|
|
547
|
+
// Keeping it a resolved value — not a rejected promise — lets the two
|
|
548
|
+
// callers share one cached promise without an unhandled rejection.
|
|
527
549
|
cachedPromise = fetchAbbreviatedMetadataCached(context.fetchOpts, name, {
|
|
528
550
|
registry,
|
|
529
551
|
authHeaderValue: context.getAuthHeaderValueByURI(registry, { pkgName: name }),
|
|
530
552
|
cacheDir: context.cacheDir,
|
|
531
|
-
}).then(projectAbbreviatedMeta, () =>
|
|
553
|
+
}).then((meta) => ({ meta: projectAbbreviatedMeta(meta) }), (error) => ({ error }));
|
|
532
554
|
}
|
|
533
555
|
context.abbreviatedMetaCache.set(cacheKey, cachedPromise);
|
|
534
556
|
}
|
|
535
557
|
return cachedPromise;
|
|
536
558
|
}
|
|
537
|
-
function readSharedMeta(cache, name) {
|
|
559
|
+
function readSharedMeta(cache, registry, name) {
|
|
538
560
|
if (cache == null)
|
|
539
561
|
return undefined;
|
|
540
|
-
// Prefer
|
|
541
|
-
//
|
|
542
|
-
//
|
|
543
|
-
//
|
|
544
|
-
// `minimumReleaseAge` configured, otherwise the bare `name` key holds
|
|
562
|
+
// Prefer a full entry — it carries every field the abbreviated form
|
|
563
|
+
// does, plus `time` and per-version trust evidence the trust check
|
|
564
|
+
// needs. The resolver only populates a full key when the install ran
|
|
565
|
+
// with `minimumReleaseAge` configured, otherwise the bare key holds
|
|
545
566
|
// the abbreviated form.
|
|
546
|
-
return
|
|
547
|
-
validateSharedMeta(cache.get(name), name);
|
|
567
|
+
return readSharedFullMeta(cache, registry, name) ??
|
|
568
|
+
validateSharedMeta(cache.get(getPkgMetaCacheKey(registry, name, false, false)), name);
|
|
548
569
|
}
|
|
549
|
-
function readSharedMetaForTrust(cache, name) {
|
|
570
|
+
function readSharedMetaForTrust(cache, registry, name) {
|
|
550
571
|
if (cache == null)
|
|
551
572
|
return undefined;
|
|
552
573
|
// Abbreviated meta is rejected for the trust check — it lacks
|
|
553
574
|
// per-version `time` and per-version trust evidence.
|
|
554
|
-
return
|
|
575
|
+
return readSharedFullMeta(cache, registry, name);
|
|
555
576
|
}
|
|
556
|
-
//
|
|
557
|
-
//
|
|
558
|
-
//
|
|
559
|
-
//
|
|
560
|
-
|
|
561
|
-
|
|
577
|
+
// The resolver keys full metadata as either filtered or unfiltered
|
|
578
|
+
// depending on its own `filterMetadata` setting; the verifier doesn't
|
|
579
|
+
// know which, and a filtered full packument keeps everything the
|
|
580
|
+
// verifier reads (`time`, per-version `_npmUser`, `dist`), so try both.
|
|
581
|
+
function readSharedFullMeta(cache, registry, name) {
|
|
582
|
+
return validateSharedMeta(cache.get(getPkgMetaCacheKey(registry, name, true, false)), name) ??
|
|
583
|
+
validateSharedMeta(cache.get(getPkgMetaCacheKey(registry, name, true, true)), name);
|
|
584
|
+
}
|
|
585
|
+
// Defensive guard against the resolver's `metaCache` returning an
|
|
586
|
+
// unexpected entry. The cache key is registry-qualified (see
|
|
587
|
+
// `getPkgMetaCacheKey`), so a package of the same name from another
|
|
588
|
+
// registry can't be returned; this name check catches accidental
|
|
562
589
|
// returns of a different package (cache corruption, factory misuse)
|
|
563
590
|
// rather than silently feeding wrong data to the trust / age check.
|
|
564
591
|
function validateSharedMeta(meta, name) {
|
|
@@ -672,25 +699,26 @@ function isExcluded(policy, name, version) {
|
|
|
672
699
|
return true;
|
|
673
700
|
return false;
|
|
674
701
|
}
|
|
675
|
-
function
|
|
702
|
+
function isRegistryTarballResolution(resolution) {
|
|
676
703
|
if (resolution == null || typeof resolution !== 'object')
|
|
677
704
|
return false;
|
|
678
705
|
// Only plain tarball resolutions (npm registry / named registries) have no
|
|
679
706
|
// `type` field. Git / directory / binary / custom resolutions all carry one.
|
|
680
707
|
if ('type' in resolution && resolution.type != null)
|
|
681
708
|
return false;
|
|
682
|
-
// Git-hosted tarballs (codeload/gitlab/bitbucket) are special-cased in
|
|
683
|
-
// the resolver and aren't subject to release-age policy.
|
|
684
|
-
if ('gitHosted' in resolution && resolution.gitHosted)
|
|
685
|
-
return false;
|
|
686
709
|
const tarball = resolution.tarball;
|
|
687
710
|
if (typeof tarball === 'string') {
|
|
711
|
+
// Git-hosted tarballs (codeload/gitlab/bitbucket) are special-cased in
|
|
712
|
+
// the resolver and aren't subject to registry policy.
|
|
713
|
+
if (isGitHostedTarballUrl(tarball))
|
|
714
|
+
return false;
|
|
688
715
|
// Local/non-registry tarballs (for example `file:`) have no packument
|
|
689
716
|
// metadata, so minimumReleaseAge/trustPolicy verification cannot apply.
|
|
690
717
|
const protocol = tryParseUrl(tarball)?.protocol;
|
|
691
718
|
if (protocol != null && protocol !== 'http:' && protocol !== 'https:')
|
|
692
719
|
return false;
|
|
693
720
|
}
|
|
694
|
-
|
|
721
|
+
// Canonical registry entries may omit both `tarball` and `integrity`.
|
|
722
|
+
return true;
|
|
695
723
|
}
|
|
696
724
|
//# sourceMappingURL=createNpmResolutionVerifier.js.map
|
package/lib/fetch.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import url from 'node:url';
|
|
2
|
+
import util from 'node:util';
|
|
2
3
|
import { requestRetryLogger } from '@pnpm/core-loggers';
|
|
3
|
-
import { FetchError, PnpmError, } from '@pnpm/error';
|
|
4
|
+
import { FetchError, PnpmError, redactUrlCredentials, } from '@pnpm/error';
|
|
4
5
|
import { globalWarn } from '@pnpm/logger';
|
|
5
6
|
import * as retry from '@zkochan/retry';
|
|
6
7
|
import semver from 'semver';
|
|
@@ -87,7 +88,18 @@ export async function fetchMetadataFromFromRegistry(fetchOpts, pkgName, { authHe
|
|
|
87
88
|
});
|
|
88
89
|
}
|
|
89
90
|
catch (error) { // eslint-disable-line
|
|
90
|
-
|
|
91
|
+
// Redact credentials embedded in the URL from the cause as well, not
|
|
92
|
+
// just the top-level message: a reporter or debugger that renders
|
|
93
|
+
// `error.cause` would otherwise print the raw URL-bearing message. The
|
|
94
|
+
// `stack` string embeds the original (pre-mutation) message, so redact
|
|
95
|
+
// it too — mutating `message` alone leaves the credentials in `stack`.
|
|
96
|
+
if (util.types.isNativeError(error)) {
|
|
97
|
+
if (typeof error.message === 'string')
|
|
98
|
+
error.message = redactUrlCredentials(error.message);
|
|
99
|
+
if (typeof error.stack === 'string')
|
|
100
|
+
error.stack = redactUrlCredentials(error.stack);
|
|
101
|
+
}
|
|
102
|
+
reject(new PnpmError('META_FETCH_FAIL', redactUrlCredentials(`GET ${uri}: ${error.message}`), { attempts: attempt, cause: error }));
|
|
91
103
|
return;
|
|
92
104
|
}
|
|
93
105
|
if (response.status === 304) {
|
package/lib/pickPackage.d.ts
CHANGED
|
@@ -15,12 +15,14 @@ export interface PickPackageOptions extends PickPackageFromMetaOptions {
|
|
|
15
15
|
includeLatestTag?: boolean;
|
|
16
16
|
optional?: boolean;
|
|
17
17
|
/**
|
|
18
|
-
* When true,
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
18
|
+
* When true, force a conditional registry request so a stale on-disk
|
|
19
|
+
* packument can't satisfy the call: the on-disk exact-version fast
|
|
20
|
+
* path is skipped, and the in-memory cache is bypassed too. The fast
|
|
21
|
+
* path now promotes disk-loaded packuments into the in-memory cache,
|
|
22
|
+
* so an entry there can no longer be assumed to come from this
|
|
23
|
+
* install's own fresh network fetch — on a shared or long-lived
|
|
24
|
+
* resolver it might be disk-sourced, which would short-circuit the
|
|
25
|
+
* revalidation updateChecksums exists to force.
|
|
24
26
|
*/
|
|
25
27
|
updateChecksums?: boolean;
|
|
26
28
|
}
|
|
@@ -44,6 +46,28 @@ export declare function pickPackage(ctx: {
|
|
|
44
46
|
pickedPackage: PackageInRegistry | null;
|
|
45
47
|
}>;
|
|
46
48
|
export declare function encodePkgName(pkgName: string): string;
|
|
49
|
+
/**
|
|
50
|
+
* Key for the in-memory `metaCache` holding a package's registry metadata. The
|
|
51
|
+
* registry is part of the key so that a package of the same name served by two
|
|
52
|
+
* registries in one install can't collide on a single slot (which would resolve
|
|
53
|
+
* the wrong tarball/integrity). `fullMetadata` and `filterMetadata` keep the
|
|
54
|
+
* abbreviated, full, and filtered-full documents in distinct slots, mirroring
|
|
55
|
+
* the on-disk `metaDir` split: a `filterMetadata` resolver stores a `clearMeta`-
|
|
56
|
+
* stripped packument, so it must not share a slot with an unfiltered full one
|
|
57
|
+
* (reachable only when a `metaCache` is shared across resolvers with different
|
|
58
|
+
* settings). `filterMetadata` only narrows the full slot — abbreviated metadata
|
|
59
|
+
* shares one on-disk mirror regardless, so its key carries no filtered variant.
|
|
60
|
+
* `\x00` can't appear in a registry URL or a package name, so it's an
|
|
61
|
+
* unambiguous separator. The verifier reads this same cache and must build the
|
|
62
|
+
* key with this function.
|
|
63
|
+
*
|
|
64
|
+
* The registry is canonicalized to its origin plus a trailing-slashed path, so
|
|
65
|
+
* the resolver (which may pass a configured named-registry URL verbatim) and
|
|
66
|
+
* the verifier (which routes through trailing-slashed prefixes) converge on one
|
|
67
|
+
* key for the same logical registry instead of creating duplicate slots. Origin
|
|
68
|
+
* and path are preserved, so two registries that genuinely differ never collapse.
|
|
69
|
+
*/
|
|
70
|
+
export declare function getPkgMetaCacheKey(registry: string, pkgName: string, fullMetadata: boolean, filterMetadata: boolean): string;
|
|
47
71
|
/**
|
|
48
72
|
* Path of the on-disk JSONL document where pnpm mirrors a package's registry
|
|
49
73
|
* metadata. `metaDir` selects between abbreviated and full caches.
|
package/lib/pickPackage.js
CHANGED
|
@@ -121,10 +121,16 @@ export async function pickPackage(ctx, spec, opts) {
|
|
|
121
121
|
const metaDir = fullMetadata
|
|
122
122
|
? (ctx.filterMetadata ? FULL_FILTERED_META_DIR : FULL_META_DIR)
|
|
123
123
|
: ABBREVIATED_META_DIR;
|
|
124
|
-
// Cache key includes
|
|
125
|
-
|
|
124
|
+
// Cache key includes the registry so a package of the same name served by two
|
|
125
|
+
// registries in one install can't share a slot (which would resolve the wrong
|
|
126
|
+
// tarball/integrity), plus fullMetadata/filterMetadata so a request is never
|
|
127
|
+
// served a less-detailed or differently-stripped document than it asked for.
|
|
128
|
+
const cacheKey = getPkgMetaCacheKey(opts.registry, spec.name, fullMetadata, ctx.filterMetadata === true);
|
|
126
129
|
const pkgMirror = getPkgMirrorPath(ctx.cacheDir, metaDir, opts.registry, spec.name);
|
|
127
|
-
|
|
130
|
+
// updateChecksums must reach the conditional registry request below, so it
|
|
131
|
+
// can't be served from the in-memory cache — which may hold a disk-promoted
|
|
132
|
+
// entry rather than a fresh network fetch (see the updateChecksums doc).
|
|
133
|
+
const cachedMeta = opts.updateChecksums ? undefined : ctx.metaCache.get(cacheKey);
|
|
128
134
|
if (cachedMeta != null) {
|
|
129
135
|
// The in-memory cache may hold abbreviated metadata from an earlier call
|
|
130
136
|
// that didn't need `time` (no publishedBy then). If this call has
|
|
@@ -187,6 +193,7 @@ export async function pickPackage(ctx, spec, opts) {
|
|
|
187
193
|
try {
|
|
188
194
|
const pickedPackage = pickMatchingVersionFast(pickerOpts, spec, metaCachedInStore);
|
|
189
195
|
if (pickedPackage) {
|
|
196
|
+
ctx.metaCache.set(cacheKey, metaCachedInStore);
|
|
190
197
|
return {
|
|
191
198
|
meta: metaCachedInStore,
|
|
192
199
|
pickedPackage,
|
|
@@ -465,6 +472,43 @@ export function encodePkgName(pkgName) {
|
|
|
465
472
|
}
|
|
466
473
|
return pkgName;
|
|
467
474
|
}
|
|
475
|
+
/**
|
|
476
|
+
* Key for the in-memory `metaCache` holding a package's registry metadata. The
|
|
477
|
+
* registry is part of the key so that a package of the same name served by two
|
|
478
|
+
* registries in one install can't collide on a single slot (which would resolve
|
|
479
|
+
* the wrong tarball/integrity). `fullMetadata` and `filterMetadata` keep the
|
|
480
|
+
* abbreviated, full, and filtered-full documents in distinct slots, mirroring
|
|
481
|
+
* the on-disk `metaDir` split: a `filterMetadata` resolver stores a `clearMeta`-
|
|
482
|
+
* stripped packument, so it must not share a slot with an unfiltered full one
|
|
483
|
+
* (reachable only when a `metaCache` is shared across resolvers with different
|
|
484
|
+
* settings). `filterMetadata` only narrows the full slot — abbreviated metadata
|
|
485
|
+
* shares one on-disk mirror regardless, so its key carries no filtered variant.
|
|
486
|
+
* `\x00` can't appear in a registry URL or a package name, so it's an
|
|
487
|
+
* unambiguous separator. The verifier reads this same cache and must build the
|
|
488
|
+
* key with this function.
|
|
489
|
+
*
|
|
490
|
+
* The registry is canonicalized to its origin plus a trailing-slashed path, so
|
|
491
|
+
* the resolver (which may pass a configured named-registry URL verbatim) and
|
|
492
|
+
* the verifier (which routes through trailing-slashed prefixes) converge on one
|
|
493
|
+
* key for the same logical registry instead of creating duplicate slots. Origin
|
|
494
|
+
* and path are preserved, so two registries that genuinely differ never collapse.
|
|
495
|
+
*/
|
|
496
|
+
export function getPkgMetaCacheKey(registry, pkgName, fullMetadata, filterMetadata) {
|
|
497
|
+
const key = `${canonicalizeRegistry(registry)}\x00${pkgName}`;
|
|
498
|
+
if (!fullMetadata)
|
|
499
|
+
return key;
|
|
500
|
+
return filterMetadata ? `${key}:full:filtered` : `${key}:full`;
|
|
501
|
+
}
|
|
502
|
+
function canonicalizeRegistry(registry) {
|
|
503
|
+
try {
|
|
504
|
+
const parsed = new URL(registry);
|
|
505
|
+
const pathname = parsed.pathname.endsWith('/') ? parsed.pathname : `${parsed.pathname}/`;
|
|
506
|
+
return `${parsed.origin}${pathname}`;
|
|
507
|
+
}
|
|
508
|
+
catch {
|
|
509
|
+
return registry;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
468
512
|
/**
|
|
469
513
|
* Path of the on-disk JSONL document where pnpm mirrors a package's registry
|
|
470
514
|
* metadata. `metaDir` selects between abbreviated and full caches.
|
package/lib/violationCodes.d.ts
CHANGED
|
@@ -11,3 +11,4 @@
|
|
|
11
11
|
export declare const MINIMUM_RELEASE_AGE_VIOLATION_CODE = "MINIMUM_RELEASE_AGE_VIOLATION";
|
|
12
12
|
export declare const TRUST_DOWNGRADE_VIOLATION_CODE = "TRUST_DOWNGRADE";
|
|
13
13
|
export declare const TARBALL_URL_MISMATCH_VIOLATION_CODE = "TARBALL_URL_MISMATCH";
|
|
14
|
+
export declare const MISSING_TARBALL_INTEGRITY_VIOLATION_CODE = "MISSING_TARBALL_INTEGRITY";
|
package/lib/violationCodes.js
CHANGED
|
@@ -11,4 +11,5 @@
|
|
|
11
11
|
export const MINIMUM_RELEASE_AGE_VIOLATION_CODE = 'MINIMUM_RELEASE_AGE_VIOLATION';
|
|
12
12
|
export const TRUST_DOWNGRADE_VIOLATION_CODE = 'TRUST_DOWNGRADE';
|
|
13
13
|
export const TARBALL_URL_MISMATCH_VIOLATION_CODE = 'TARBALL_URL_MISMATCH';
|
|
14
|
+
export const MISSING_TARBALL_INTEGRITY_VIOLATION_CODE = 'MISSING_TARBALL_INTEGRITY';
|
|
14
15
|
//# sourceMappingURL=violationCodes.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pnpm/resolving.npm-resolver",
|
|
3
|
-
"version": "1102.0
|
|
3
|
+
"version": "1102.1.0",
|
|
4
4
|
"description": "Resolver for npm-hosted packages",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pnpm",
|
|
@@ -12,9 +12,9 @@
|
|
|
12
12
|
"funding": "https://opencollective.com/pnpm",
|
|
13
13
|
"repository": {
|
|
14
14
|
"type": "git",
|
|
15
|
-
"url": "https://github.com/pnpm/pnpm/tree/main/resolving/npm-resolver"
|
|
15
|
+
"url": "https://github.com/pnpm/pnpm/tree/main/pnpm11/resolving/npm-resolver"
|
|
16
16
|
},
|
|
17
|
-
"homepage": "https://github.com/pnpm/pnpm/tree/main/resolving/npm-resolver#readme",
|
|
17
|
+
"homepage": "https://github.com/pnpm/pnpm/tree/main/pnpm11/resolving/npm-resolver#readme",
|
|
18
18
|
"bugs": {
|
|
19
19
|
"url": "https://github.com/pnpm/pnpm/issues"
|
|
20
20
|
},
|
|
@@ -43,27 +43,27 @@
|
|
|
43
43
|
"semver-utils": "^1.1.4",
|
|
44
44
|
"ssri": "13.0.1",
|
|
45
45
|
"version-selector-type": "^3.0.0",
|
|
46
|
-
"@pnpm/
|
|
46
|
+
"@pnpm/config.pick-registry-for-package": "1100.0.9",
|
|
47
47
|
"@pnpm/constants": "1100.0.0",
|
|
48
|
+
"@pnpm/config.version-policy": "1100.1.6",
|
|
49
|
+
"@pnpm/core-loggers": "1100.2.1",
|
|
48
50
|
"@pnpm/crypto.hash": "1100.0.1",
|
|
49
|
-
"@pnpm/
|
|
51
|
+
"@pnpm/fetching.types": "1100.0.2",
|
|
52
|
+
"@pnpm/resolving.jsr-specifier-parser": "1100.0.1",
|
|
50
53
|
"@pnpm/fs.graceful-fs": "1100.1.0",
|
|
51
|
-
"@pnpm/resolving.resolver-base": "1100.4.2",
|
|
52
|
-
"@pnpm/resolving.jsr-specifier-parser": "1100.0.0",
|
|
53
54
|
"@pnpm/resolving.registry.types": "1100.1.3",
|
|
54
|
-
"@pnpm/resolving.
|
|
55
|
-
"@pnpm/
|
|
56
|
-
"@pnpm/store.cafs": "1100.1.
|
|
57
|
-
"@pnpm/store.index": "1100.2.0",
|
|
55
|
+
"@pnpm/resolving.resolver-base": "1100.5.0",
|
|
56
|
+
"@pnpm/store.index": "1100.2.1",
|
|
57
|
+
"@pnpm/store.cafs": "1100.1.11",
|
|
58
58
|
"@pnpm/types": "1101.3.2",
|
|
59
|
-
"@pnpm/workspace.
|
|
60
|
-
"@pnpm/
|
|
61
|
-
"@pnpm/
|
|
62
|
-
"@pnpm/workspace.
|
|
59
|
+
"@pnpm/workspace.range-resolver": "1100.0.2",
|
|
60
|
+
"@pnpm/error": "1100.0.1",
|
|
61
|
+
"@pnpm/resolving.registry.pkg-metadata-filter": "1100.0.9",
|
|
62
|
+
"@pnpm/workspace.spec-parser": "1100.0.0"
|
|
63
63
|
},
|
|
64
64
|
"peerDependencies": {
|
|
65
65
|
"@pnpm/logger": "^1100.0.0",
|
|
66
|
-
"@pnpm/worker": "^1100.2.
|
|
66
|
+
"@pnpm/worker": "^1100.2.2"
|
|
67
67
|
},
|
|
68
68
|
"devDependencies": {
|
|
69
69
|
"@jest/globals": "30.4.1",
|
|
@@ -73,11 +73,11 @@
|
|
|
73
73
|
"@types/ssri": "^7.1.5",
|
|
74
74
|
"load-json-file": "^7.0.1",
|
|
75
75
|
"tempy": "3.0.0",
|
|
76
|
-
"@pnpm/
|
|
77
|
-
"@pnpm/
|
|
78
|
-
"@pnpm/
|
|
79
|
-
"@pnpm/testing.mock-agent": "1101.0.
|
|
80
|
-
"@pnpm/
|
|
76
|
+
"@pnpm/network.fetch": "1100.1.4",
|
|
77
|
+
"@pnpm/resolving.npm-resolver": "1102.1.0",
|
|
78
|
+
"@pnpm/test-fixtures": "1100.0.0",
|
|
79
|
+
"@pnpm/testing.mock-agent": "1101.0.4",
|
|
80
|
+
"@pnpm/logger": "1100.0.0"
|
|
81
81
|
},
|
|
82
82
|
"engines": {
|
|
83
83
|
"node": ">=22.13"
|