@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.
- package/lib/createNpmResolutionVerifier.d.ts +74 -0
- package/lib/createNpmResolutionVerifier.js +473 -0
- package/lib/fetch.js +53 -6
- package/lib/fetchAttestationPublishedAt.d.ts +31 -0
- package/lib/fetchAttestationPublishedAt.js +99 -0
- package/lib/fetchFullMetadataCached.d.ts +33 -0
- package/lib/fetchFullMetadataCached.js +63 -0
- package/lib/index.d.ts +2 -4
- package/lib/index.js +84 -38
- package/lib/pickPackage.d.ts +32 -1
- package/lib/pickPackage.js +130 -25
- package/lib/violationCodes.d.ts +12 -0
- package/lib/violationCodes.js +13 -0
- package/package.json +13 -13
|
@@ -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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
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;
|
package/lib/pickPackage.d.ts
CHANGED
|
@@ -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 {};
|