@pnpm/resolving.npm-resolver 1101.3.3 → 1101.5.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.
@@ -71,15 +71,18 @@ export interface CreateNpmResolutionVerifierOptions {
71
71
  now?: number;
72
72
  }
73
73
  /**
74
- * Returns a `ResolutionVerifier` that re-applies the `minimumReleaseAge`
75
- * and/or `trustPolicy='no-downgrade'` policies to npm-registry-resolved
76
- * lockfile entries, or `undefined` when no policy is active. Pairs with
77
- * `createNpmResolver`: each resolver factory may export a sibling
78
- * verifier factory that the default-resolver combines.
74
+ * Returns a `ResolutionVerifier` for npm-registry-resolved lockfile
75
+ * entries. It always binds each entry's recorded tarball URL to the
76
+ * artifact the registry's metadata lists (an anti-tamper check that does
77
+ * not depend on any policy), and additionally re-applies the
78
+ * `minimumReleaseAge` and/or `trustPolicy='no-downgrade'` policies when
79
+ * those are configured. Pairs with `createNpmResolver`: each resolver
80
+ * factory may export a sibling verifier factory that the default-resolver
81
+ * combines.
79
82
  *
80
83
  * Designed for fail-closed semantics: if the manifest can't be loaded or
81
84
  * the pinned version is missing from it, the verifier reports a violation
82
85
  * rather than silently passing. Mirrors the post-resolution gate bun added
83
86
  * for the same shape of bug in oven-sh/bun#30526.
84
87
  */
85
- export declare function createNpmResolutionVerifier(opts: CreateNpmResolutionVerifierOptions): ResolutionVerifier | undefined;
88
+ export declare function createNpmResolutionVerifier(opts: CreateNpmResolutionVerifierOptions): ResolutionVerifier;
@@ -5,16 +5,20 @@ import { PnpmError } from '@pnpm/error';
5
5
  import semver from 'semver';
6
6
  import { fetchAttestationPublishedAt } from './fetchAttestationPublishedAt.js';
7
7
  import { fetchAbbreviatedMetadataCached, fetchFullMetadataCached, } from './fetchFullMetadataCached.js';
8
+ import { normalizeRegistryUrl } from './normalizeRegistryUrl.js';
8
9
  import { BUILTIN_NAMED_REGISTRIES } from './parseBareSpecifier.js';
9
10
  import { getPkgMirrorPath, loadMeta, warnMissingTimeFieldOnce } from './pickPackage.js';
10
11
  import { failIfTrustDowngraded } from './trustChecks.js';
11
- import { MINIMUM_RELEASE_AGE_VIOLATION_CODE, TRUST_DOWNGRADE_VIOLATION_CODE, } from './violationCodes.js';
12
+ import { MINIMUM_RELEASE_AGE_VIOLATION_CODE, TARBALL_URL_MISMATCH_VIOLATION_CODE, TRUST_DOWNGRADE_VIOLATION_CODE, } from './violationCodes.js';
12
13
  /**
13
- * Returns a `ResolutionVerifier` that re-applies the `minimumReleaseAge`
14
- * and/or `trustPolicy='no-downgrade'` policies to npm-registry-resolved
15
- * lockfile entries, or `undefined` when no policy is active. Pairs with
16
- * `createNpmResolver`: each resolver factory may export a sibling
17
- * verifier factory that the default-resolver combines.
14
+ * Returns a `ResolutionVerifier` for npm-registry-resolved lockfile
15
+ * entries. It always binds each entry's recorded tarball URL to the
16
+ * artifact the registry's metadata lists (an anti-tamper check that does
17
+ * not depend on any policy), and additionally re-applies the
18
+ * `minimumReleaseAge` and/or `trustPolicy='no-downgrade'` policies when
19
+ * those are configured. Pairs with `createNpmResolver`: each resolver
20
+ * factory may export a sibling verifier factory that the default-resolver
21
+ * combines.
18
22
  *
19
23
  * Designed for fail-closed semantics: if the manifest can't be loaded or
20
24
  * the pinned version is missing from it, the verifier reports a violation
@@ -24,10 +28,6 @@ import { MINIMUM_RELEASE_AGE_VIOLATION_CODE, TRUST_DOWNGRADE_VIOLATION_CODE, } f
24
28
  export function createNpmResolutionVerifier(opts) {
25
29
  const ageCheckActive = Boolean(opts.minimumReleaseAge);
26
30
  const trustCheckActive = opts.trustPolicy === 'no-downgrade';
27
- // No policy → no verifier. Skipping early keeps the install-side fan-out
28
- // empty when nothing is configured.
29
- if (!ageCheckActive && !trustCheckActive)
30
- return undefined;
31
31
  const cutoff = ageCheckActive
32
32
  ? (opts.now ?? Date.now()) - opts.minimumReleaseAge * 60 * 1000
33
33
  : 0;
@@ -84,20 +84,37 @@ export function createNpmResolutionVerifier(opts) {
84
84
  const minimumReleaseAge = opts.minimumReleaseAge ?? 0;
85
85
  const trustPolicy = opts.trustPolicy;
86
86
  const trustPolicyIgnoreAfter = opts.trustPolicyIgnoreAfter;
87
- const verify = async (resolution, { name, version }) => {
87
+ const verify = async (resolution, { name, version, nonSemverVersion }) => {
88
88
  if (!isNpmRegistryResolution(resolution))
89
89
  return { ok: true };
90
- // Non-semver versions identify URL tarballs, file: refs, git refs, etc.
91
- // Neither the age nor the trust policy applies, and a registry lookup
90
+ // URL/git-keyed entries are deliberate non-registry deps. They can still
91
+ // carry a semver `version` copied from the resolved manifest, so the
92
+ // semver guard below isn't enough on its own — the registry policies and
93
+ // the tarball-URL binding don't apply to them, and a registry lookup
92
94
  // would 404.
95
+ if (nonSemverVersion != null)
96
+ return { ok: true };
93
97
  if (!semver.valid(version))
94
98
  return { ok: true };
99
+ const tarballUrl = resolution.tarball;
100
+ const registry = pickRegistryForVersion(opts.registries, namedRegistryPrefixes, name, tarballUrl);
101
+ // A registry entry that pins an explicit tarball URL must point at the
102
+ // artifact the registry's own metadata lists. Otherwise a trusted
103
+ // `name@version` could front bytes from an attacker-chosen URL (with a
104
+ // matching integrity for those bytes). This binding is unconditional —
105
+ // it does not depend on `minimumReleaseAge`/`trustPolicy` and isn't
106
+ // narrowed by their exclude lists, since it guards integrity rather
107
+ // than maturity/trust. Registry entries with no tarball URL reconstruct
108
+ // it from name+version+registry, so they're inherently bound.
109
+ if (typeof tarballUrl === 'string') {
110
+ const urlViolation = await runTarballUrlCheck(lookupContext, registry, name, version, tarballUrl);
111
+ if (urlViolation)
112
+ return urlViolation;
113
+ }
95
114
  const ageApplies = ageCheckActive && !isExcluded(excludePolicy, name, version);
96
115
  const trustApplies = trustCheckActive && !isExcluded(trustExcludePolicy, name, version);
97
116
  if (!ageApplies && !trustApplies)
98
117
  return { ok: true };
99
- const tarballUrl = resolution.tarball;
100
- const registry = pickRegistryForVersion(opts.registries, namedRegistryPrefixes, name, tarballUrl);
101
118
  if (ageApplies) {
102
119
  const ageViolation = await runAgeCheck(lookupContext, registry, name, version, cutoff, opts.ignoreMissingTimeField === true);
103
120
  if (ageViolation)
@@ -127,6 +144,12 @@ export function createNpmResolutionVerifier(opts) {
127
144
  return {
128
145
  verify,
129
146
  policy: {
147
+ // Marks runs that enforced the tarball-URL binding. A cache record
148
+ // written before this rule existed lacks the flag, so
149
+ // `canTrustPastCheck` rejects it and forces a re-verification that
150
+ // applies the binding — otherwise an upgrade could keep trusting a
151
+ // lockfile that was only ever age/trust-checked.
152
+ tarballUrlBinding: true,
130
153
  minimumReleaseAge,
131
154
  minimumReleaseAgeExclude: sortedMinAgeExcludes,
132
155
  trustPolicy: trustPolicy ?? null,
@@ -134,6 +157,10 @@ export function createNpmResolutionVerifier(opts) {
134
157
  trustPolicyIgnoreAfter: trustPolicyIgnoreAfter ?? null,
135
158
  },
136
159
  canTrustPastCheck: (cached) => {
160
+ // The tarball-URL binding is unconditional today; a cached run that
161
+ // didn't record it can't be trusted to have enforced it.
162
+ if (cached.tarballUrlBinding !== true)
163
+ return false;
137
164
  // Maturity: a previously cached run under a larger cutoff
138
165
  // (stricter window) is trustworthy under a smaller current one —
139
166
  // its set of accepted versions is a subset of today's. The
@@ -223,6 +250,44 @@ async function runAgeCheck(context, registry, name, version, cutoff, ignoreMissi
223
250
  }
224
251
  return undefined;
225
252
  }
253
+ /**
254
+ * Confirm the lockfile-pinned tarball URL is the artifact the registry's
255
+ * own metadata lists for this exact `name@version`.
256
+ *
257
+ * Fail-closed: the entry passes only when the registry metadata
258
+ * affirmatively lists this version with a matching tarball URL. If the
259
+ * metadata can't be fetched, doesn't list the version, or omits
260
+ * `dist.tarball`, the entry can't be confirmed and is rejected — otherwise
261
+ * a tampered lockfile could smuggle a malicious URL past the check by
262
+ * pointing it at a `name@version` the registry can't vouch for.
263
+ */
264
+ async function runTarballUrlCheck(context, registry, name, version, lockfileTarball) {
265
+ const meta = await fetchAbbreviatedMeta(context, registry, name);
266
+ const registryTarball = meta?.versionTarballs?.get(version);
267
+ if (registryTarball != null && sameTarballUrl(lockfileTarball, registryTarball)) {
268
+ return undefined;
269
+ }
270
+ return {
271
+ ok: false,
272
+ code: TARBALL_URL_MISMATCH_VIOLATION_CODE,
273
+ reason: registryTarball == null
274
+ ? "could not be verified against the registry's published metadata"
275
+ : `has a tarball URL (${lockfileTarball}) that does not match the registry's published metadata (${registryTarball})`,
276
+ };
277
+ }
278
+ function sameTarballUrl(a, b) {
279
+ return canonicalTarballUrl(a) === canonicalTarballUrl(b);
280
+ }
281
+ // Mirror the tolerance toLockfileResolution applies when it decides whether
282
+ // a tarball URL is "the expected one": ignore the protocol and `%2f` scope
283
+ // encoding so a benign http/https or encoding difference isn't read as
284
+ // tampering. The `%2f` match is case-insensitive because `normalizeRegistryUrl`
285
+ // (`new URL().toString()`) can upper-case percent-escapes to `%2F`.
286
+ function canonicalTarballUrl(url) {
287
+ const normalized = normalizeRegistryUrl(url).replace(/%2f/gi, '/');
288
+ const schemeEnd = normalized.indexOf('://');
289
+ return schemeEnd === -1 ? normalized : normalized.slice(schemeEnd + 3);
290
+ }
226
291
  /**
227
292
  * Run the resolver-time `failIfTrustDowngraded` check against the
228
293
  * pinned lockfile version. The packument is fetched through a
@@ -341,12 +406,23 @@ function projectTrustManifest(manifest) {
341
406
  // reads them; cast away the unsoundness so callers see the same nominal
342
407
  // shape without the per-version dependency graph / scripts / README bulk
343
408
  // 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.
409
+ // `trustedPublisher` and `approver` — the only sub-fields the trust check
410
+ // inspects — so we don't keep maintainer name/email PII resident in the
411
+ // cache.
412
+ const approver = manifest._npmUser?.approver;
346
413
  const trustedPublisher = manifest._npmUser?.trustedPublisher;
347
414
  const provenance = manifest.dist?.attestations?.provenance;
415
+ let npmUser = undefined;
416
+ if (approver) {
417
+ npmUser ||= {};
418
+ npmUser.approver = {};
419
+ }
420
+ if (trustedPublisher) {
421
+ npmUser ||= {};
422
+ npmUser.trustedPublisher = trustedPublisher;
423
+ }
348
424
  return {
349
- _npmUser: trustedPublisher != null ? { trustedPublisher } : undefined,
425
+ _npmUser: npmUser,
350
426
  dist: provenance != null
351
427
  ? { attestations: { provenance } }
352
428
  : undefined,
@@ -429,7 +505,7 @@ async function tryAbbreviatedModifiedShortcut(context, registry, name, version)
429
505
  // publish time — but only for versions the registry currently lists.
430
506
  // An unpublished or never-published pin would otherwise pass the gate
431
507
  // on a stale package-level timestamp.
432
- if (!meta?.versionNames?.has(version))
508
+ if (!meta?.versionTarballs?.has(version))
433
509
  return undefined;
434
510
  return modified;
435
511
  }
@@ -492,18 +568,26 @@ function validateSharedMeta(meta, name) {
492
568
  return undefined;
493
569
  return meta;
494
570
  }
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
571
+ // Project the abbreviated packument down to the few fields the verifier
572
+ // actually reads — package-level `modified`, plus a per-version map of
573
+ // `dist.tarball` (whose keys double as the version-existence set for the
574
+ // `tryAbbreviatedModifiedShortcut` check and the tarball-URL binding). The
498
575
  // resolver populates the abbreviated mirror with every version's
499
576
  // dependency / engine / dist info, which can run to hundreds of KB per
500
577
  // package and accumulate to many GB across a multi-thousand-entry
501
578
  // lockfile (see #11860). The full document is GC-able as soon as this
502
- // closure returns.
579
+ // closure returns; only the short tarball-URL strings are retained.
503
580
  function projectAbbreviatedMeta(meta) {
581
+ let versionTarballs;
582
+ if (meta.versions) {
583
+ versionTarballs = new Map();
584
+ for (const [version, manifest] of Object.entries(meta.versions)) {
585
+ versionTarballs.set(version, manifest.dist?.tarball);
586
+ }
587
+ }
504
588
  return {
505
589
  modified: meta.modified,
506
- versionNames: meta.versions ? new Set(Object.keys(meta.versions)) : undefined,
590
+ versionTarballs,
507
591
  };
508
592
  }
509
593
  function readLocalMetaTime(context, registry, name) {
@@ -542,12 +626,14 @@ function pickRegistryForVersion(registries, namedRegistryPrefixes, name, tarball
542
626
  // origin we'd otherwise miss. Match the longest prefix so that two named
543
627
  // registries sharing a host but differing by path don't collide.
544
628
  if (tarballUrl) {
545
- const normalized = tryParseUrl(tarballUrl)?.toString();
546
- if (normalized) {
547
- for (const prefix of namedRegistryPrefixes) {
548
- if (normalized.startsWith(prefix))
549
- return prefix;
550
- }
629
+ // Match on the same canonical form the tarball comparison uses, so a
630
+ // named-registry tarball that differs from the configured base only by
631
+ // scheme or `%2f` encoding still routes to its registry instead of
632
+ // falling back (and then failing closed against the wrong packument).
633
+ const normalized = canonicalTarballUrl(tarballUrl);
634
+ for (const prefix of namedRegistryPrefixes) {
635
+ if (normalized.startsWith(canonicalTarballUrl(prefix)))
636
+ return prefix;
551
637
  }
552
638
  }
553
639
  return pickRegistryForPackage(registries, name);
@@ -597,6 +683,14 @@ function isNpmRegistryResolution(resolution) {
597
683
  // the resolver and aren't subject to release-age policy.
598
684
  if ('gitHosted' in resolution && resolution.gitHosted)
599
685
  return false;
686
+ const tarball = resolution.tarball;
687
+ if (typeof tarball === 'string') {
688
+ // Local/non-registry tarballs (for example `file:`) have no packument
689
+ // metadata, so minimumReleaseAge/trustPolicy verification cannot apply.
690
+ const protocol = tryParseUrl(tarball)?.protocol;
691
+ if (protocol != null && protocol !== 'http:' && protocol !== 'https:')
692
+ return false;
693
+ }
600
694
  return 'tarball' in resolution || 'integrity' in resolution;
601
695
  }
602
696
  //# sourceMappingURL=createNpmResolutionVerifier.js.map
@@ -201,7 +201,7 @@ export async function pickPackage(ctx, spec, opts) {
201
201
  }
202
202
  }
203
203
  }
204
- if (opts.publishedBy) {
204
+ if (opts.publishedBy && opts.publishedByExclude?.(spec.name) !== true) {
205
205
  const mtime = await limit(async () => getFileMtime(pkgMirror));
206
206
  if (mtime != null && mtime >= opts.publishedBy) {
207
207
  metaCachedInStore = metaCachedInStore ?? await limit(async () => loadMeta(pkgMirror));
@@ -1,6 +1,6 @@
1
1
  import type { PackageInRegistry, PackageMeta } from '@pnpm/resolving.registry.types';
2
2
  import type { PackageVersionPolicy } from '@pnpm/types';
3
- type TrustEvidence = 'provenance' | 'trustedPublisher';
3
+ type TrustEvidence = 'provenance' | 'trustedPublisher' | 'stagedPublish';
4
4
  export declare function failIfTrustDowngraded(meta: PackageMeta, version: string, opts?: {
5
5
  trustPolicyExclude?: PackageVersionPolicy;
6
6
  trustPolicyIgnoreAfter?: number;
@@ -2,6 +2,7 @@ import { PnpmError } from '@pnpm/error';
2
2
  import semver from 'semver';
3
3
  import { assertMetaHasTime } from './pickPackageFromMeta.js';
4
4
  const TRUST_RANK = {
5
+ stagedPublish: 3,
5
6
  trustedPublisher: 2,
6
7
  provenance: 1,
7
8
  };
@@ -51,6 +52,7 @@ export function failIfTrustDowngraded(meta, version, opts) {
51
52
  }
52
53
  function prettyPrintTrustEvidence(trustEvidence) {
53
54
  switch (trustEvidence) {
55
+ case 'stagedPublish': return 'staged publish';
54
56
  case 'trustedPublisher': return 'trusted publisher';
55
57
  case 'provenance': return 'provenance attestation';
56
58
  default: return 'no trust evidence';
@@ -70,14 +72,19 @@ function detectStrongestTrustEvidenceBeforeDate(meta, beforeDate, options) {
70
72
  const trustEvidence = getTrustEvidence(manifest);
71
73
  if (!trustEvidence)
72
74
  continue;
73
- if (trustEvidence === 'trustedPublisher') {
74
- return 'trustedPublisher';
75
+ if (best === undefined || TRUST_RANK[trustEvidence] > TRUST_RANK[best]) {
76
+ best = trustEvidence;
77
+ if (best === 'stagedPublish') {
78
+ return best;
79
+ }
75
80
  }
76
- best ||= 'provenance';
77
81
  }
78
82
  return best;
79
83
  }
80
84
  export function getTrustEvidence(manifest) {
85
+ if (manifest._npmUser?.approver) {
86
+ return 'stagedPublish';
87
+ }
81
88
  if (manifest._npmUser?.trustedPublisher && manifest.dist?.attestations?.provenance) {
82
89
  return 'trustedPublisher';
83
90
  }
@@ -10,3 +10,4 @@
10
10
  */
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
+ export declare const TARBALL_URL_MISMATCH_VIOLATION_CODE = "TARBALL_URL_MISMATCH";
@@ -10,4 +10,5 @@
10
10
  */
11
11
  export const MINIMUM_RELEASE_AGE_VIOLATION_CODE = 'MINIMUM_RELEASE_AGE_VIOLATION';
12
12
  export const TRUST_DOWNGRADE_VIOLATION_CODE = 'TRUST_DOWNGRADE';
13
+ export const TARBALL_URL_MISMATCH_VIOLATION_CODE = 'TARBALL_URL_MISMATCH';
13
14
  //# sourceMappingURL=violationCodes.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pnpm/resolving.npm-resolver",
3
- "version": "1101.3.3",
3
+ "version": "1101.5.0",
4
4
  "description": "Resolver for npm-hosted packages",
5
5
  "keywords": [
6
6
  "pnpm",
@@ -10,7 +10,10 @@
10
10
  ],
11
11
  "license": "MIT",
12
12
  "funding": "https://opencollective.com/pnpm",
13
- "repository": "https://github.com/pnpm/pnpm/tree/main/resolving/npm-resolver",
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "https://github.com/pnpm/pnpm/tree/main/resolving/npm-resolver"
16
+ },
14
17
  "homepage": "https://github.com/pnpm/pnpm/tree/main/resolving/npm-resolver#readme",
15
18
  "bugs": {
16
19
  "url": "https://github.com/pnpm/pnpm/issues"
@@ -40,27 +43,27 @@
40
43
  "semver-utils": "^1.1.4",
41
44
  "ssri": "13.0.1",
42
45
  "version-selector-type": "^3.0.0",
43
- "@pnpm/config.pick-registry-for-package": "1100.0.6",
44
- "@pnpm/constants": "1100.0.0",
45
- "@pnpm/core-loggers": "1100.1.2",
46
+ "@pnpm/config.pick-registry-for-package": "1100.0.7",
47
+ "@pnpm/config.version-policy": "1100.1.3",
46
48
  "@pnpm/crypto.hash": "1100.0.1",
47
- "@pnpm/config.version-policy": "1100.1.2",
48
- "@pnpm/error": "1100.0.0",
49
- "@pnpm/fetching.types": "1100.0.1",
50
- "@pnpm/resolving.registry.pkg-metadata-filter": "1100.0.5",
51
- "@pnpm/resolving.registry.types": "1100.0.5",
52
- "@pnpm/resolving.resolver-base": "1100.3.1",
53
- "@pnpm/store.cafs": "1100.1.7",
54
- "@pnpm/store.index": "1100.1.0",
55
49
  "@pnpm/fs.graceful-fs": "1100.1.0",
56
- "@pnpm/types": "1101.2.0",
57
- "@pnpm/workspace.range-resolver": "1100.0.1",
58
50
  "@pnpm/resolving.jsr-specifier-parser": "1100.0.0",
51
+ "@pnpm/constants": "1100.0.0",
52
+ "@pnpm/resolving.registry.types": "1100.1.1",
53
+ "@pnpm/resolving.resolver-base": "1100.4.0",
54
+ "@pnpm/store.cafs": "1100.1.8",
55
+ "@pnpm/store.index": "1100.1.0",
56
+ "@pnpm/resolving.registry.pkg-metadata-filter": "1100.0.7",
57
+ "@pnpm/core-loggers": "1100.1.3",
58
+ "@pnpm/types": "1101.3.0",
59
+ "@pnpm/fetching.types": "1100.0.1",
60
+ "@pnpm/workspace.range-resolver": "1100.0.1",
61
+ "@pnpm/error": "1100.0.0",
59
62
  "@pnpm/workspace.spec-parser": "1100.0.0"
60
63
  },
61
64
  "peerDependencies": {
62
65
  "@pnpm/logger": "^1001.0.1",
63
- "@pnpm/worker": "^1100.1.8"
66
+ "@pnpm/worker": "^1100.1.9"
64
67
  },
65
68
  "devDependencies": {
66
69
  "@jest/globals": "30.3.0",
@@ -70,11 +73,11 @@
70
73
  "@types/ssri": "^7.1.5",
71
74
  "load-json-file": "^7.0.1",
72
75
  "tempy": "3.0.0",
73
- "@pnpm/logger": "1100.0.0",
74
- "@pnpm/testing.mock-agent": "1100.0.7",
75
- "@pnpm/resolving.npm-resolver": "1101.3.3",
76
+ "@pnpm/network.fetch": "1100.1.0",
77
+ "@pnpm/resolving.npm-resolver": "1101.5.0",
76
78
  "@pnpm/test-fixtures": "1100.0.0",
77
- "@pnpm/network.fetch": "1100.0.7"
79
+ "@pnpm/testing.mock-agent": "1101.0.0",
80
+ "@pnpm/logger": "1100.0.0"
78
81
  },
79
82
  "engines": {
80
83
  "node": ">=22.13"