@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.
- package/lib/createNpmResolutionVerifier.d.ts +74 -0
- package/lib/createNpmResolutionVerifier.js +473 -0
- 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 +7 -5
- package/lib/index.js +134 -39
- package/lib/pickPackage.d.ts +32 -1
- package/lib/pickPackage.js +37 -31
- package/lib/pickPackageFromMeta.js +6 -2
- package/lib/violationCodes.d.ts +12 -0
- package/lib/violationCodes.js +13 -0
- package/package.json +16 -16
|
@@ -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
|
-
|
|
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:
|
|
140
|
-
resolveFromJsr:
|
|
141
|
-
resolveFromNamedRegistry:
|
|
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
|
|
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
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
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;
|
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 {};
|
package/lib/pickPackage.js
CHANGED
|
@@ -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
|
-
//
|
|
62
|
-
//
|
|
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
|
|
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
|
|
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
|
|
198
|
-
|
|
199
|
-
|
|
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
|
|
219
|
-
|
|
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
|
-
|
|
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
|
|
367
|
-
// The package was last modified before the maturity cutoff.
|
|
368
|
-
//
|
|
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
|
|
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
|
|
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";
|