@pnpm/resolving.npm-resolver 1101.3.1 → 1101.3.2
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 +11 -0
- package/lib/createNpmResolutionVerifier.js +146 -17
- package/lib/index.d.ts +8 -0
- package/lib/index.js +23 -5
- package/package.json +14 -14
|
@@ -2,6 +2,7 @@ import type { ResolutionVerifier } from '@pnpm/resolving.resolver-base';
|
|
|
2
2
|
import type { Registries, TrustPolicy } from '@pnpm/types';
|
|
3
3
|
import type { FetchMetadataFromFromRegistryOptions } from './fetch.js';
|
|
4
4
|
import { type FetchFullMetadataCachedOptions } from './fetchFullMetadataCached.js';
|
|
5
|
+
import type { PackageMetaCache } from './pickPackage.js';
|
|
5
6
|
export interface CreateNpmResolutionVerifierOptions {
|
|
6
7
|
/**
|
|
7
8
|
* Minimum age (in minutes) a published version must reach before it is
|
|
@@ -56,6 +57,16 @@ export interface CreateNpmResolutionVerifierOptions {
|
|
|
56
57
|
fetchOpts: FetchMetadataFromFromRegistryOptions;
|
|
57
58
|
getAuthHeaderValueByURI: (registry: string) => string | undefined;
|
|
58
59
|
cacheDir?: FetchFullMetadataCachedOptions['cacheDir'];
|
|
60
|
+
/**
|
|
61
|
+
* Per-install LRU shared with the npm resolver's `pickPackage`
|
|
62
|
+
* (`{ get, set }` over `PackageMeta`). When provided, the verifier
|
|
63
|
+
* consults it before fetching: a name the resolver already pulled
|
|
64
|
+
* during the same install yields the cached packument instead of a
|
|
65
|
+
* fresh disk/network round-trip. Optional — frozen-install paths and
|
|
66
|
+
* unit tests don't have a resolver running alongside, in which case
|
|
67
|
+
* the verifier falls back to its own fetch chain.
|
|
68
|
+
*/
|
|
69
|
+
metaCache?: PackageMetaCache;
|
|
59
70
|
/** Overrides Date.now() for tests. */
|
|
60
71
|
now?: number;
|
|
61
72
|
}
|
|
@@ -74,6 +74,7 @@ export function createNpmResolutionVerifier(opts) {
|
|
|
74
74
|
getAuthHeaderValueByURI: opts.getAuthHeaderValueByURI,
|
|
75
75
|
cacheDir: opts.cacheDir,
|
|
76
76
|
cutoffMs: cutoff,
|
|
77
|
+
sharedMetaCache: opts.metaCache,
|
|
77
78
|
abbreviatedMetaCache: new Map(),
|
|
78
79
|
publishedAtCache: new Map(),
|
|
79
80
|
localMetaCache: new Map(),
|
|
@@ -268,21 +269,89 @@ function fetchFullMetaForTrust(context, registry, name) {
|
|
|
268
269
|
const cacheKey = `${registry}\x00${name}`;
|
|
269
270
|
let cachedPromise = context.fullMetaForTrustCache.get(cacheKey);
|
|
270
271
|
if (cachedPromise == null) {
|
|
271
|
-
//
|
|
272
|
-
//
|
|
273
|
-
//
|
|
274
|
-
//
|
|
275
|
-
//
|
|
276
|
-
//
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
272
|
+
// Fast path: if the resolver already upgraded to full meta for this
|
|
273
|
+
// name during the same install (e.g. minimumReleaseAge active),
|
|
274
|
+
// reuse that document. Abbreviated meta is rejected here — it lacks
|
|
275
|
+
// per-version `time` and per-version trust evidence, both required
|
|
276
|
+
// by failIfTrustDowngraded.
|
|
277
|
+
//
|
|
278
|
+
// Limitation: the resolver's `metaCache` keys by `${name}:full` —
|
|
279
|
+
// it doesn't include the registry (pickPackage.ts cacheKey shape).
|
|
280
|
+
// If two registries serve packages of the same name in one install
|
|
281
|
+
// the resolver itself silently keeps the first fetch; the verifier
|
|
282
|
+
// here inherits that scope. The name check below is a defensive
|
|
283
|
+
// guard against accidental cache mixups; tightening this to a
|
|
284
|
+
// registry-qualified read needs the resolver's `metaCache` key
|
|
285
|
+
// shape to change first.
|
|
286
|
+
const shared = readSharedMetaForTrust(context.sharedMetaCache, name);
|
|
287
|
+
if (shared != null) {
|
|
288
|
+
cachedPromise = Promise.resolve(projectTrustMeta(shared));
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
// Don't swallow the fetch rejection here — `runTrustCheck` catches it
|
|
292
|
+
// and surfaces the underlying message in the violation reason, which
|
|
293
|
+
// is more actionable than the generic "metadata is unavailable" the
|
|
294
|
+
// `!meta` fallback emits. The cache still holds the rejected promise
|
|
295
|
+
// so repeat verifier calls for the same (registry, name) within one
|
|
296
|
+
// install don't refetch a known-failing endpoint.
|
|
297
|
+
//
|
|
298
|
+
// The fetched packument is projected down to just the trust-relevant
|
|
299
|
+
// fields (per-version `_npmUser.trustedPublisher` and
|
|
300
|
+
// `dist.attestations.provenance`, plus the package-level `time` map)
|
|
301
|
+
// before being stored. The full document — dependency maps, scripts,
|
|
302
|
+
// READMEs for every version — would otherwise stay resident in this
|
|
303
|
+
// map for the entire install, which on multi-thousand-entry
|
|
304
|
+
// workspaces OOMs CI runners with a 2GB heap (see #11860).
|
|
305
|
+
cachedPromise = fetchFullMetadataCached(context.fetchOpts, name, {
|
|
306
|
+
registry,
|
|
307
|
+
authHeaderValue: context.getAuthHeaderValueByURI(registry),
|
|
308
|
+
cacheDir: context.cacheDir,
|
|
309
|
+
}).then(projectTrustMeta);
|
|
310
|
+
}
|
|
282
311
|
context.fullMetaForTrustCache.set(cacheKey, cachedPromise);
|
|
283
312
|
}
|
|
284
313
|
return cachedPromise;
|
|
285
314
|
}
|
|
315
|
+
// Project the full packument to a minimal `PackageMeta`-shaped view
|
|
316
|
+
// that exposes only the fields `failIfTrustDowngraded` reads:
|
|
317
|
+
// • `name` and `modified` for error messages and cache keys
|
|
318
|
+
// • `time` for the per-version publish-date walk
|
|
319
|
+
// • `versions[v]._npmUser.trustedPublisher`
|
|
320
|
+
// • `versions[v].dist.attestations.provenance`
|
|
321
|
+
// The shape is still a valid `PackageMeta` so the downstream consumer
|
|
322
|
+
// doesn't have to special-case it — only the bulk fields (dependency
|
|
323
|
+
// graph, scripts, README, etc.) are dropped.
|
|
324
|
+
function projectTrustMeta(meta) {
|
|
325
|
+
const versions = {};
|
|
326
|
+
for (const [version, manifest] of Object.entries(meta.versions ?? {})) {
|
|
327
|
+
versions[version] = projectTrustManifest(manifest);
|
|
328
|
+
}
|
|
329
|
+
return {
|
|
330
|
+
name: meta.name,
|
|
331
|
+
'dist-tags': {},
|
|
332
|
+
versions,
|
|
333
|
+
time: meta.time,
|
|
334
|
+
modified: meta.modified,
|
|
335
|
+
etag: meta.etag,
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
function projectTrustManifest(manifest) {
|
|
339
|
+
// Drop everything except the trust-evidence fields. `PackageInRegistry.dist`
|
|
340
|
+
// is typed as requiring `shasum` and `tarball`, but the trust check never
|
|
341
|
+
// reads them; cast away the unsoundness so callers see the same nominal
|
|
342
|
+
// shape without the per-version dependency graph / scripts / README bulk
|
|
343
|
+
// carrying through. `_npmUser` is similarly narrowed to just
|
|
344
|
+
// `trustedPublisher` — the only sub-field the trust check inspects — so
|
|
345
|
+
// we don't keep maintainer name/email PII resident in the cache.
|
|
346
|
+
const trustedPublisher = manifest._npmUser?.trustedPublisher;
|
|
347
|
+
const provenance = manifest.dist?.attestations?.provenance;
|
|
348
|
+
return {
|
|
349
|
+
_npmUser: trustedPublisher != null ? { trustedPublisher } : undefined,
|
|
350
|
+
dist: provenance != null
|
|
351
|
+
? { attestations: { provenance } }
|
|
352
|
+
: undefined,
|
|
353
|
+
};
|
|
354
|
+
}
|
|
286
355
|
/**
|
|
287
356
|
* Per-(registry, name, version) lookup with a layered fallback:
|
|
288
357
|
*
|
|
@@ -360,7 +429,7 @@ async function tryAbbreviatedModifiedShortcut(context, registry, name, version)
|
|
|
360
429
|
// publish time — but only for versions the registry currently lists.
|
|
361
430
|
// An unpublished or never-published pin would otherwise pass the gate
|
|
362
431
|
// on a stale package-level timestamp.
|
|
363
|
-
if (!meta?.
|
|
432
|
+
if (!meta?.versionNames?.has(version))
|
|
364
433
|
return undefined;
|
|
365
434
|
return modified;
|
|
366
435
|
}
|
|
@@ -368,15 +437,75 @@ function fetchAbbreviatedMeta(context, registry, name) {
|
|
|
368
437
|
const cacheKey = `${registry}\x00${name}`;
|
|
369
438
|
let cachedPromise = context.abbreviatedMetaCache.get(cacheKey);
|
|
370
439
|
if (cachedPromise == null) {
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
440
|
+
// Fast path: the resolver's per-install LRU already holds this
|
|
441
|
+
// packument from its own pickPackage pass — abbreviated or full.
|
|
442
|
+
// Project it for the shortcut and skip the disk/network round-trip.
|
|
443
|
+
// Mismatch on `name` is the same risk the resolver carries today
|
|
444
|
+
// (its cache key omits the registry), so reuse is no less correct
|
|
445
|
+
// than the resolver's own get.
|
|
446
|
+
const shared = readSharedMeta(context.sharedMetaCache, name);
|
|
447
|
+
if (shared != null) {
|
|
448
|
+
cachedPromise = Promise.resolve(projectAbbreviatedMeta(shared));
|
|
449
|
+
}
|
|
450
|
+
else {
|
|
451
|
+
cachedPromise = fetchAbbreviatedMetadataCached(context.fetchOpts, name, {
|
|
452
|
+
registry,
|
|
453
|
+
authHeaderValue: context.getAuthHeaderValueByURI(registry),
|
|
454
|
+
cacheDir: context.cacheDir,
|
|
455
|
+
}).then(projectAbbreviatedMeta, () => undefined);
|
|
456
|
+
}
|
|
376
457
|
context.abbreviatedMetaCache.set(cacheKey, cachedPromise);
|
|
377
458
|
}
|
|
378
459
|
return cachedPromise;
|
|
379
460
|
}
|
|
461
|
+
function readSharedMeta(cache, name) {
|
|
462
|
+
if (cache == null)
|
|
463
|
+
return undefined;
|
|
464
|
+
// Prefer the full entry — a `name:full` hit subsumes the abbreviated
|
|
465
|
+
// hit (full meta carries every field the abbreviated form does, plus
|
|
466
|
+
// `time` and per-version trust evidence the trust check needs). The
|
|
467
|
+
// resolver only populates `name:full` when the install ran with
|
|
468
|
+
// `minimumReleaseAge` configured, otherwise the bare `name` key holds
|
|
469
|
+
// the abbreviated form.
|
|
470
|
+
return validateSharedMeta(cache.get(`${name}:full`), name) ??
|
|
471
|
+
validateSharedMeta(cache.get(name), name);
|
|
472
|
+
}
|
|
473
|
+
function readSharedMetaForTrust(cache, name) {
|
|
474
|
+
if (cache == null)
|
|
475
|
+
return undefined;
|
|
476
|
+
// Abbreviated meta is rejected for the trust check — it lacks
|
|
477
|
+
// per-version `time` and per-version trust evidence.
|
|
478
|
+
return validateSharedMeta(cache.get(`${name}:full`), name);
|
|
479
|
+
}
|
|
480
|
+
// Defensive guard against the resolver's `name`-only cache key
|
|
481
|
+
// returning something unexpected. The known correctness gap (two
|
|
482
|
+
// registries serving the same package name share one cache slot)
|
|
483
|
+
// is inherited from the resolver itself — see the `pickPackage.ts`
|
|
484
|
+
// cacheKey shape; the verifier can't be stricter than the resolver
|
|
485
|
+
// without changing both. This name check at least catches accidental
|
|
486
|
+
// returns of a different package (cache corruption, factory misuse)
|
|
487
|
+
// rather than silently feeding wrong data to the trust / age check.
|
|
488
|
+
function validateSharedMeta(meta, name) {
|
|
489
|
+
if (meta == null)
|
|
490
|
+
return undefined;
|
|
491
|
+
if (meta.name !== name)
|
|
492
|
+
return undefined;
|
|
493
|
+
return meta;
|
|
494
|
+
}
|
|
495
|
+
// Project the abbreviated packument down to the two fields the verifier
|
|
496
|
+
// actually reads — package-level `modified` and the set of version names
|
|
497
|
+
// for the existence check inside `tryAbbreviatedModifiedShortcut`. The
|
|
498
|
+
// resolver populates the abbreviated mirror with every version's
|
|
499
|
+
// dependency / engine / dist info, which can run to hundreds of KB per
|
|
500
|
+
// package and accumulate to many GB across a multi-thousand-entry
|
|
501
|
+
// lockfile (see #11860). The full document is GC-able as soon as this
|
|
502
|
+
// closure returns.
|
|
503
|
+
function projectAbbreviatedMeta(meta) {
|
|
504
|
+
return {
|
|
505
|
+
modified: meta.modified,
|
|
506
|
+
versionNames: meta.versions ? new Set(Object.keys(meta.versions)) : undefined,
|
|
507
|
+
};
|
|
508
|
+
}
|
|
380
509
|
function readLocalMetaTime(context, registry, name) {
|
|
381
510
|
if (!context.cacheDir)
|
|
382
511
|
return Promise.resolve(undefined);
|
package/lib/index.d.ts
CHANGED
|
@@ -118,3 +118,11 @@ export type ResolveFromNpmOptions = {
|
|
|
118
118
|
projectDir: string;
|
|
119
119
|
workspacePackages: WorkspacePackages;
|
|
120
120
|
});
|
|
121
|
+
/**
|
|
122
|
+
* Construct the LRU `PackageMetaCache` instance the resolver uses by
|
|
123
|
+
* default. Exported so the install layer can build one cache and hand
|
|
124
|
+
* the same reference to both the resolver and the verifier — the
|
|
125
|
+
* verifier's fast path reads from it when the resolver has already
|
|
126
|
+
* fetched a packument during the same install.
|
|
127
|
+
*/
|
|
128
|
+
export declare function createDefaultPackageMetaCache(): PackageMetaCache;
|
package/lib/index.js
CHANGED
|
@@ -76,10 +76,15 @@ export function createNpmResolver(fetchFromRegistry, getAuthHeader, opts) {
|
|
|
76
76
|
const fetch = pMemoize(fetchMetadataFromFromRegistry.bind(null, fetchOpts), {
|
|
77
77
|
cacheKey: (...args) => JSON.stringify(args),
|
|
78
78
|
});
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
79
|
+
// Track ownership so `clearCache()` below only wipes the in-memory
|
|
80
|
+
// cache when this factory created it. A caller-supplied
|
|
81
|
+
// `opts.metaCache` may be shared with another resolver instance (or
|
|
82
|
+
// outlive this resolver entirely — e.g. a long-lived agent process
|
|
83
|
+
// that keeps one cache across many install requests); clearing it
|
|
84
|
+
// here would silently evict entries that other consumers are still
|
|
85
|
+
// using.
|
|
86
|
+
const ownsMetaCache = opts.metaCache == null;
|
|
87
|
+
const metaCache = opts.metaCache ?? createDefaultPackageMetaCache();
|
|
83
88
|
// Create peek function if storeDir is provided
|
|
84
89
|
const storeDir = opts.storeDir;
|
|
85
90
|
const peekLockerForPeek = new Map();
|
|
@@ -137,7 +142,7 @@ export function createNpmResolver(fetchFromRegistry, getAuthHeader, opts) {
|
|
|
137
142
|
resolveLatestFromJsr: createResolveLatest(boundResolveFromJsr, isJsrSpec),
|
|
138
143
|
resolveLatestFromNamedRegistry: createResolveLatest(boundResolveFromNamedRegistry, (query) => isNamedRegistrySpec(query, ctx.namedRegistryNames)),
|
|
139
144
|
clearCache: () => {
|
|
140
|
-
if ('clear' in metaCache && typeof metaCache.clear === 'function') {
|
|
145
|
+
if (ownsMetaCache && 'clear' in metaCache && typeof metaCache.clear === 'function') {
|
|
141
146
|
metaCache.clear();
|
|
142
147
|
}
|
|
143
148
|
pMemoizeClear(fetch);
|
|
@@ -730,4 +735,17 @@ function createVersionSpec(version, pinnedVersion) {
|
|
|
730
735
|
throw new PnpmError('BAD_PINNED_VERSION', `Cannot pin '${pinnedVersion ?? 'undefined'}'`);
|
|
731
736
|
}
|
|
732
737
|
}
|
|
738
|
+
/**
|
|
739
|
+
* Construct the LRU `PackageMetaCache` instance the resolver uses by
|
|
740
|
+
* default. Exported so the install layer can build one cache and hand
|
|
741
|
+
* the same reference to both the resolver and the verifier — the
|
|
742
|
+
* verifier's fast path reads from it when the resolver has already
|
|
743
|
+
* fetched a packument during the same install.
|
|
744
|
+
*/
|
|
745
|
+
export function createDefaultPackageMetaCache() {
|
|
746
|
+
return new LRUCache({
|
|
747
|
+
max: 10000,
|
|
748
|
+
ttl: 120 * 1000, // 2 minutes
|
|
749
|
+
});
|
|
750
|
+
}
|
|
733
751
|
//# sourceMappingURL=index.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pnpm/resolving.npm-resolver",
|
|
3
|
-
"version": "1101.3.
|
|
3
|
+
"version": "1101.3.2",
|
|
4
4
|
"description": "Resolver for npm-hosted packages",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pnpm",
|
|
@@ -41,22 +41,22 @@
|
|
|
41
41
|
"ssri": "13.0.1",
|
|
42
42
|
"version-selector-type": "^3.0.0",
|
|
43
43
|
"@pnpm/config.version-policy": "1100.1.1",
|
|
44
|
-
"@pnpm/config.pick-registry-for-package": "1100.0.5",
|
|
45
|
-
"@pnpm/constants": "1100.0.0",
|
|
46
44
|
"@pnpm/core-loggers": "1100.1.1",
|
|
47
45
|
"@pnpm/crypto.hash": "1100.0.1",
|
|
48
|
-
"@pnpm/error": "1100.0.0",
|
|
49
46
|
"@pnpm/fetching.types": "1100.0.1",
|
|
50
|
-
"@pnpm/
|
|
47
|
+
"@pnpm/constants": "1100.0.0",
|
|
51
48
|
"@pnpm/fs.graceful-fs": "1100.1.0",
|
|
52
|
-
"@pnpm/
|
|
49
|
+
"@pnpm/error": "1100.0.0",
|
|
50
|
+
"@pnpm/config.pick-registry-for-package": "1100.0.5",
|
|
53
51
|
"@pnpm/resolving.registry.pkg-metadata-filter": "1100.0.4",
|
|
52
|
+
"@pnpm/resolving.registry.types": "1100.0.4",
|
|
54
53
|
"@pnpm/resolving.resolver-base": "1100.3.0",
|
|
55
|
-
"@pnpm/
|
|
56
|
-
"@pnpm/types": "1101.1.1",
|
|
57
|
-
"@pnpm/workspace.range-resolver": "1100.0.1",
|
|
54
|
+
"@pnpm/resolving.jsr-specifier-parser": "1100.0.0",
|
|
58
55
|
"@pnpm/store.cafs": "1100.1.6",
|
|
59
|
-
"@pnpm/workspace.
|
|
56
|
+
"@pnpm/workspace.range-resolver": "1100.0.1",
|
|
57
|
+
"@pnpm/workspace.spec-parser": "1100.0.0",
|
|
58
|
+
"@pnpm/types": "1101.1.1",
|
|
59
|
+
"@pnpm/store.index": "1100.1.0"
|
|
60
60
|
},
|
|
61
61
|
"peerDependencies": {
|
|
62
62
|
"@pnpm/logger": ">=1001.0.0 <1002.0.0",
|
|
@@ -70,11 +70,11 @@
|
|
|
70
70
|
"@types/ssri": "^7.1.5",
|
|
71
71
|
"load-json-file": "^7.0.1",
|
|
72
72
|
"tempy": "3.0.0",
|
|
73
|
-
"@pnpm/network.fetch": "1100.0.6",
|
|
74
73
|
"@pnpm/logger": "1100.0.0",
|
|
75
|
-
"@pnpm/
|
|
76
|
-
"@pnpm/
|
|
77
|
-
"@pnpm/test-fixtures": "1100.0.0"
|
|
74
|
+
"@pnpm/network.fetch": "1100.0.6",
|
|
75
|
+
"@pnpm/resolving.npm-resolver": "1101.3.2",
|
|
76
|
+
"@pnpm/test-fixtures": "1100.0.0",
|
|
77
|
+
"@pnpm/testing.mock-agent": "1100.0.6"
|
|
78
78
|
},
|
|
79
79
|
"engines": {
|
|
80
80
|
"node": ">=22.13"
|