@pnpm/resolving.npm-resolver 1101.1.0 → 1101.2.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,99 @@
1
+ import * as retry from '@zkochan/retry';
2
+ export async function fetchAttestationPublishedAt(fetchOpts, pkgName, version, opts) {
3
+ const url = `${opts.registry.replace(/\/$/, '')}/-/npm/v1/attestations/${pkgName}@${version}`;
4
+ const retryOperation = retry.operation(fetchOpts.retry);
5
+ return new Promise((resolve) => {
6
+ retryOperation.attempt(async () => {
7
+ let response;
8
+ try {
9
+ response = await fetchOpts.fetch(url, {
10
+ authHeaderValue: opts.authHeaderValue,
11
+ retry: fetchOpts.retry,
12
+ timeout: fetchOpts.timeout,
13
+ });
14
+ }
15
+ catch {
16
+ // Network errors fall through to the full-metadata path; the
17
+ // caller's `fetchFullMetadataCached` has its own retry policy.
18
+ resolve(undefined);
19
+ return;
20
+ }
21
+ // 404 = package never published attestations. Other 4xx/5xx also
22
+ // mean "can't get an answer from this endpoint, fall back."
23
+ if (response.status >= 400) {
24
+ resolve(undefined);
25
+ return;
26
+ }
27
+ let body;
28
+ try {
29
+ body = await response.json();
30
+ }
31
+ catch {
32
+ resolve(undefined);
33
+ return;
34
+ }
35
+ resolve(extractPublishedAt(body));
36
+ });
37
+ });
38
+ }
39
+ /**
40
+ * Pull the earliest `integratedTime` across every attestation bundle in
41
+ * the response and convert it to an ISO timestamp. Earliest is the
42
+ * conservative choice: if two attestations disagree (e.g. publish
43
+ * v0.1 vs SLSA provenance v1), we attribute the publish to the older
44
+ * Rekor entry. The Rekor timestamp is what tells us when the artifact
45
+ * existed in a transparency log — that's the floor on publish time.
46
+ */
47
+ function extractPublishedAt(body) {
48
+ if (!body || typeof body !== 'object')
49
+ return undefined;
50
+ const attestations = body.attestations;
51
+ if (!Array.isArray(attestations))
52
+ return undefined;
53
+ let earliestSeconds;
54
+ for (const attestation of attestations) {
55
+ const seconds = readEarliestIntegratedTime(attestation);
56
+ if (seconds == null)
57
+ continue;
58
+ if (earliestSeconds == null || seconds < earliestSeconds) {
59
+ earliestSeconds = seconds;
60
+ }
61
+ }
62
+ if (earliestSeconds == null)
63
+ return undefined;
64
+ return new Date(earliestSeconds * 1000).toISOString();
65
+ }
66
+ function readEarliestIntegratedTime(attestation) {
67
+ if (!attestation || typeof attestation !== 'object')
68
+ return undefined;
69
+ const bundle = attestation.bundle;
70
+ if (!bundle || typeof bundle !== 'object')
71
+ return undefined;
72
+ const verificationMaterial = bundle.verificationMaterial;
73
+ if (!verificationMaterial || typeof verificationMaterial !== 'object')
74
+ return undefined;
75
+ const tlogEntries = verificationMaterial.tlogEntries;
76
+ if (!Array.isArray(tlogEntries))
77
+ return undefined;
78
+ let earliest;
79
+ for (const entry of tlogEntries) {
80
+ if (!entry || typeof entry !== 'object')
81
+ continue;
82
+ const rawIntegratedTime = entry.integratedTime;
83
+ // npm serializes integratedTime as a string ("1778583836") to avoid
84
+ // JSON precision loss; accept either string or number defensively.
85
+ const seconds = parseIntegratedTimeSeconds(rawIntegratedTime);
86
+ if (seconds == null)
87
+ continue;
88
+ if (earliest == null || seconds < earliest)
89
+ earliest = seconds;
90
+ }
91
+ return earliest;
92
+ }
93
+ function parseIntegratedTimeSeconds(raw) {
94
+ const seconds = typeof raw === 'string' ? Number(raw) : typeof raw === 'number' ? raw : NaN;
95
+ if (!Number.isFinite(seconds) || seconds <= 0)
96
+ return undefined;
97
+ return seconds;
98
+ }
99
+ //# sourceMappingURL=fetchAttestationPublishedAt.js.map
@@ -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
@@ -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
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,
@@ -180,7 +170,13 @@ async function resolveNpm(ctx, wantedDependency, opts) {
180
170
  // Fast path: if we have a current resolution with integrity, try to peek the manifest from the store.
181
171
  // This avoids the expensive metadata fetch from the registry.
182
172
  // We do this AFTER ensuring the spec is valid for this resolver to avoids hijacking other resolvers.
183
- if (ctx.peekManifestFromStore && opts.currentPkg?.resolution && !opts.update) {
173
+ // If publishedBy is set (resolutionMode=time-based or minimumReleaseAge is configured), we only take
174
+ // the fast path when publishedAt is already known from the lockfile's `time:` block; otherwise we
175
+ // fall through to a registry fetch so the cutoff isn't computed from missing data.
176
+ if (ctx.peekManifestFromStore &&
177
+ opts.currentPkg?.resolution &&
178
+ !opts.update &&
179
+ (opts.publishedBy == null || opts.currentPkg.publishedAt != null)) {
184
180
  const currentResolution = opts.currentPkg.resolution;
185
181
  // Only use this optimization for tarball resolutions with integrity (npm packages)
186
182
  if ('tarball' in currentResolution && currentResolution.integrity) {
@@ -200,7 +196,20 @@ async function resolveNpm(ctx, wantedDependency, opts) {
200
196
  manifest,
201
197
  resolution: currentResolution,
202
198
  resolvedVia: 'npm-registry',
203
- publishedAt: undefined, // Don't have this without metadata
199
+ publishedAt: opts.currentPkg.publishedAt,
200
+ // Loose-mode bypass: a lockfile entry whose publishedAt sits
201
+ // after the maturity cutoff would have been rejected at
202
+ // resolver time, but the peek path skips the maturity check.
203
+ // Report inline so the deps-resolver aggregator surfaces it
204
+ // to the install command.
205
+ policyViolation: detectMinReleaseAgeViolation({
206
+ name: manifest.name,
207
+ version: manifest.version,
208
+ publishedAt: opts.currentPkg.publishedAt,
209
+ resolution: currentResolution,
210
+ publishedBy: opts.publishedBy,
211
+ publishedByExclude: opts.publishedByExclude,
212
+ }),
204
213
  };
205
214
  }
206
215
  }
@@ -261,22 +270,6 @@ async function resolveNpm(ctx, wantedDependency, opts) {
261
270
  // ignore
262
271
  }
263
272
  }
264
- if (opts.publishedBy) {
265
- const immatureVersion = pickVersionByVersionRange({
266
- meta,
267
- versionRange: spec.fetchSpec,
268
- preferredVersionSelectors: opts.preferredVersions?.[spec.name],
269
- });
270
- if (immatureVersion) {
271
- throw new NoMatchingVersionError({
272
- wantedDependency,
273
- packageMeta: meta,
274
- registry,
275
- immatureVersion,
276
- publishedBy: opts.publishedBy,
277
- });
278
- }
279
- }
280
273
  throw new NoMatchingVersionError({ wantedDependency, packageMeta: meta, registry });
281
274
  }
282
275
  else if (opts.trustPolicy === 'no-downgrade') {
@@ -329,14 +322,23 @@ async function resolveNpm(ctx, wantedDependency, opts) {
329
322
  defaultPinnedVersion: opts.pinnedVersion,
330
323
  });
331
324
  }
325
+ const publishedAt = meta.time?.[pickedPackage.version];
332
326
  return {
333
327
  id,
334
328
  latest: meta['dist-tags'].latest,
335
329
  manifest: pickedPackage,
336
330
  resolution,
337
331
  resolvedVia: 'npm-registry',
338
- publishedAt: meta.time?.[pickedPackage.version],
332
+ publishedAt,
339
333
  normalizedBareSpecifier,
334
+ policyViolation: detectMinReleaseAgeViolation({
335
+ name: pickedPackage.name,
336
+ version: pickedPackage.version,
337
+ publishedAt,
338
+ resolution,
339
+ publishedBy: opts.publishedBy,
340
+ publishedByExclude: opts.publishedByExclude,
341
+ }),
340
342
  };
341
343
  }
342
344
  async function resolveJsr(ctx, wantedDependency, opts) {
@@ -433,15 +435,25 @@ async function pickFromSimpleRegistry(ctx, wantedDependency, opts, spec, registr
433
435
  if (pickedPackage == null) {
434
436
  throw new NoMatchingVersionError({ wantedDependency, packageMeta: meta, registry });
435
437
  }
438
+ const resolution = {
439
+ integrity: getIntegrity(pickedPackage.dist),
440
+ tarball: normalizeRegistryUrl(pickedPackage.dist.tarball),
441
+ };
442
+ const publishedAt = meta.time?.[pickedPackage.version];
436
443
  return {
437
444
  id: `${pickedPackage.name}@${pickedPackage.version}`,
438
445
  latest: meta['dist-tags'].latest,
439
446
  manifest: pickedPackage,
440
- resolution: {
441
- integrity: getIntegrity(pickedPackage.dist),
442
- tarball: normalizeRegistryUrl(pickedPackage.dist.tarball),
443
- },
444
- publishedAt: meta.time?.[pickedPackage.version],
447
+ resolution,
448
+ publishedAt,
449
+ policyViolation: detectMinReleaseAgeViolation({
450
+ name: pickedPackage.name,
451
+ version: pickedPackage.version,
452
+ publishedAt,
453
+ resolution,
454
+ publishedBy: opts.publishedBy,
455
+ publishedByExclude: opts.publishedByExclude,
456
+ }),
445
457
  };
446
458
  }
447
459
  // Builds a `<prefix><pkgName>@<range>` specifier (or a bare `<prefix><range>`
@@ -603,6 +615,40 @@ function defaultTagForAlias(alias, defaultTag) {
603
615
  type: 'tag',
604
616
  };
605
617
  }
618
+ /**
619
+ * Inline minimumReleaseAge detection: returns a violation entry when the
620
+ * picked version's publish timestamp is past the policy cutoff (and
621
+ * isn't covered by `publishedByExclude`). The resolver already has the
622
+ * timestamp in hand, so reporting inline saves the install layer from
623
+ * re-walking the resolved tree and re-fetching the same metadata. The
624
+ * deps-resolver aggregates the per-resolve `policyViolation` fields into
625
+ * a single set the install command reacts to.
626
+ *
627
+ * Returns `undefined` for resolutions outside the policy — no policy
628
+ * active, version excluded by pattern, timestamp missing or malformed,
629
+ * or version mature. Specific-version exclusions (`pkg@1.0.0`) and
630
+ * full-name exclusions (`pkg`) are both honored so an entry already on
631
+ * the user's exclude list isn't re-announced every install.
632
+ */
633
+ function detectMinReleaseAgeViolation(args) {
634
+ if (!args.publishedBy || !args.publishedAt)
635
+ return undefined;
636
+ const excludeResult = args.publishedByExclude?.(args.name);
637
+ if (excludeResult === true)
638
+ return undefined;
639
+ if (Array.isArray(excludeResult) && excludeResult.includes(args.version))
640
+ return undefined;
641
+ const ts = new Date(args.publishedAt).getTime();
642
+ if (Number.isNaN(ts) || ts <= args.publishedBy.getTime())
643
+ return undefined;
644
+ return {
645
+ name: args.name,
646
+ version: args.version,
647
+ resolution: args.resolution,
648
+ code: MINIMUM_RELEASE_AGE_VIOLATION_CODE,
649
+ reason: `was published at ${new Date(ts).toISOString()}, within the minimumReleaseAge cutoff (${args.publishedBy.toISOString()})`,
650
+ };
651
+ }
606
652
  function getIntegrity(dist) {
607
653
  if (dist.integrity) {
608
654
  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 {};