@pnpm/resolving.npm-resolver 1101.1.1 → 1101.3.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.
@@ -0,0 +1,33 @@
1
+ import type { PackageMeta } from '@pnpm/resolving.registry.types';
2
+ import { type FetchMetadataFromFromRegistryOptions } from './fetch.js';
3
+ export interface FetchMetadataCachedOptions {
4
+ registry: string;
5
+ authHeaderValue?: string;
6
+ /**
7
+ * pnpm's on-disk cache directory. When set, the call issues a conditional
8
+ * GET against the matching mirror the resolver populates: a 304 Not
9
+ * Modified response serves the body from disk, a 200 writes the new body
10
+ * back. Omit to disable caching — every call re-fetches.
11
+ */
12
+ cacheDir?: string;
13
+ }
14
+ export type FetchFullMetadataCachedOptions = FetchMetadataCachedOptions;
15
+ /**
16
+ * Fetch a full registry metadata document for `pkgName`, reusing pnpm's
17
+ * shared on-disk metadata mirror when `cacheDir` is supplied. Built for the
18
+ * `minimumReleaseAge` lockfile revalidation gate, which needs the `time`
19
+ * field that abbreviated metadata omits; the cache reuse keeps repeat
20
+ * installs from re-downloading the same multi-megabyte document for every
21
+ * locked package.
22
+ */
23
+ export declare function fetchFullMetadataCached(fetchOpts: FetchMetadataFromFromRegistryOptions, pkgName: string, opts: FetchFullMetadataCachedOptions): Promise<PackageMeta>;
24
+ /**
25
+ * Sibling of {@link fetchFullMetadataCached} that hits the abbreviated
26
+ * metadata endpoint (`Accept: application/vnd.npm.install-v1+json`) and
27
+ * caches under `ABBREVIATED_META_DIR` — the same mirror the resolver
28
+ * populates by default. Used by the lockfile verification gate as a
29
+ * cheap upper-bound check: if the package's `modified` field is older
30
+ * than the policy cutoff, every version in it predates the cutoff and
31
+ * no per-version timestamp lookup is needed.
32
+ */
33
+ export declare function fetchAbbreviatedMetadataCached(fetchOpts: FetchMetadataFromFromRegistryOptions, pkgName: string, opts: FetchMetadataCachedOptions): Promise<PackageMeta>;
@@ -0,0 +1,63 @@
1
+ import { ABBREVIATED_META_DIR, FULL_META_DIR } from '@pnpm/constants';
2
+ import { PnpmError } from '@pnpm/error';
3
+ import { fetchMetadataFromFromRegistry } from './fetch.js';
4
+ import { getPkgMirrorPath, loadMeta, loadMetaHeaders, prepareJsonForDisk, saveMeta } from './pickPackage.js';
5
+ /**
6
+ * Fetch a full registry metadata document for `pkgName`, reusing pnpm's
7
+ * shared on-disk metadata mirror when `cacheDir` is supplied. Built for the
8
+ * `minimumReleaseAge` lockfile revalidation gate, which needs the `time`
9
+ * field that abbreviated metadata omits; the cache reuse keeps repeat
10
+ * installs from re-downloading the same multi-megabyte document for every
11
+ * locked package.
12
+ */
13
+ export async function fetchFullMetadataCached(fetchOpts, pkgName, opts) {
14
+ return fetchMetadataCached(fetchOpts, pkgName, { ...opts, fullMetadata: true, metaDir: FULL_META_DIR });
15
+ }
16
+ /**
17
+ * Sibling of {@link fetchFullMetadataCached} that hits the abbreviated
18
+ * metadata endpoint (`Accept: application/vnd.npm.install-v1+json`) and
19
+ * caches under `ABBREVIATED_META_DIR` — the same mirror the resolver
20
+ * populates by default. Used by the lockfile verification gate as a
21
+ * cheap upper-bound check: if the package's `modified` field is older
22
+ * than the policy cutoff, every version in it predates the cutoff and
23
+ * no per-version timestamp lookup is needed.
24
+ */
25
+ export async function fetchAbbreviatedMetadataCached(fetchOpts, pkgName, opts) {
26
+ return fetchMetadataCached(fetchOpts, pkgName, { ...opts, fullMetadata: false, metaDir: ABBREVIATED_META_DIR });
27
+ }
28
+ async function fetchMetadataCached(fetchOpts, pkgName, opts) {
29
+ const pkgMirror = opts.cacheDir != null
30
+ ? getPkgMirrorPath(opts.cacheDir, opts.metaDir, opts.registry, pkgName)
31
+ : null;
32
+ const cacheHeaders = pkgMirror != null ? await loadMetaHeaders(pkgMirror) : null;
33
+ const result = await fetchMetadataFromFromRegistry(fetchOpts, pkgName, {
34
+ registry: opts.registry,
35
+ authHeaderValue: opts.authHeaderValue,
36
+ fullMetadata: opts.fullMetadata,
37
+ etag: cacheHeaders?.etag,
38
+ modified: cacheHeaders?.modified,
39
+ });
40
+ if ('notModified' in result && result.notModified) {
41
+ if (pkgMirror == null) {
42
+ // We didn't send conditional headers (no cacheDir), but the registry
43
+ // returned 304 anyway. There's no body to fall back on.
44
+ throw new PnpmError('META_NOT_MODIFIED_WITHOUT_CACHE', `Registry returned 304 for ${pkgName} without an existing cache to refresh.`);
45
+ }
46
+ const meta = await loadMeta(pkgMirror);
47
+ if (meta == null) {
48
+ // Cache file vanished between header-load and meta-load (concurrent
49
+ // store cleanup, antivirus, etc.).
50
+ throw new PnpmError('META_CACHE_MISSING_AFTER_304', `Metadata cache for ${pkgName} disappeared between headers read and full read.`);
51
+ }
52
+ return meta;
53
+ }
54
+ if (pkgMirror != null) {
55
+ // Persist so the next install can do a headers-only conditional GET.
56
+ // Fire-and-forget — a cache-write failure isn't a reason to fail the
57
+ // caller; the next install just won't get the speedup.
58
+ const json = prepareJsonForDisk(result.meta, result.etag, result.jsonText);
59
+ saveMeta(pkgMirror, json).catch(() => { });
60
+ }
61
+ return result.meta;
62
+ }
63
+ //# sourceMappingURL=fetchFullMetadataCached.js.map
package/lib/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { PnpmError } from '@pnpm/error';
2
2
  import type { FetchFromRegistry, GetAuthHeader, RetryTimeoutOptions } from '@pnpm/fetching.types';
3
3
  import type { PackageMeta } from '@pnpm/resolving.registry.types';
4
- import type { DirectoryResolution, PkgResolutionId, PreferredVersions, ResolveResult, TarballResolution, WantedDependency, WorkspacePackages } from '@pnpm/resolving.resolver-base';
4
+ import type { DirectoryResolution, LatestInfo, LatestQuery, PkgResolutionId, PreferredVersions, ResolveOptions, ResolveResult, TarballResolution, WantedDependency, WorkspacePackages } from '@pnpm/resolving.resolver-base';
5
5
  import type { DependencyManifest, PackageVersionPolicy, PinnedVersion, Registries, TrustPolicy } from '@pnpm/types';
6
6
  import { fetchMetadataFromFromRegistry, type FetchMetadataFromFromRegistryOptions, RegistryResponseError } from './fetch.js';
7
7
  import { BUILTIN_NAMED_REGISTRIES, parseBareSpecifier, type RegistryPackageSpec } from './parseBareSpecifier.js';
@@ -12,16 +12,15 @@ export interface NoMatchingVersionErrorOptions {
12
12
  wantedDependency: WantedDependency;
13
13
  packageMeta: PackageMeta;
14
14
  registry: string;
15
- immatureVersion?: string;
16
- publishedBy?: Date;
17
15
  }
18
16
  export declare class NoMatchingVersionError extends PnpmError {
19
17
  readonly packageMeta: PackageMeta;
20
- readonly immatureVersion?: string;
21
18
  constructor(opts: NoMatchingVersionErrorOptions);
22
19
  }
23
20
  export declare function formatTimeAgo(date: Date): string | null;
24
21
  export { BUILTIN_NAMED_REGISTRIES, fetchMetadataFromFromRegistry, type FetchMetadataFromFromRegistryOptions, type PackageMeta, type PackageMetaCache, parseBareSpecifier, pickPackageFromMeta, pickVersionByVersionRange, type RegistryPackageSpec, RegistryResponseError, workspacePrefToNpm, };
22
+ export { createNpmResolutionVerifier, type CreateNpmResolutionVerifierOptions } from './createNpmResolutionVerifier.js';
23
+ export { MINIMUM_RELEASE_AGE_VIOLATION_CODE, TRUST_DOWNGRADE_VIOLATION_CODE, } from './violationCodes.js';
25
24
  export { whichVersionIsPinned } from './whichVersionIsPinned.js';
26
25
  export interface ResolverFactoryOptions {
27
26
  cacheDir: string;
@@ -36,7 +35,6 @@ export interface ResolverFactoryOptions {
36
35
  namedRegistries?: Record<string, string>;
37
36
  saveWorkspaceProtocol?: boolean | 'rolling';
38
37
  preserveAbsolutePaths?: boolean;
39
- strictPublishedByCheck?: boolean;
40
38
  ignoreMissingTimeField?: boolean;
41
39
  fetchWarnTimeoutMs?: number;
42
40
  /** Pre-populated metadata cache. When provided, the resolver uses this
@@ -72,10 +70,14 @@ export interface WorkspaceResolveResult extends ResolveResult {
72
70
  export type NpmResolver = (wantedDependency: WantedDependency & {
73
71
  optional?: boolean;
74
72
  }, opts: ResolveFromNpmOptions) => Promise<NpmResolveResult | JsrResolveResult | NamedRegistryResolveResult | WorkspaceResolveResult | null>;
73
+ export type ResolveLatestFromNpmStyle = (query: LatestQuery, opts: ResolveOptions) => Promise<LatestInfo | undefined>;
75
74
  export declare function createNpmResolver(fetchFromRegistry: FetchFromRegistry, getAuthHeader: GetAuthHeader, opts: ResolverFactoryOptions): {
76
75
  resolveFromNpm: NpmResolver;
77
76
  resolveFromJsr: NpmResolver;
78
77
  resolveFromNamedRegistry: NpmResolver;
78
+ resolveLatestFromNpm: ResolveLatestFromNpmStyle;
79
+ resolveLatestFromJsr: ResolveLatestFromNpmStyle;
80
+ resolveLatestFromNamedRegistry: ResolveLatestFromNpmStyle;
79
81
  clearCache: () => void;
80
82
  };
81
83
  export interface ResolveFromNpmContext {
package/lib/index.js CHANGED
@@ -17,28 +17,17 @@ import { BUILTIN_NAMED_REGISTRIES, parseBareSpecifier, parseJsrSpecifierToRegist
17
17
  import { pickPackage, } from './pickPackage.js';
18
18
  import { pickPackageFromMeta, pickVersionByVersionRange } from './pickPackageFromMeta.js';
19
19
  import { failIfTrustDowngraded } from './trustChecks.js';
20
+ import { MINIMUM_RELEASE_AGE_VIOLATION_CODE } from './violationCodes.js';
20
21
  import { whichVersionIsPinned } from './whichVersionIsPinned.js';
21
22
  import { workspacePrefToNpm } from './workspacePrefToNpm.js';
22
23
  export class NoMatchingVersionError extends PnpmError {
23
24
  packageMeta;
24
- immatureVersion;
25
25
  constructor(opts) {
26
26
  const dep = opts.wantedDependency.alias
27
27
  ? `${opts.wantedDependency.alias}@${opts.wantedDependency.bareSpecifier ?? ''}`
28
28
  : opts.wantedDependency.bareSpecifier;
29
- let errorMessage;
30
- if (opts.publishedBy && opts.immatureVersion && opts.packageMeta.time) {
31
- const time = new Date(opts.packageMeta.time[opts.immatureVersion]);
32
- const releaseAgeText = formatTimeAgo(time) ?? 'just now';
33
- const pkgName = opts.wantedDependency.alias ?? opts.packageMeta.name;
34
- errorMessage = `Version ${opts.immatureVersion} (released ${releaseAgeText}) of ${pkgName} does not meet the minimumReleaseAge constraint`;
35
- }
36
- else {
37
- errorMessage = `No matching version found for ${dep} while fetching it from ${opts.registry}`;
38
- }
39
- super(opts.publishedBy ? 'NO_MATURE_MATCHING_VERSION' : 'NO_MATCHING_VERSION', errorMessage);
29
+ super('NO_MATCHING_VERSION', `No matching version found for ${dep} while fetching it from ${opts.registry}`);
40
30
  this.packageMeta = opts.packageMeta;
41
- this.immatureVersion = opts.immatureVersion;
42
31
  }
43
32
  }
44
33
  export function formatTimeAgo(date) {
@@ -71,6 +60,8 @@ export function formatTimeAgo(date) {
71
60
  return 'a few seconds ago';
72
61
  }
73
62
  export { BUILTIN_NAMED_REGISTRIES, fetchMetadataFromFromRegistry, parseBareSpecifier, pickPackageFromMeta, pickVersionByVersionRange, RegistryResponseError, workspacePrefToNpm, };
63
+ export { createNpmResolutionVerifier } from './createNpmResolutionVerifier.js';
64
+ export { MINIMUM_RELEASE_AGE_VIOLATION_CODE, TRUST_DOWNGRADE_VIOLATION_CODE, } from './violationCodes.js';
74
65
  export { whichVersionIsPinned } from './whichVersionIsPinned.js';
75
66
  export function createNpmResolver(fetchFromRegistry, getAuthHeader, opts) {
76
67
  if (typeof opts.cacheDir !== 'string') {
@@ -126,7 +117,6 @@ export function createNpmResolver(fetchFromRegistry, getAuthHeader, opts) {
126
117
  offline: opts.offline,
127
118
  preferOffline: opts.preferOffline,
128
119
  cacheDir: opts.cacheDir,
129
- strictPublishedByCheck: opts.strictPublishedByCheck,
130
120
  ignoreMissingTimeField: opts.ignoreMissingTimeField,
131
121
  }),
132
122
  registries: opts.registries,
@@ -135,10 +125,17 @@ export function createNpmResolver(fetchFromRegistry, getAuthHeader, opts) {
135
125
  saveWorkspaceProtocol: opts.saveWorkspaceProtocol,
136
126
  peekManifestFromStore,
137
127
  };
128
+ const boundResolveFromNpm = resolveNpm.bind(null, ctx);
129
+ const boundResolveFromJsr = resolveJsr.bind(null, ctx);
130
+ const boundResolveFromNamedRegistry = resolveFromNamedRegistry.bind(null, ctx);
131
+ const defaultRegistry = opts.registries.default;
138
132
  return {
139
- resolveFromNpm: resolveNpm.bind(null, ctx),
140
- resolveFromJsr: resolveJsr.bind(null, ctx),
141
- resolveFromNamedRegistry: resolveFromNamedRegistry.bind(null, ctx),
133
+ resolveFromNpm: boundResolveFromNpm,
134
+ resolveFromJsr: boundResolveFromJsr,
135
+ resolveFromNamedRegistry: boundResolveFromNamedRegistry,
136
+ resolveLatestFromNpm: createResolveLatest(boundResolveFromNpm, (query) => isNpmSpec(query, defaultRegistry)),
137
+ resolveLatestFromJsr: createResolveLatest(boundResolveFromJsr, isJsrSpec),
138
+ resolveLatestFromNamedRegistry: createResolveLatest(boundResolveFromNamedRegistry, (query) => isNamedRegistrySpec(query, ctx.namedRegistryNames)),
142
139
  clearCache: () => {
143
140
  if ('clear' in metaCache && typeof metaCache.clear === 'function') {
144
141
  metaCache.clear();
@@ -147,6 +144,54 @@ export function createNpmResolver(fetchFromRegistry, getAuthHeader, opts) {
147
144
  },
148
145
  };
149
146
  }
147
+ function isNpmSpec(query, defaultRegistry) {
148
+ const { alias, bareSpecifier } = query.wantedDependency;
149
+ if (!bareSpecifier)
150
+ return alias != null;
151
+ return parseBareSpecifier(bareSpecifier, alias, 'latest', defaultRegistry) != null;
152
+ }
153
+ function isJsrSpec(query) {
154
+ if (!query.wantedDependency.bareSpecifier?.startsWith('jsr:'))
155
+ return false;
156
+ return parseJsrSpecifierToRegistryPackageSpec(query.wantedDependency.bareSpecifier, query.wantedDependency.alias, 'latest') != null;
157
+ }
158
+ function isNamedRegistrySpec(query, knownRegistryNames) {
159
+ if (!query.wantedDependency.bareSpecifier)
160
+ return false;
161
+ try {
162
+ return parseNamedRegistrySpecifierToRegistryPackageSpec(query.wantedDependency.bareSpecifier, knownRegistryNames, query.wantedDependency.alias, 'latest') != null;
163
+ }
164
+ catch {
165
+ return false;
166
+ }
167
+ }
168
+ function createResolveLatest(resolve, matches) {
169
+ return async (query, opts) => {
170
+ if (!matches(query))
171
+ return undefined;
172
+ // Always pass the manifest's bareSpecifier so protocol-prefixed specs
173
+ // (`jsr:@scope/pkg@^1.0.0`, `gh:owner/repo@^1.0.0`) still match their
174
+ // resolver. In --compatible mode that range drives the pick; otherwise
175
+ // `update: 'latest'` tells the resolver to ignore the range and take
176
+ // the absolute newest.
177
+ const bareSpecifier = query.wantedDependency.bareSpecifier ?? 'latest';
178
+ const resolveOpts = query.compatible ? opts : { ...opts, update: 'latest' };
179
+ try {
180
+ const result = await resolve({ alias: query.wantedDependency.alias, bareSpecifier }, resolveOpts);
181
+ // Policy-blocked: handled but no latest to surface.
182
+ if (result?.policyViolation?.code === MINIMUM_RELEASE_AGE_VIOLATION_CODE) {
183
+ return {};
184
+ }
185
+ return { latestManifest: result?.manifest };
186
+ }
187
+ catch (err) {
188
+ if (opts.publishedBy && err.code === 'ERR_PNPM_NO_MATCHING_VERSION') {
189
+ return {};
190
+ }
191
+ throw err;
192
+ }
193
+ };
194
+ }
150
195
  async function resolveNpm(ctx, wantedDependency, opts) {
151
196
  const defaultTag = opts.defaultTag ?? 'latest';
152
197
  const registry = wantedDependency.alias
@@ -207,6 +252,19 @@ async function resolveNpm(ctx, wantedDependency, opts) {
207
252
  resolution: currentResolution,
208
253
  resolvedVia: 'npm-registry',
209
254
  publishedAt: opts.currentPkg.publishedAt,
255
+ // Loose-mode bypass: a lockfile entry whose publishedAt sits
256
+ // after the maturity cutoff would have been rejected at
257
+ // resolver time, but the peek path skips the maturity check.
258
+ // Report inline so the deps-resolver aggregator surfaces it
259
+ // to the install command.
260
+ policyViolation: detectMinReleaseAgeViolation({
261
+ name: manifest.name,
262
+ version: manifest.version,
263
+ publishedAt: opts.currentPkg.publishedAt,
264
+ resolution: currentResolution,
265
+ publishedBy: opts.publishedBy,
266
+ publishedByExclude: opts.publishedByExclude,
267
+ }),
210
268
  };
211
269
  }
212
270
  }
@@ -267,22 +325,6 @@ async function resolveNpm(ctx, wantedDependency, opts) {
267
325
  // ignore
268
326
  }
269
327
  }
270
- if (opts.publishedBy) {
271
- const immatureVersion = pickVersionByVersionRange({
272
- meta,
273
- versionRange: spec.fetchSpec,
274
- preferredVersionSelectors: opts.preferredVersions?.[spec.name],
275
- });
276
- if (immatureVersion) {
277
- throw new NoMatchingVersionError({
278
- wantedDependency,
279
- packageMeta: meta,
280
- registry,
281
- immatureVersion,
282
- publishedBy: opts.publishedBy,
283
- });
284
- }
285
- }
286
328
  throw new NoMatchingVersionError({ wantedDependency, packageMeta: meta, registry });
287
329
  }
288
330
  else if (opts.trustPolicy === 'no-downgrade') {
@@ -335,14 +377,23 @@ async function resolveNpm(ctx, wantedDependency, opts) {
335
377
  defaultPinnedVersion: opts.pinnedVersion,
336
378
  });
337
379
  }
380
+ const publishedAt = meta.time?.[pickedPackage.version];
338
381
  return {
339
382
  id,
340
383
  latest: meta['dist-tags'].latest,
341
384
  manifest: pickedPackage,
342
385
  resolution,
343
386
  resolvedVia: 'npm-registry',
344
- publishedAt: meta.time?.[pickedPackage.version],
387
+ publishedAt,
345
388
  normalizedBareSpecifier,
389
+ policyViolation: detectMinReleaseAgeViolation({
390
+ name: pickedPackage.name,
391
+ version: pickedPackage.version,
392
+ publishedAt,
393
+ resolution,
394
+ publishedBy: opts.publishedBy,
395
+ publishedByExclude: opts.publishedByExclude,
396
+ }),
346
397
  };
347
398
  }
348
399
  async function resolveJsr(ctx, wantedDependency, opts) {
@@ -439,15 +490,25 @@ async function pickFromSimpleRegistry(ctx, wantedDependency, opts, spec, registr
439
490
  if (pickedPackage == null) {
440
491
  throw new NoMatchingVersionError({ wantedDependency, packageMeta: meta, registry });
441
492
  }
493
+ const resolution = {
494
+ integrity: getIntegrity(pickedPackage.dist),
495
+ tarball: normalizeRegistryUrl(pickedPackage.dist.tarball),
496
+ };
497
+ const publishedAt = meta.time?.[pickedPackage.version];
442
498
  return {
443
499
  id: `${pickedPackage.name}@${pickedPackage.version}`,
444
500
  latest: meta['dist-tags'].latest,
445
501
  manifest: pickedPackage,
446
- resolution: {
447
- integrity: getIntegrity(pickedPackage.dist),
448
- tarball: normalizeRegistryUrl(pickedPackage.dist.tarball),
449
- },
450
- publishedAt: meta.time?.[pickedPackage.version],
502
+ resolution,
503
+ publishedAt,
504
+ policyViolation: detectMinReleaseAgeViolation({
505
+ name: pickedPackage.name,
506
+ version: pickedPackage.version,
507
+ publishedAt,
508
+ resolution,
509
+ publishedBy: opts.publishedBy,
510
+ publishedByExclude: opts.publishedByExclude,
511
+ }),
451
512
  };
452
513
  }
453
514
  // Builds a `<prefix><pkgName>@<range>` specifier (or a bare `<prefix><range>`
@@ -609,6 +670,40 @@ function defaultTagForAlias(alias, defaultTag) {
609
670
  type: 'tag',
610
671
  };
611
672
  }
673
+ /**
674
+ * Inline minimumReleaseAge detection: returns a violation entry when the
675
+ * picked version's publish timestamp is past the policy cutoff (and
676
+ * isn't covered by `publishedByExclude`). The resolver already has the
677
+ * timestamp in hand, so reporting inline saves the install layer from
678
+ * re-walking the resolved tree and re-fetching the same metadata. The
679
+ * deps-resolver aggregates the per-resolve `policyViolation` fields into
680
+ * a single set the install command reacts to.
681
+ *
682
+ * Returns `undefined` for resolutions outside the policy — no policy
683
+ * active, version excluded by pattern, timestamp missing or malformed,
684
+ * or version mature. Specific-version exclusions (`pkg@1.0.0`) and
685
+ * full-name exclusions (`pkg`) are both honored so an entry already on
686
+ * the user's exclude list isn't re-announced every install.
687
+ */
688
+ function detectMinReleaseAgeViolation(args) {
689
+ if (!args.publishedBy || !args.publishedAt)
690
+ return undefined;
691
+ const excludeResult = args.publishedByExclude?.(args.name);
692
+ if (excludeResult === true)
693
+ return undefined;
694
+ if (Array.isArray(excludeResult) && excludeResult.includes(args.version))
695
+ return undefined;
696
+ const ts = new Date(args.publishedAt).getTime();
697
+ if (Number.isNaN(ts) || ts <= args.publishedBy.getTime())
698
+ return undefined;
699
+ return {
700
+ name: args.name,
701
+ version: args.version,
702
+ resolution: args.resolution,
703
+ code: MINIMUM_RELEASE_AGE_VIOLATION_CODE,
704
+ reason: `was published at ${new Date(ts).toISOString()}, within the minimumReleaseAge cutoff (${args.publishedBy.toISOString()})`,
705
+ };
706
+ }
612
707
  function getIntegrity(dist) {
613
708
  if (dist.integrity) {
614
709
  return dist.integrity;
@@ -29,9 +29,40 @@ export declare function pickPackage(ctx: {
29
29
  offline?: boolean;
30
30
  preferOffline?: boolean;
31
31
  filterMetadata?: boolean;
32
- strictPublishedByCheck?: boolean;
33
32
  ignoreMissingTimeField?: boolean;
34
33
  }, spec: RegistryPackageSpec, opts: PickPackageOptions): Promise<{
35
34
  meta: PackageMeta;
36
35
  pickedPackage: PackageInRegistry | null;
37
36
  }>;
37
+ export declare function encodePkgName(pkgName: string): string;
38
+ /**
39
+ * Path of the on-disk JSONL document where pnpm mirrors a package's registry
40
+ * metadata. `metaDir` selects between abbreviated and full caches.
41
+ */
42
+ export declare function getPkgMirrorPath(cacheDir: string, metaDir: string, registry: string, pkgName: string): string;
43
+ /**
44
+ * Formats metadata for disk storage as two-line NDJSON:
45
+ * Line 1: cache headers (etag, modified) — small, fast to read
46
+ * Line 2: the full registry metadata JSON — unchanged from the registry response
47
+ */
48
+ export declare function prepareJsonForDisk(meta: PackageMeta, etag: string | undefined, jsonText?: string): string;
49
+ export declare function warnMissingTimeFieldOnce(pkgName: string): void;
50
+ interface MetaHeaders {
51
+ etag?: string;
52
+ modified?: string;
53
+ }
54
+ /**
55
+ * Reads only the first line of the cached NDJSON metadata file to extract
56
+ * the cache headers (etag, modified). This avoids reading and
57
+ * parsing the full metadata (which can be megabytes for popular packages)
58
+ * when we only need conditional-request headers.
59
+ */
60
+ export declare function loadMetaHeaders(pkgMirror: string): Promise<MetaHeaders | null>;
61
+ /**
62
+ * Reads the full metadata from the cached NDJSON file.
63
+ * Line 1: cache headers (etag, modified)
64
+ * Line 2: registry metadata JSON
65
+ */
66
+ export declare function loadMeta(pkgMirror: string): Promise<PackageMeta | null>;
67
+ export declare function saveMeta(pkgMirror: string, json: string): Promise<void>;
68
+ export {};
@@ -58,12 +58,13 @@ function pickMax(a, b) {
58
58
  const pickHighest = pickPackageFromMeta.bind(null, pickVersionByVersionRange);
59
59
  const pickLowest = pickPackageFromMeta.bind(null, pickLowestVersionByVersionRange);
60
60
  // When minimumReleaseAge is active: try the highest mature version; if none
61
- // and strictPublishedByCheck is off, fall back to the lowest version in range
62
- // without applying the maturity filter.
61
+ // satisfies the range, fall back to the lowest version regardless of maturity
62
+ // so the resolver can report the violation inline and let the install layer
63
+ // (or other caller) decide what to do — never throw at this layer.
63
64
  function pickRespectingMinReleaseAge(pickerOpts, spec, meta) {
64
65
  return runPicker(pickerOpts, spec, (targetSpec) => {
65
66
  const highest = pickHighest(pickerOpts, meta, targetSpec);
66
- if (highest || pickerOpts.strictPublishedByCheck)
67
+ if (highest)
67
68
  return highest;
68
69
  return pickLowest({
69
70
  preferredVersionSelectors: pickerOpts.preferredVersionSelectors,
@@ -111,7 +112,6 @@ export async function pickPackage(ctx, spec, opts) {
111
112
  publishedByExclude: opts.publishedByExclude,
112
113
  pickLowestVersion: opts.pickLowestVersion,
113
114
  includeLatestTag: opts.includeLatestTag,
114
- strictPublishedByCheck: ctx.strictPublishedByCheck,
115
115
  ignoreMissingTimeField: ctx.ignoreMissingTimeField,
116
116
  };
117
117
  validatePackageName(spec.name);
@@ -123,8 +123,7 @@ export async function pickPackage(ctx, spec, opts) {
123
123
  : ABBREVIATED_META_DIR;
124
124
  // Cache key includes fullMetadata to avoid returning abbreviated metadata when full metadata is requested.
125
125
  const cacheKey = fullMetadata ? `${spec.name}:full` : spec.name;
126
- const registryName = getRegistryName(opts.registry);
127
- const pkgMirror = path.join(ctx.cacheDir, metaDir, registryName, `${encodePkgName(spec.name)}.jsonl`);
126
+ const pkgMirror = getPkgMirrorPath(ctx.cacheDir, metaDir, opts.registry, spec.name);
128
127
  const cachedMeta = ctx.metaCache.get(cacheKey);
129
128
  if (cachedMeta != null) {
130
129
  // The in-memory cache may hold abbreviated metadata from an earlier call
@@ -194,10 +193,11 @@ export async function pickPackage(ctx, spec, opts) {
194
193
  };
195
194
  }
196
195
  }
197
- catch (err) {
198
- if (shouldRethrowFromFastPathCache(err, ctx.strictPublishedByCheck)) {
199
- throw err;
200
- }
196
+ catch {
197
+ // Swallow fast-path errors (e.g. ERR_PNPM_MISSING_TIME from
198
+ // abbreviated meta) and fall through to the network fetch, which
199
+ // can upgrade to full metadata and run the maturity check on
200
+ // real `time` data.
201
201
  }
202
202
  }
203
203
  }
@@ -215,10 +215,8 @@ export async function pickPackage(ctx, spec, opts) {
215
215
  };
216
216
  }
217
217
  }
218
- catch (err) {
219
- if (shouldRethrowFromFastPathCache(err, ctx.strictPublishedByCheck)) {
220
- throw err;
221
- }
218
+ catch {
219
+ // Same as above — fall through to the network fetch.
222
220
  }
223
221
  }
224
222
  }
@@ -279,7 +277,12 @@ export async function pickPackage(ctx, spec, opts) {
279
277
  opts.publishedByExclude?.(spec.name) !== true) {
280
278
  const modifiedDate = meta.modified ? new Date(meta.modified) : null;
281
279
  const isModifiedValid = modifiedDate != null && !Number.isNaN(modifiedDate.getTime());
282
- if (!isModifiedValid || modifiedDate >= opts.publishedBy) {
280
+ // Strict `>` (not `>=`) so the boundary case `modified == publishedBy`
281
+ // takes the abbreviated fast path: `modified` is an upper bound on
282
+ // every version's publish time, so when it equals the cutoff every
283
+ // version passes the per-version `<=` filter in
284
+ // `filterPkgMetadataByPublishDate` and a full re-fetch isn't needed.
285
+ if (!isModifiedValid || modifiedDate > opts.publishedBy) {
283
286
  // Save the abbreviated metadata to the abbreviated cache before re-fetching full.
284
287
  if (!opts.dryRun) {
285
288
  const abbreviatedJson = prepareJsonForDisk(fetchResult.meta, fetchResult.etag, fetchResult.jsonText);
@@ -363,9 +366,12 @@ async function maybeUpgradeAbbreviatedMetaForReleaseAge(ctx, spec, opts, meta) {
363
366
  }
364
367
  const modifiedDate = meta.modified ? new Date(meta.modified) : null;
365
368
  const isModifiedValid = modifiedDate != null && !Number.isNaN(modifiedDate.getTime());
366
- if (isModifiedValid && modifiedDate < opts.publishedBy) {
367
- // The package was last modified before the maturity cutoff. No individual
368
- // version can be newer than the cutoff, so the abbreviated form is fine.
369
+ if (isModifiedValid && modifiedDate <= opts.publishedBy) {
370
+ // The package was last modified at or before the maturity cutoff. Since
371
+ // `modified` is an upper bound on every version's publish time, no version
372
+ // can be newer than the cutoff, so the abbreviated form is fine.
373
+ // Inclusive at the boundary on purpose: matches the per-version `<=` filter
374
+ // in `filterPkgMetadataByPublishDate`.
369
375
  return { meta };
370
376
  }
371
377
  // When `modified` is missing or malformed we fall through to the upgrade
@@ -388,13 +394,6 @@ async function maybeUpgradeAbbreviatedMetaForReleaseAge(ctx, spec, opts, meta) {
388
394
  }
389
395
  return { meta: fullFetchResult.meta, upgradedFrom: fullFetchResult };
390
396
  }
391
- // Returns true when a fast-path cache catch should rethrow under
392
- // strictPublishedByCheck. ERR_PNPM_MISSING_TIME is excluded so callers fall
393
- // through to the network fetch path, which can upgrade abbreviated cached
394
- // metadata to full and run the maturity check on real `time` data.
395
- function shouldRethrowFromFastPathCache(err, strictPublishedByCheck) {
396
- return strictPublishedByCheck === true && !isMissingTimeError(err);
397
- }
398
397
  // Persists upgraded full metadata to the on-disk cache mirror and returns
399
398
  // the meta to store in the in-memory cache. When `filterMetadata` is on, the
400
399
  // in-memory and on-disk forms are both stripped via `clearMeta`; otherwise
@@ -449,18 +448,25 @@ function clearMeta(pkg) {
449
448
  modified: pkg.modified,
450
449
  };
451
450
  }
452
- function encodePkgName(pkgName) {
451
+ export function encodePkgName(pkgName) {
453
452
  if (pkgName !== pkgName.toLowerCase()) {
454
453
  return `${pkgName}_${createHexHash(pkgName)}`;
455
454
  }
456
455
  return pkgName;
457
456
  }
457
+ /**
458
+ * Path of the on-disk JSONL document where pnpm mirrors a package's registry
459
+ * metadata. `metaDir` selects between abbreviated and full caches.
460
+ */
461
+ export function getPkgMirrorPath(cacheDir, metaDir, registry, pkgName) {
462
+ return path.join(cacheDir, metaDir, getRegistryName(registry), `${encodePkgName(pkgName)}.jsonl`);
463
+ }
458
464
  /**
459
465
  * Formats metadata for disk storage as two-line NDJSON:
460
466
  * Line 1: cache headers (etag, modified) — small, fast to read
461
467
  * Line 2: the full registry metadata JSON — unchanged from the registry response
462
468
  */
463
- function prepareJsonForDisk(meta, etag, jsonText) {
469
+ export function prepareJsonForDisk(meta, etag, jsonText) {
464
470
  const modified = meta.modified ?? meta.time?.modified;
465
471
  const headers = JSON.stringify({ etag, modified });
466
472
  const body = jsonText ?? JSON.stringify(meta);
@@ -476,7 +482,7 @@ function isMissingTimeError(err) {
476
482
  // memory via this Set as they resolve ever more distinct packages.
477
483
  const MAX_WARNED_MISSING_TIME = 1024;
478
484
  const warnedMissingTimeFor = new Set();
479
- function warnMissingTimeFieldOnce(pkgName) {
485
+ export function warnMissingTimeFieldOnce(pkgName) {
480
486
  if (warnedMissingTimeFor.has(pkgName))
481
487
  return;
482
488
  if (warnedMissingTimeFor.size >= MAX_WARNED_MISSING_TIME) {
@@ -503,7 +509,7 @@ async function getFileMtime(filePath) {
503
509
  * parsing the full metadata (which can be megabytes for popular packages)
504
510
  * when we only need conditional-request headers.
505
511
  */
506
- async function loadMetaHeaders(pkgMirror) {
512
+ export async function loadMetaHeaders(pkgMirror) {
507
513
  let fh;
508
514
  try {
509
515
  fh = await fs.open(pkgMirror, 'r');
@@ -530,7 +536,7 @@ async function loadMetaHeaders(pkgMirror) {
530
536
  * Line 1: cache headers (etag, modified)
531
537
  * Line 2: registry metadata JSON
532
538
  */
533
- async function loadMeta(pkgMirror) {
539
+ export async function loadMeta(pkgMirror) {
534
540
  try {
535
541
  const data = await gfs.readFile(pkgMirror, 'utf8');
536
542
  const newlineIdx = data.indexOf('\n');
@@ -546,7 +552,7 @@ async function loadMeta(pkgMirror) {
546
552
  }
547
553
  }
548
554
  const createdDirs = new Set();
549
- async function saveMeta(pkgMirror, json) {
555
+ export async function saveMeta(pkgMirror, json) {
550
556
  const dir = path.dirname(pkgMirror);
551
557
  if (!createdDirs.has(dir)) {
552
558
  await fs.mkdir(dir, { recursive: true });
@@ -14,13 +14,17 @@ export function pickPackageFromMeta(pickVersionByVersionRangeFn, { preferredVers
14
14
  }
15
15
  else {
16
16
  const modifiedDate = parseModifiedDate(meta.modified);
17
- if (modifiedDate == null || modifiedDate >= publishedBy) {
17
+ if (modifiedDate == null || modifiedDate > publishedBy) {
18
18
  // Abbreviated metadata without per-version timestamps, and the package
19
19
  // was recently modified (or has no/invalid modified field). We cannot determine
20
20
  // which individual versions are mature enough — need full metadata.
21
21
  assertMetaHasTime(meta);
22
22
  }
23
- // else: meta.modified < publishedBy — all versions are old enough, no filtering needed
23
+ // else: meta.modified <= publishedBy — every version was published at or
24
+ // before the cutoff (modified is an upper bound on per-version time), so
25
+ // they all pass the per-version `<=` maturity filter and no filtering is
26
+ // needed. Inclusive at the boundary on purpose so this branch matches the
27
+ // per-version filter in `filterPkgMetadataByPublishDate`.
24
28
  }
25
29
  }
26
30
  }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Violation codes the npm resolver attaches to
3
+ * `ResolutionPolicyViolation.code` when an inline policy check rejects
4
+ * a pick. Exported so downstream code (the install command, the strict
5
+ * resolver wrapper, tests) references one source of truth instead of
6
+ * re-typing the string.
7
+ *
8
+ * Lives in its own module — both `index.ts` and `createNpmResolutionVerifier.ts`
9
+ * import it, so keeping the constants here avoids a cycle.
10
+ */
11
+ export declare const MINIMUM_RELEASE_AGE_VIOLATION_CODE = "MINIMUM_RELEASE_AGE_VIOLATION";
12
+ export declare const TRUST_DOWNGRADE_VIOLATION_CODE = "TRUST_DOWNGRADE";