@pnpm/resolving.npm-resolver 1102.1.7 → 1102.1.9
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/CHANGELOG.md +39 -0
- package/lib/clearMeta.d.ts +24 -0
- package/lib/clearMeta.js +76 -0
- package/lib/createNpmResolutionVerifier.d.ts +89 -0
- package/lib/createNpmResolutionVerifier.js +724 -0
- package/lib/fetch.d.ts +45 -0
- package/lib/fetch.js +237 -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 +75 -0
- package/lib/index.d.ts +133 -0
- package/lib/memoizeFetchMetadata.d.ts +40 -0
- package/lib/memoizeFetchMetadata.js +66 -0
- package/lib/normalizeRegistryUrl.d.ts +4 -0
- package/lib/normalizeRegistryUrl.js +12 -0
- package/lib/parseBareSpecifier.d.ts +16 -0
- package/lib/parseBareSpecifier.js +143 -0
- package/lib/pickPackage.d.ts +113 -0
- package/lib/pickPackage.js +656 -0
- package/lib/pickPackageFromMeta.d.ts +20 -0
- package/lib/pickPackageFromMeta.js +227 -0
- package/lib/toRaw.d.ts +2 -0
- package/lib/toRaw.js +4 -0
- package/lib/trustChecks.d.ts +9 -0
- package/lib/trustChecks.js +96 -0
- package/lib/violationCodes.d.ts +14 -0
- package/lib/violationCodes.js +15 -0
- package/lib/whichVersionIsPinned.d.ts +2 -0
- package/lib/whichVersionIsPinned.js +38 -0
- package/lib/workspacePrefToNpm.d.ts +1 -0
- package/lib/workspacePrefToNpm.js +13 -0
- package/package.json +26 -26
package/lib/fetch.d.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { FetchError, type FetchErrorRequest, type FetchErrorResponse, PnpmError } from '@pnpm/error';
|
|
2
|
+
import type { FetchFromRegistry, RetryTimeoutOptions } from '@pnpm/fetching.types';
|
|
3
|
+
import type { PackageMeta } from '@pnpm/resolving.registry.types';
|
|
4
|
+
export interface FetchMetadataResult {
|
|
5
|
+
meta: PackageMeta;
|
|
6
|
+
/**
|
|
7
|
+
* The raw registry response body, used only to mirror the response to disk
|
|
8
|
+
* without re-serializing `meta`. A fresh fetch always sets it, and every
|
|
9
|
+
* caller sharing that in-flight request sees it. Once the request settles
|
|
10
|
+
* the phase-long memo cache drops the body (see memoizeFetchMetadata.ts),
|
|
11
|
+
* so later cache hits see `undefined` and the cache never pins the body.
|
|
12
|
+
*/
|
|
13
|
+
jsonText: string | undefined;
|
|
14
|
+
etag?: string;
|
|
15
|
+
notModified?: false;
|
|
16
|
+
}
|
|
17
|
+
export interface FetchMetadataNotModifiedResult {
|
|
18
|
+
notModified: true;
|
|
19
|
+
}
|
|
20
|
+
export declare class RegistryResponseError extends FetchError {
|
|
21
|
+
readonly pkgName: string;
|
|
22
|
+
constructor(request: FetchErrorRequest, response: FetchErrorResponse, pkgName: string);
|
|
23
|
+
}
|
|
24
|
+
export interface FetchMetadataFromFromRegistryOptions {
|
|
25
|
+
fetch: FetchFromRegistry;
|
|
26
|
+
retry: RetryTimeoutOptions;
|
|
27
|
+
timeout: number;
|
|
28
|
+
fetchWarnTimeoutMs: number;
|
|
29
|
+
}
|
|
30
|
+
export interface FetchMetadataOptions {
|
|
31
|
+
registry: string;
|
|
32
|
+
authHeaderValue?: string;
|
|
33
|
+
cacheBypass?: boolean;
|
|
34
|
+
fullMetadata?: boolean;
|
|
35
|
+
etag?: string;
|
|
36
|
+
modified?: string;
|
|
37
|
+
}
|
|
38
|
+
export declare function fetchMetadataFromFromRegistry(fetchOpts: FetchMetadataFromFromRegistryOptions, pkgName: string, { authHeaderValue, cacheBypass, etag: cachedEtag, fullMetadata, modified: cachedModified, registry, }: FetchMetadataOptions): Promise<FetchMetadataResult | FetchMetadataNotModifiedResult>;
|
|
39
|
+
/**
|
|
40
|
+
* A 304 answers a validator with "the body you already have is current". Sent
|
|
41
|
+
* without one — either because nothing was cached or because `cacheBypass`
|
|
42
|
+
* dropped the validators to recover a lost cache entry — it refers to a body
|
|
43
|
+
* nobody holds, so there is nothing to serve and nothing left to retry.
|
|
44
|
+
*/
|
|
45
|
+
export declare function notModifiedWithoutCacheError(pkgName: string): PnpmError;
|
package/lib/fetch.js
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import url from 'node:url';
|
|
2
|
+
import util from 'node:util';
|
|
3
|
+
import { requestRetryLogger } from '@pnpm/core-loggers';
|
|
4
|
+
import { FetchError, PnpmError, redactUrlCredentials, } from '@pnpm/error';
|
|
5
|
+
import { globalWarn } from '@pnpm/logger';
|
|
6
|
+
import * as retry from '@zkochan/retry';
|
|
7
|
+
import semver from 'semver';
|
|
8
|
+
import { clearMeta } from './clearMeta.js';
|
|
9
|
+
/**
|
|
10
|
+
* Content type of an abbreviated (install-oriented) package metadata document.
|
|
11
|
+
* A spec-compliant registry echoes this in the response `Content-Type` when it
|
|
12
|
+
* honors the abbreviated `Accept` header. Its absence signals that the registry
|
|
13
|
+
* ignored the header and served the full document instead.
|
|
14
|
+
* https://github.com/npm/registry/blob/main/docs/responses/package-metadata.md
|
|
15
|
+
*/
|
|
16
|
+
const ABBREVIATED_META_CONTENT_TYPE = 'application/vnd.npm.install-v1+json';
|
|
17
|
+
export class RegistryResponseError extends FetchError {
|
|
18
|
+
pkgName;
|
|
19
|
+
constructor(request, response, pkgName) {
|
|
20
|
+
let hint;
|
|
21
|
+
if (response.status === 404) {
|
|
22
|
+
hint = `${pkgName} is not in the npm registry, or you have no permission to fetch it.`;
|
|
23
|
+
const nameWithoutVersion = stripTrailingSemverSuffix(pkgName);
|
|
24
|
+
if (nameWithoutVersion != null) {
|
|
25
|
+
hint += ` Did you mean ${nameWithoutVersion}?`;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
super(request, response, hint);
|
|
29
|
+
this.pkgName = pkgName;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Detect when a package name accidentally includes a `<version>` suffix
|
|
34
|
+
* (e.g. `lodash@4.17.21` or `lodash4.17.21`) and return the part before the
|
|
35
|
+
* version. Returns `undefined` when no semver suffix is present.
|
|
36
|
+
*
|
|
37
|
+
* Implemented as an O(n) scan to avoid polynomial backtracking on adversarial
|
|
38
|
+
* input (CodeQL: js/polynomial-redos).
|
|
39
|
+
*/
|
|
40
|
+
function stripTrailingSemverSuffix(pkgName) {
|
|
41
|
+
// Common case: "name@version" – split on the rightmost '@'.
|
|
42
|
+
// `atIdx > 0` rules out the leading '@' of scoped names like '@scope/foo'.
|
|
43
|
+
const atIdx = pkgName.lastIndexOf('@');
|
|
44
|
+
if (atIdx > 0 && semver.valid(pkgName.slice(atIdx + 1)) != null) {
|
|
45
|
+
return pkgName.slice(0, atIdx);
|
|
46
|
+
}
|
|
47
|
+
// Fallback: detect a trailing "<digits>.<digits>.<digits>" appended to a name
|
|
48
|
+
// with no separator (e.g. "foo1.0.0"). We walk backwards through three
|
|
49
|
+
// digit-blocks separated by dots; this is O(n) and free of regex backtracking.
|
|
50
|
+
let i = pkgName.length;
|
|
51
|
+
i = consumeTrailingDigits(pkgName, i);
|
|
52
|
+
if (i === pkgName.length || i === 0 || pkgName.charCodeAt(i - 1) !== 46 /* '.' */)
|
|
53
|
+
return undefined;
|
|
54
|
+
i--;
|
|
55
|
+
const beforePatch = i;
|
|
56
|
+
i = consumeTrailingDigits(pkgName, i);
|
|
57
|
+
if (i === beforePatch || i === 0 || pkgName.charCodeAt(i - 1) !== 46)
|
|
58
|
+
return undefined;
|
|
59
|
+
i--;
|
|
60
|
+
const beforeMinor = i;
|
|
61
|
+
i = consumeTrailingDigits(pkgName, i);
|
|
62
|
+
if (i === beforeMinor || i === 0)
|
|
63
|
+
return undefined;
|
|
64
|
+
if (semver.valid(pkgName.slice(i)) == null)
|
|
65
|
+
return undefined;
|
|
66
|
+
let prefix = pkgName.slice(0, i);
|
|
67
|
+
if (prefix.endsWith('@'))
|
|
68
|
+
prefix = prefix.slice(0, -1);
|
|
69
|
+
return prefix.length > 0 ? prefix : undefined;
|
|
70
|
+
}
|
|
71
|
+
function consumeTrailingDigits(s, end) {
|
|
72
|
+
let i = end;
|
|
73
|
+
while (i > 0) {
|
|
74
|
+
const c = s.charCodeAt(i - 1);
|
|
75
|
+
if (c < 48 || c > 57)
|
|
76
|
+
break;
|
|
77
|
+
i--;
|
|
78
|
+
}
|
|
79
|
+
return i;
|
|
80
|
+
}
|
|
81
|
+
export async function fetchMetadataFromFromRegistry(fetchOpts, pkgName, { authHeaderValue, cacheBypass = false, etag: cachedEtag, fullMetadata, modified: cachedModified, registry, }) {
|
|
82
|
+
const uri = toUri(pkgName, registry);
|
|
83
|
+
const op = retry.operation(fetchOpts.retry);
|
|
84
|
+
const ifNoneMatch = cacheBypass ? undefined : cachedEtag;
|
|
85
|
+
const ifModifiedSince = cacheBypass || !cachedModified
|
|
86
|
+
? undefined
|
|
87
|
+
: new Date(cachedModified).toUTCString();
|
|
88
|
+
const hasValidator = Boolean(ifNoneMatch || ifModifiedSince);
|
|
89
|
+
return new Promise((resolve, reject) => {
|
|
90
|
+
op.attempt(async (attempt) => {
|
|
91
|
+
let response;
|
|
92
|
+
const startTime = Date.now();
|
|
93
|
+
try {
|
|
94
|
+
const requestOptions = {
|
|
95
|
+
authHeaderValue,
|
|
96
|
+
compress: true,
|
|
97
|
+
fullMetadata,
|
|
98
|
+
ifNoneMatch,
|
|
99
|
+
ifModifiedSince,
|
|
100
|
+
retry: fetchOpts.retry,
|
|
101
|
+
timeout: fetchOpts.timeout,
|
|
102
|
+
headers: cacheBypass ? { 'cache-control': 'no-cache' } : undefined,
|
|
103
|
+
};
|
|
104
|
+
response = await fetchOpts.fetch(uri, requestOptions);
|
|
105
|
+
if (response.status === 304 && !hasValidator && !cacheBypass) {
|
|
106
|
+
response = await fetchOpts.fetch(uri, {
|
|
107
|
+
...requestOptions,
|
|
108
|
+
headers: {
|
|
109
|
+
'cache-control': 'no-cache',
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
catch (error) { // eslint-disable-line
|
|
115
|
+
// Redact credentials embedded in the URL from the cause as well, not
|
|
116
|
+
// just the top-level message: a reporter or debugger that renders
|
|
117
|
+
// `error.cause` would otherwise print the raw URL-bearing message. The
|
|
118
|
+
// `stack` string embeds the original (pre-mutation) message, so redact
|
|
119
|
+
// it too — mutating `message` alone leaves the credentials in `stack`.
|
|
120
|
+
if (util.types.isNativeError(error)) {
|
|
121
|
+
if (typeof error.message === 'string')
|
|
122
|
+
error.message = redactUrlCredentials(error.message);
|
|
123
|
+
if (typeof error.stack === 'string')
|
|
124
|
+
error.stack = redactUrlCredentials(error.stack);
|
|
125
|
+
}
|
|
126
|
+
reject(new PnpmError('META_FETCH_FAIL', redactUrlCredentials(`GET ${uri}: ${error.message}`), { attempts: attempt, cause: error }));
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (response.status === 304) {
|
|
130
|
+
if (!hasValidator) {
|
|
131
|
+
reject(notModifiedWithoutCacheError(pkgName));
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
resolve({ notModified: true });
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (response.status >= 400) {
|
|
138
|
+
const request = {
|
|
139
|
+
authHeaderValue,
|
|
140
|
+
url: uri,
|
|
141
|
+
};
|
|
142
|
+
reject(new RegistryResponseError(request, response, pkgName));
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
// Here we only retry broken JSON responses.
|
|
146
|
+
// Other HTTP issues are retried by the @pnpm/network.fetch library
|
|
147
|
+
try {
|
|
148
|
+
const jsonText = await response.text();
|
|
149
|
+
const meta = JSON.parse(jsonText);
|
|
150
|
+
// Check if request took longer than expected
|
|
151
|
+
const elapsedMs = Date.now() - startTime;
|
|
152
|
+
if (elapsedMs > fetchOpts.fetchWarnTimeoutMs) {
|
|
153
|
+
globalWarn(`Request took ${elapsedMs}ms: ${uri}`);
|
|
154
|
+
}
|
|
155
|
+
resolve({
|
|
156
|
+
...normalizeAbbreviatedResponse({ fullMetadata, meta, jsonText, response }),
|
|
157
|
+
etag: response.headers.get('etag') ?? undefined,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
catch (error) { // eslint-disable-line
|
|
161
|
+
const timeout = op.retry(new PnpmError('BROKEN_METADATA_JSON', error.message));
|
|
162
|
+
if (timeout === false) {
|
|
163
|
+
reject(op.mainError());
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
// Extract error properties into a plain object because Error properties
|
|
167
|
+
// are non-enumerable and don't serialize well through the logging system
|
|
168
|
+
const errorInfo = {
|
|
169
|
+
name: error.name,
|
|
170
|
+
message: error.message,
|
|
171
|
+
code: error.code,
|
|
172
|
+
errno: error.errno,
|
|
173
|
+
};
|
|
174
|
+
requestRetryLogger.debug({
|
|
175
|
+
attempt,
|
|
176
|
+
error: errorInfo,
|
|
177
|
+
maxRetries: fetchOpts.retry.retries,
|
|
178
|
+
method: 'GET',
|
|
179
|
+
timeout,
|
|
180
|
+
url: uri,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* A 304 answers a validator with "the body you already have is current". Sent
|
|
188
|
+
* without one — either because nothing was cached or because `cacheBypass`
|
|
189
|
+
* dropped the validators to recover a lost cache entry — it refers to a body
|
|
190
|
+
* nobody holds, so there is nothing to serve and nothing left to retry.
|
|
191
|
+
*/
|
|
192
|
+
export function notModifiedWithoutCacheError(pkgName) {
|
|
193
|
+
return new PnpmError('META_NOT_MODIFIED_WITHOUT_CACHE', `Registry returned 304 for ${pkgName} without an existing cache to refresh.`);
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* When the resolver asked for abbreviated metadata but the registry ignored the
|
|
197
|
+
* `Accept` header and returned the full document (detected via the response
|
|
198
|
+
* `Content-Type`), strip it down to the abbreviated field set so downstream
|
|
199
|
+
* consumers — the in-memory cache, the on-disk mirror, and the resolver — never
|
|
200
|
+
* carry the megabytes of install-irrelevant data (scripts, exports, readme,
|
|
201
|
+
* custom fields) that a full document contains.
|
|
202
|
+
*
|
|
203
|
+
* Registries that honor the header (e.g. the npm registry) echo the abbreviated
|
|
204
|
+
* `Content-Type`, so this is a no-op for them: no re-serialization, no field
|
|
205
|
+
* stripping — the happy path pays nothing.
|
|
206
|
+
*/
|
|
207
|
+
function normalizeAbbreviatedResponse({ fullMetadata, meta, jsonText, response }) {
|
|
208
|
+
if (fullMetadata)
|
|
209
|
+
return { meta, jsonText };
|
|
210
|
+
if (parseMediaType(response.headers.get('content-type')) === ABBREVIATED_META_CONTENT_TYPE)
|
|
211
|
+
return { meta, jsonText };
|
|
212
|
+
const normalized = clearMeta(meta);
|
|
213
|
+
return { meta: normalized, jsonText: JSON.stringify(normalized) };
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Extracts the media type from a `Content-Type` header value, dropping
|
|
217
|
+
* parameters such as `; charset=utf-8`. Media types are case-insensitive
|
|
218
|
+
* (RFC 9110 §8.3.1), so the result is lowercased for comparison.
|
|
219
|
+
*/
|
|
220
|
+
function parseMediaType(contentType) {
|
|
221
|
+
if (contentType == null)
|
|
222
|
+
return undefined;
|
|
223
|
+
const semicolonIndex = contentType.indexOf(';');
|
|
224
|
+
const mediaType = semicolonIndex === -1 ? contentType : contentType.slice(0, semicolonIndex);
|
|
225
|
+
return mediaType.trim().toLowerCase();
|
|
226
|
+
}
|
|
227
|
+
function toUri(pkgName, registry) {
|
|
228
|
+
let encodedName;
|
|
229
|
+
if (pkgName[0] === '@') {
|
|
230
|
+
encodedName = `@${encodeURIComponent(pkgName.slice(1))}`;
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
encodedName = encodeURIComponent(pkgName);
|
|
234
|
+
}
|
|
235
|
+
return new url.URL(encodedName, registry.endsWith('/') ? registry : `${registry}/`).toString();
|
|
236
|
+
}
|
|
237
|
+
//# sourceMappingURL=fetch.js.map
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { FetchMetadataFromFromRegistryOptions } from './fetch.js';
|
|
2
|
+
/**
|
|
3
|
+
* Per-version publish timestamp from npm's attestation endpoint —
|
|
4
|
+
* `/-/npm/v1/attestations/<name>@<version>`.
|
|
5
|
+
*
|
|
6
|
+
* The response is a small JSON document containing one or more Sigstore
|
|
7
|
+
* bundles. We read `bundle.verificationMaterial.tlogEntries[].integratedTime`
|
|
8
|
+
* (the Rekor inclusion time) and surface it as an ISO date. This is a
|
|
9
|
+
* couple of seconds after the actual publish — close enough for a
|
|
10
|
+
* release-age policy that operates in minutes/hours/days.
|
|
11
|
+
*
|
|
12
|
+
* We deliberately do **not** verify the Sigstore signature here: the
|
|
13
|
+
* trust model is identical to reading the registry's `time` field on
|
|
14
|
+
* the full metadata document. The win is bandwidth — the attestation
|
|
15
|
+
* payload is tens of KB versus the multi-MB full metadata document, so
|
|
16
|
+
* cold-cache + `--frozen-lockfile` installs against a fleet of
|
|
17
|
+
* provenance-published packages pay far less to verify timestamps.
|
|
18
|
+
*
|
|
19
|
+
* Returns `undefined` when:
|
|
20
|
+
*
|
|
21
|
+
* - The package has no published attestations (`404`).
|
|
22
|
+
* - The response is malformed or missing the timestamp.
|
|
23
|
+
* - The request itself fails (network error, registry 5xx).
|
|
24
|
+
*
|
|
25
|
+
* In all of those cases the caller falls back to fetching full metadata.
|
|
26
|
+
*/
|
|
27
|
+
export interface FetchAttestationOptions {
|
|
28
|
+
registry: string;
|
|
29
|
+
authHeaderValue?: string;
|
|
30
|
+
}
|
|
31
|
+
export declare function fetchAttestationPublishedAt(fetchOpts: FetchMetadataFromFromRegistryOptions, pkgName: string, version: string, opts: FetchAttestationOptions): Promise<string | undefined>;
|
|
@@ -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,75 @@
|
|
|
1
|
+
import { ABBREVIATED_META_DIR, FULL_META_DIR } from '@pnpm/constants';
|
|
2
|
+
import { fetchMetadataFromFromRegistry, } from './fetch.js';
|
|
3
|
+
import { getPkgMirrorPath, loadMeta, loadMetaHeaders, prepareJsonForDisk, saveMeta } from './pickPackage.js';
|
|
4
|
+
/**
|
|
5
|
+
* Fetch a full registry metadata document for `pkgName`, reusing pnpm's
|
|
6
|
+
* shared on-disk metadata mirror when `cacheDir` is supplied. Built for the
|
|
7
|
+
* `minimumReleaseAge` lockfile revalidation gate, which needs the `time`
|
|
8
|
+
* field that abbreviated metadata omits; the cache reuse keeps repeat
|
|
9
|
+
* installs from re-downloading the same multi-megabyte document for every
|
|
10
|
+
* locked package.
|
|
11
|
+
*/
|
|
12
|
+
export async function fetchFullMetadataCached(fetchOpts, pkgName, opts) {
|
|
13
|
+
return fetchMetadataCached(fetchOpts, pkgName, { ...opts, fullMetadata: true, metaDir: FULL_META_DIR });
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Sibling of {@link fetchFullMetadataCached} that hits the abbreviated
|
|
17
|
+
* metadata endpoint (`Accept: application/vnd.npm.install-v1+json`) and
|
|
18
|
+
* caches under `ABBREVIATED_META_DIR` — the same mirror the resolver
|
|
19
|
+
* populates by default. Used by the lockfile verification gate as a
|
|
20
|
+
* cheap upper-bound check: if the package's `modified` field is older
|
|
21
|
+
* than the policy cutoff, every version in it predates the cutoff and
|
|
22
|
+
* no per-version timestamp lookup is needed.
|
|
23
|
+
*/
|
|
24
|
+
export async function fetchAbbreviatedMetadataCached(fetchOpts, pkgName, opts) {
|
|
25
|
+
return fetchMetadataCached(fetchOpts, pkgName, { ...opts, fullMetadata: false, metaDir: ABBREVIATED_META_DIR });
|
|
26
|
+
}
|
|
27
|
+
async function fetchMetadataCached(fetchOpts, pkgName, opts) {
|
|
28
|
+
const pkgMirror = opts.cacheDir != null
|
|
29
|
+
? getPkgMirrorPath(opts.cacheDir, opts.metaDir, opts.registry, pkgName)
|
|
30
|
+
: null;
|
|
31
|
+
const cacheHeaders = pkgMirror != null ? await loadMetaHeaders(pkgMirror) : null;
|
|
32
|
+
const conditional = await fetchMetadataFromFromRegistry(fetchOpts, pkgName, {
|
|
33
|
+
registry: opts.registry,
|
|
34
|
+
authHeaderValue: opts.authHeaderValue,
|
|
35
|
+
fullMetadata: opts.fullMetadata,
|
|
36
|
+
etag: cacheHeaders?.etag,
|
|
37
|
+
modified: cacheHeaders?.modified,
|
|
38
|
+
});
|
|
39
|
+
if (!conditional.notModified)
|
|
40
|
+
return persistAndReturn(conditional);
|
|
41
|
+
// A 304 only resolves as `notModified` when a validator was sent, which
|
|
42
|
+
// requires cache headers loaded from a mirror — so a null mirror here is an
|
|
43
|
+
// unreachable invariant breach.
|
|
44
|
+
if (pkgMirror == null)
|
|
45
|
+
throw new Error(`Unexpected 304 for ${pkgName} without a metadata cache`);
|
|
46
|
+
const cached = await loadMeta(pkgMirror);
|
|
47
|
+
if (cached != null)
|
|
48
|
+
return cached;
|
|
49
|
+
// The mirror vanished between the headers read and this read (concurrent
|
|
50
|
+
// store cleanup, antivirus, ...), so the 304 now validates nothing. Ask again
|
|
51
|
+
// as a cold cache would, which the registry can only answer with a body or an
|
|
52
|
+
// error — never another 304.
|
|
53
|
+
const refetched = await fetchMetadataFromFromRegistry(fetchOpts, pkgName, {
|
|
54
|
+
registry: opts.registry,
|
|
55
|
+
authHeaderValue: opts.authHeaderValue,
|
|
56
|
+
cacheBypass: true,
|
|
57
|
+
fullMetadata: opts.fullMetadata,
|
|
58
|
+
});
|
|
59
|
+
// Unreachable narrowing guard: the cache-bypassing request sends no validator,
|
|
60
|
+
// so fetchMetadataFromFromRegistry rejects a repeated 304 before returning.
|
|
61
|
+
if (refetched.notModified)
|
|
62
|
+
throw new Error(`Unexpected 304 for ${pkgName} on a cache-bypassing refetch`);
|
|
63
|
+
return persistAndReturn(refetched);
|
|
64
|
+
// Persist a freshly downloaded body so the next install can do a headers-only
|
|
65
|
+
// conditional GET, then hand its meta back. Fire-and-forget — a cache-write
|
|
66
|
+
// failure isn't a reason to fail the caller; the next install just won't get
|
|
67
|
+
// the speedup.
|
|
68
|
+
function persistAndReturn(fetched) {
|
|
69
|
+
if (pkgMirror != null) {
|
|
70
|
+
saveMeta(pkgMirror, prepareJsonForDisk(fetched.meta, fetched.etag, fetched.jsonText)).catch(() => { });
|
|
71
|
+
}
|
|
72
|
+
return fetched.meta;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=fetchFullMetadataCached.js.map
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { PnpmError } from '@pnpm/error';
|
|
2
|
+
import type { FetchFromRegistry, GetAuthHeader, RetryTimeoutOptions } from '@pnpm/fetching.types';
|
|
3
|
+
import type { PackageMeta } from '@pnpm/resolving.registry.types';
|
|
4
|
+
import type { DirectoryResolution, LatestInfo, LatestQuery, PkgResolutionId, PreferredVersions, ResolveOptions, ResolveResult, TarballResolution, WantedDependency, WorkspacePackages } from '@pnpm/resolving.resolver-base';
|
|
5
|
+
import type { DependencyManifest, PackageVersionPolicy, PinnedVersion, Registries, TrustPolicy } from '@pnpm/types';
|
|
6
|
+
import { fetchMetadataFromFromRegistry, type FetchMetadataFromFromRegistryOptions, RegistryResponseError } from './fetch.js';
|
|
7
|
+
import { BUILTIN_NAMED_REGISTRIES, parseBareSpecifier, type RegistryPackageSpec } from './parseBareSpecifier.js';
|
|
8
|
+
import { type PackageMetaCache, pickPackage, type PickPackageOptions } from './pickPackage.js';
|
|
9
|
+
import { pickPackageFromMeta, pickVersionByVersionRange } from './pickPackageFromMeta.js';
|
|
10
|
+
import { workspacePrefToNpm } from './workspacePrefToNpm.js';
|
|
11
|
+
export interface NoMatchingVersionErrorOptions {
|
|
12
|
+
wantedDependency: WantedDependency;
|
|
13
|
+
packageMeta: PackageMeta;
|
|
14
|
+
registry: string;
|
|
15
|
+
}
|
|
16
|
+
export declare class NoMatchingVersionError extends PnpmError {
|
|
17
|
+
readonly packageMeta: PackageMeta;
|
|
18
|
+
constructor(opts: NoMatchingVersionErrorOptions);
|
|
19
|
+
}
|
|
20
|
+
export declare function formatTimeAgo(date: Date): string | null;
|
|
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';
|
|
24
|
+
export { whichVersionIsPinned } from './whichVersionIsPinned.js';
|
|
25
|
+
export interface ResolverFactoryOptions {
|
|
26
|
+
cacheDir: string;
|
|
27
|
+
storeDir?: string;
|
|
28
|
+
frozenStore?: boolean;
|
|
29
|
+
fullMetadata?: boolean;
|
|
30
|
+
filterMetadata?: boolean;
|
|
31
|
+
offline?: boolean;
|
|
32
|
+
preferOffline?: boolean;
|
|
33
|
+
retry?: RetryTimeoutOptions;
|
|
34
|
+
timeout?: number;
|
|
35
|
+
registries: Registries;
|
|
36
|
+
namedRegistries?: Record<string, string>;
|
|
37
|
+
saveWorkspaceProtocol?: boolean | 'rolling';
|
|
38
|
+
preserveAbsolutePaths?: boolean;
|
|
39
|
+
ignoreMissingTimeField?: boolean;
|
|
40
|
+
fetchWarnTimeoutMs?: number;
|
|
41
|
+
/** Pre-populated metadata cache. When provided, the resolver uses this
|
|
42
|
+
* instead of creating a new LRU cache. Useful for servers that keep
|
|
43
|
+
* metadata in SQLite or persist it across requests. */
|
|
44
|
+
metaCache?: PackageMetaCache;
|
|
45
|
+
}
|
|
46
|
+
export interface NpmResolveResult extends ResolveResult {
|
|
47
|
+
latest?: string;
|
|
48
|
+
manifest: DependencyManifest;
|
|
49
|
+
resolution: TarballResolution;
|
|
50
|
+
resolvedVia: 'npm-registry';
|
|
51
|
+
}
|
|
52
|
+
export interface JsrResolveResult extends ResolveResult {
|
|
53
|
+
alias: string;
|
|
54
|
+
manifest: DependencyManifest;
|
|
55
|
+
resolution: TarballResolution;
|
|
56
|
+
resolvedVia: 'jsr-registry';
|
|
57
|
+
}
|
|
58
|
+
export interface NamedRegistryResolveResult extends ResolveResult {
|
|
59
|
+
alias: string;
|
|
60
|
+
/** The named-registry alias that was matched, e.g. `gh` or a user-defined name. */
|
|
61
|
+
registryName: string;
|
|
62
|
+
manifest: DependencyManifest;
|
|
63
|
+
resolution: TarballResolution;
|
|
64
|
+
resolvedVia: 'named-registry';
|
|
65
|
+
}
|
|
66
|
+
export interface WorkspaceResolveResult extends ResolveResult {
|
|
67
|
+
manifest: DependencyManifest;
|
|
68
|
+
resolution: DirectoryResolution;
|
|
69
|
+
resolvedVia: 'workspace';
|
|
70
|
+
}
|
|
71
|
+
export type NpmResolver = (wantedDependency: WantedDependency & {
|
|
72
|
+
optional?: boolean;
|
|
73
|
+
}, opts: ResolveFromNpmOptions) => Promise<NpmResolveResult | JsrResolveResult | NamedRegistryResolveResult | WorkspaceResolveResult | null>;
|
|
74
|
+
export type ResolveLatestFromNpmStyle = (query: LatestQuery, opts: ResolveOptions) => Promise<LatestInfo | undefined>;
|
|
75
|
+
export declare function createNpmResolver(fetchFromRegistry: FetchFromRegistry, getAuthHeader: GetAuthHeader, opts: ResolverFactoryOptions): {
|
|
76
|
+
resolveFromNpm: NpmResolver;
|
|
77
|
+
resolveFromJsr: NpmResolver;
|
|
78
|
+
resolveFromNamedRegistry: NpmResolver;
|
|
79
|
+
resolveLatestFromNpm: ResolveLatestFromNpmStyle;
|
|
80
|
+
resolveLatestFromJsr: ResolveLatestFromNpmStyle;
|
|
81
|
+
resolveLatestFromNamedRegistry: ResolveLatestFromNpmStyle;
|
|
82
|
+
clearCache: () => void;
|
|
83
|
+
};
|
|
84
|
+
export interface ResolveFromNpmContext {
|
|
85
|
+
pickPackage: (spec: RegistryPackageSpec, opts: PickPackageOptions) => ReturnType<typeof pickPackage>;
|
|
86
|
+
getAuthHeaderValueByURI: GetAuthHeader;
|
|
87
|
+
registries: Registries;
|
|
88
|
+
namedRegistries: Record<string, string>;
|
|
89
|
+
namedRegistryNames: ReadonlySet<string>;
|
|
90
|
+
saveWorkspaceProtocol?: boolean | 'rolling';
|
|
91
|
+
peekManifestFromStore?: (opts: {
|
|
92
|
+
id: PkgResolutionId;
|
|
93
|
+
integrity: string;
|
|
94
|
+
name?: string;
|
|
95
|
+
version?: string;
|
|
96
|
+
}) => Promise<DependencyManifest | undefined>;
|
|
97
|
+
/** Deduplicates the held-back-update warning per `(name, picked, preferred)`. */
|
|
98
|
+
warnedHeldBackUpdates: Set<string>;
|
|
99
|
+
}
|
|
100
|
+
export type ResolveFromNpmOptions = {
|
|
101
|
+
alwaysTryWorkspacePackages?: boolean;
|
|
102
|
+
defaultTag?: string;
|
|
103
|
+
publishedBy?: Date;
|
|
104
|
+
publishedByExclude?: PackageVersionPolicy;
|
|
105
|
+
pickLowestVersion?: boolean;
|
|
106
|
+
trustPolicy?: TrustPolicy;
|
|
107
|
+
trustPolicyExclude?: PackageVersionPolicy;
|
|
108
|
+
trustPolicyIgnoreAfter?: number;
|
|
109
|
+
dryRun?: boolean;
|
|
110
|
+
lockfileDir?: string;
|
|
111
|
+
preferredVersions?: PreferredVersions;
|
|
112
|
+
preferWorkspacePackages?: boolean;
|
|
113
|
+
update?: false | 'compatible' | 'latest';
|
|
114
|
+
updateRequested?: boolean;
|
|
115
|
+
updateChecksums?: boolean;
|
|
116
|
+
injectWorkspacePackages?: boolean;
|
|
117
|
+
calcSpecifier?: boolean;
|
|
118
|
+
pinnedVersion?: PinnedVersion;
|
|
119
|
+
} & ({
|
|
120
|
+
projectDir?: string;
|
|
121
|
+
workspacePackages?: undefined;
|
|
122
|
+
} | {
|
|
123
|
+
projectDir: string;
|
|
124
|
+
workspacePackages: WorkspacePackages;
|
|
125
|
+
});
|
|
126
|
+
/**
|
|
127
|
+
* Construct the LRU `PackageMetaCache` instance the resolver uses by
|
|
128
|
+
* default. Exported so the install layer can build one cache and hand
|
|
129
|
+
* the same reference to both the resolver and the verifier — the
|
|
130
|
+
* verifier's fast path reads from it when the resolver has already
|
|
131
|
+
* fetched a packument during the same install.
|
|
132
|
+
*/
|
|
133
|
+
export declare function createDefaultPackageMetaCache(): PackageMetaCache;
|