@pnpm/resolving.npm-resolver 1102.1.3 → 1102.1.4
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 +7 -0
- package/package.json +6 -6
- package/lib/clearMeta.d.ts +0 -16
- package/lib/clearMeta.js +0 -55
- package/lib/createNpmResolutionVerifier.d.ts +0 -89
- package/lib/createNpmResolutionVerifier.js +0 -724
- package/lib/fetch.d.ts +0 -37
- package/lib/fetch.js +0 -224
- package/lib/fetchAttestationPublishedAt.d.ts +0 -31
- package/lib/fetchAttestationPublishedAt.js +0 -99
- package/lib/fetchFullMetadataCached.d.ts +0 -33
- package/lib/fetchFullMetadataCached.js +0 -63
- package/lib/index.d.ts +0 -133
- package/lib/memoizeFetchMetadata.d.ts +0 -24
- package/lib/memoizeFetchMetadata.js +0 -37
- package/lib/normalizeRegistryUrl.d.ts +0 -4
- package/lib/normalizeRegistryUrl.js +0 -12
- package/lib/parseBareSpecifier.d.ts +0 -16
- package/lib/parseBareSpecifier.js +0 -143
- package/lib/pickPackage.d.ts +0 -109
- package/lib/pickPackage.js +0 -631
- package/lib/pickPackageFromMeta.d.ts +0 -20
- package/lib/pickPackageFromMeta.js +0 -227
- package/lib/toRaw.d.ts +0 -2
- package/lib/toRaw.js +0 -4
- package/lib/trustChecks.d.ts +0 -9
- package/lib/trustChecks.js +0 -96
- package/lib/violationCodes.d.ts +0 -14
- package/lib/violationCodes.js +0 -15
- package/lib/whichVersionIsPinned.d.ts +0 -2
- package/lib/whichVersionIsPinned.js +0 -36
- package/lib/workspacePrefToNpm.d.ts +0 -1
- package/lib/workspacePrefToNpm.js +0 -13
|
@@ -1,724 +0,0 @@
|
|
|
1
|
-
import { pickRegistryForPackage } from '@pnpm/config.pick-registry-for-package';
|
|
2
|
-
import { createPackageVersionPolicy } from '@pnpm/config.version-policy';
|
|
3
|
-
import { FULL_META_DIR } from '@pnpm/constants';
|
|
4
|
-
import { PnpmError } from '@pnpm/error';
|
|
5
|
-
import { isGitHostedTarballUrl, } from '@pnpm/resolving.resolver-base';
|
|
6
|
-
import semver from 'semver';
|
|
7
|
-
import { fetchAttestationPublishedAt } from './fetchAttestationPublishedAt.js';
|
|
8
|
-
import { fetchAbbreviatedMetadataCached, fetchFullMetadataCached, } from './fetchFullMetadataCached.js';
|
|
9
|
-
import { normalizeRegistryUrl } from './normalizeRegistryUrl.js';
|
|
10
|
-
import { BUILTIN_NAMED_REGISTRIES } from './parseBareSpecifier.js';
|
|
11
|
-
import { getPkgMetaCacheKey, getPkgMirrorPath, loadMeta, warnMissingTimeFieldOnce } from './pickPackage.js';
|
|
12
|
-
import { failIfTrustDowngraded } from './trustChecks.js';
|
|
13
|
-
import { MINIMUM_RELEASE_AGE_VIOLATION_CODE, MISSING_TARBALL_INTEGRITY_VIOLATION_CODE, TARBALL_URL_MISMATCH_VIOLATION_CODE, TRUST_DOWNGRADE_VIOLATION_CODE, } from './violationCodes.js';
|
|
14
|
-
/**
|
|
15
|
-
* Returns a `ResolutionVerifier` for npm-registry-resolved lockfile
|
|
16
|
-
* entries. It always binds each entry's recorded tarball URL to the
|
|
17
|
-
* artifact the registry's metadata lists (an anti-tamper check that does
|
|
18
|
-
* not depend on any policy), and additionally re-applies the
|
|
19
|
-
* `minimumReleaseAge` and/or `trustPolicy='no-downgrade'` policies when
|
|
20
|
-
* those are configured. Pairs with `createNpmResolver`: each resolver
|
|
21
|
-
* factory may export a sibling verifier factory that the default-resolver
|
|
22
|
-
* combines.
|
|
23
|
-
*
|
|
24
|
-
* Designed for fail-closed semantics: if the manifest can't be loaded or
|
|
25
|
-
* the pinned version is missing from it, the verifier reports a violation
|
|
26
|
-
* rather than silently passing. Mirrors the post-resolution gate bun added
|
|
27
|
-
* for the same shape of bug in oven-sh/bun#30526.
|
|
28
|
-
*/
|
|
29
|
-
export function createNpmResolutionVerifier(opts) {
|
|
30
|
-
const ageCheckActive = Boolean(opts.minimumReleaseAge);
|
|
31
|
-
const trustCheckActive = opts.trustPolicy === 'no-downgrade';
|
|
32
|
-
const cutoff = ageCheckActive
|
|
33
|
-
? (opts.now ?? Date.now()) - opts.minimumReleaseAge * 60 * 1000
|
|
34
|
-
: 0;
|
|
35
|
-
const excludePolicy = opts.minimumReleaseAgeExclude?.length
|
|
36
|
-
? createExcludePolicy(opts.minimumReleaseAgeExclude, 'minimumReleaseAgeExclude')
|
|
37
|
-
: undefined;
|
|
38
|
-
const trustExcludePolicy = opts.trustPolicyExclude?.length
|
|
39
|
-
? createExcludePolicy(opts.trustPolicyExclude, 'trustPolicyExclude')
|
|
40
|
-
: undefined;
|
|
41
|
-
// Pre-normalize named-registry URLs and sort by length so two registries
|
|
42
|
-
// that share a hostname but differ by path (e.g. `https://npm/team-a/` vs
|
|
43
|
-
// `https://npm/team-b/`) route to the longest matching prefix — matching
|
|
44
|
-
// only `origin` would silently send lookups to the wrong one. Built-in
|
|
45
|
-
// aliases (`gh:` → npm.pkg.github.com, etc.) are merged in alongside the
|
|
46
|
-
// user-defined ones so the verifier recognizes the same set of named
|
|
47
|
-
// registries the resolver does; otherwise a package resolved via `gh:`
|
|
48
|
-
// would land in the lockfile with a tarball URL the verifier can't route.
|
|
49
|
-
const namedRegistryPrefixes = Object.values({
|
|
50
|
-
...BUILTIN_NAMED_REGISTRIES,
|
|
51
|
-
...(opts.namedRegistries ?? {}),
|
|
52
|
-
})
|
|
53
|
-
.map((url) => {
|
|
54
|
-
const parsed = tryParseUrl(url);
|
|
55
|
-
if (!parsed)
|
|
56
|
-
return null;
|
|
57
|
-
// Ensure trailing slash so prefix matching against tarball URLs (which
|
|
58
|
-
// always include the package path under the registry root) does not
|
|
59
|
-
// accidentally match a sibling registry whose URL shares a prefix string.
|
|
60
|
-
const pathname = parsed.pathname.endsWith('/') ? parsed.pathname : `${parsed.pathname}/`;
|
|
61
|
-
return `${parsed.origin}${pathname}`;
|
|
62
|
-
})
|
|
63
|
-
.filter((value) => value != null)
|
|
64
|
-
.sort((a, b) => b.length - a.length);
|
|
65
|
-
// Per-install dedup of every network/disk fetch the verifier issues.
|
|
66
|
-
// The maturity check uses the layered `fetchPublishedAt` lookup; the
|
|
67
|
-
// trust check uses an attestation fast-path before falling back to
|
|
68
|
-
// the same full-metadata mirror. All maps live here so verifying
|
|
69
|
-
// many versions of the same package only pays the disk/network costs
|
|
70
|
-
// once. The on-disk conditional-GET cache is handled inside
|
|
71
|
-
// fetch{Abbreviated,Full}MetadataCached via the resolver's shared
|
|
72
|
-
// mirrors at opts.cacheDir.
|
|
73
|
-
const lookupContext = {
|
|
74
|
-
fetchOpts: opts.fetchOpts,
|
|
75
|
-
getAuthHeaderValueByURI: opts.getAuthHeaderValueByURI,
|
|
76
|
-
cacheDir: opts.cacheDir,
|
|
77
|
-
cutoffMs: cutoff,
|
|
78
|
-
sharedMetaCache: opts.metaCache,
|
|
79
|
-
abbreviatedMetaCache: new Map(),
|
|
80
|
-
publishedAtCache: new Map(),
|
|
81
|
-
localMetaCache: new Map(),
|
|
82
|
-
fullMetaCache: new Map(),
|
|
83
|
-
fullMetaForTrustCache: new Map(),
|
|
84
|
-
};
|
|
85
|
-
const minimumReleaseAge = opts.minimumReleaseAge ?? 0;
|
|
86
|
-
const trustPolicy = opts.trustPolicy;
|
|
87
|
-
const trustPolicyIgnoreAfter = opts.trustPolicyIgnoreAfter;
|
|
88
|
-
const verify = async (resolution, { name, version, nonSemverVersion }) => {
|
|
89
|
-
if (!isRegistryTarballResolution(resolution))
|
|
90
|
-
return { ok: true };
|
|
91
|
-
// Network-free structural checks must run before registry metadata shortcuts.
|
|
92
|
-
const integrity = resolution.integrity;
|
|
93
|
-
if (typeof integrity !== 'string' || integrity.length === 0) {
|
|
94
|
-
return {
|
|
95
|
-
ok: false,
|
|
96
|
-
code: MISSING_TARBALL_INTEGRITY_VIOLATION_CODE,
|
|
97
|
-
reason: 'has no "integrity" field, so its downloaded tarball cannot be verified',
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
// URL/git-keyed entries are deliberate non-registry deps. They can still
|
|
101
|
-
// carry a semver `version` copied from the resolved manifest, so the
|
|
102
|
-
// semver guard below isn't enough on its own — the registry policies and
|
|
103
|
-
// the tarball-URL binding don't apply to them, and a registry lookup
|
|
104
|
-
// would 404.
|
|
105
|
-
if (nonSemverVersion != null)
|
|
106
|
-
return { ok: true };
|
|
107
|
-
if (!semver.valid(version)) {
|
|
108
|
-
return {
|
|
109
|
-
ok: false,
|
|
110
|
-
code: TARBALL_URL_MISMATCH_VIOLATION_CODE,
|
|
111
|
-
reason: `has a non-semver version ("${version}") and so cannot be verified against the registry's published metadata`,
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
const rawTarball = resolution.tarball;
|
|
115
|
-
if (rawTarball != null && typeof rawTarball !== 'string') {
|
|
116
|
-
return {
|
|
117
|
-
ok: false,
|
|
118
|
-
code: TARBALL_URL_MISMATCH_VIOLATION_CODE,
|
|
119
|
-
reason: 'has a non-string "tarball" field, so its URL cannot be verified',
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
const tarballUrl = typeof rawTarball === 'string' ? rawTarball : undefined;
|
|
123
|
-
const registry = pickRegistryForVersion(opts.registries, namedRegistryPrefixes, name, tarballUrl);
|
|
124
|
-
// A registry entry that pins an explicit tarball URL must point at the
|
|
125
|
-
// artifact the registry's own metadata lists. Otherwise a trusted
|
|
126
|
-
// `name@version` could front bytes from an attacker-chosen URL (with a
|
|
127
|
-
// matching integrity for those bytes). This binding is unconditional —
|
|
128
|
-
// it does not depend on `minimumReleaseAge`/`trustPolicy` and isn't
|
|
129
|
-
// narrowed by their exclude lists, since it guards integrity rather
|
|
130
|
-
// than maturity/trust. Registry entries with no tarball URL reconstruct
|
|
131
|
-
// it from name+version+registry, so they're inherently bound.
|
|
132
|
-
if (typeof tarballUrl === 'string') {
|
|
133
|
-
const urlViolation = await runTarballUrlCheck(lookupContext, registry, name, version, tarballUrl);
|
|
134
|
-
if (urlViolation)
|
|
135
|
-
return urlViolation;
|
|
136
|
-
}
|
|
137
|
-
const ageApplies = ageCheckActive && !isExcluded(excludePolicy, name, version);
|
|
138
|
-
const trustApplies = trustCheckActive && !isExcluded(trustExcludePolicy, name, version);
|
|
139
|
-
if (!ageApplies && !trustApplies)
|
|
140
|
-
return { ok: true };
|
|
141
|
-
if (ageApplies) {
|
|
142
|
-
const ageViolation = await runAgeCheck(lookupContext, registry, name, version, cutoff, opts.ignoreMissingTimeField === true);
|
|
143
|
-
if (ageViolation)
|
|
144
|
-
return ageViolation;
|
|
145
|
-
}
|
|
146
|
-
if (trustApplies) {
|
|
147
|
-
const trustViolation = await runTrustCheck(lookupContext, registry, name, version, {
|
|
148
|
-
trustPolicyExclude: trustExcludePolicy,
|
|
149
|
-
trustPolicyIgnoreAfter,
|
|
150
|
-
});
|
|
151
|
-
if (trustViolation)
|
|
152
|
-
return trustViolation;
|
|
153
|
-
}
|
|
154
|
-
return { ok: true };
|
|
155
|
-
};
|
|
156
|
-
// Snapshot the exclude lists (sorted, deduped) and require an exact
|
|
157
|
-
// match in `canTrustPastCheck`: cache identity == policy identity.
|
|
158
|
-
// Any change to either exclude list — adding, removing, or
|
|
159
|
-
// substituting an entry — invalidates the cached run. This is
|
|
160
|
-
// stricter than a pure correctness check would require (adding to
|
|
161
|
-
// either list is more permissive and the cached pass would still
|
|
162
|
-
// hold), but it makes the cache contract trivial to reason about and
|
|
163
|
-
// removes a class of bypasses where a previously-approved version
|
|
164
|
-
// stays trusted after its exclude entry has been pulled.
|
|
165
|
-
const sortedMinAgeExcludes = [...new Set(opts.minimumReleaseAgeExclude ?? [])].sort();
|
|
166
|
-
const sortedTrustExcludes = [...new Set(opts.trustPolicyExclude ?? [])].sort();
|
|
167
|
-
return {
|
|
168
|
-
verify,
|
|
169
|
-
policy: {
|
|
170
|
-
// Marks runs that enforced the tarball-URL binding. A cache record
|
|
171
|
-
// written before this rule existed lacks the flag, so
|
|
172
|
-
// `canTrustPastCheck` rejects it and forces a re-verification that
|
|
173
|
-
// applies the binding — otherwise an upgrade could keep trusting a
|
|
174
|
-
// lockfile that was only ever age/trust-checked.
|
|
175
|
-
tarballUrlBinding: true,
|
|
176
|
-
// Same cache identity rule for the missing-integrity structural check.
|
|
177
|
-
integrityRequired: true,
|
|
178
|
-
minimumReleaseAge,
|
|
179
|
-
minimumReleaseAgeExclude: sortedMinAgeExcludes,
|
|
180
|
-
trustPolicy: trustPolicy ?? null,
|
|
181
|
-
trustPolicyExclude: sortedTrustExcludes,
|
|
182
|
-
trustPolicyIgnoreAfter: trustPolicyIgnoreAfter ?? null,
|
|
183
|
-
},
|
|
184
|
-
canTrustPastCheck: (cached) => {
|
|
185
|
-
// The tarball-URL binding is unconditional today; a cached run that
|
|
186
|
-
// didn't record it can't be trusted to have enforced it.
|
|
187
|
-
if (cached.tarballUrlBinding !== true)
|
|
188
|
-
return false;
|
|
189
|
-
// The missing-integrity check is also unconditional; older cache records
|
|
190
|
-
// without the flag cannot prove they rejected unverifiable tarballs.
|
|
191
|
-
if (cached.integrityRequired !== true)
|
|
192
|
-
return false;
|
|
193
|
-
// Maturity: a previously cached run under a larger cutoff
|
|
194
|
-
// (stricter window) is trustworthy under a smaller current one —
|
|
195
|
-
// its set of accepted versions is a subset of today's. The
|
|
196
|
-
// reverse — tightening the cutoff — invalidates the cached run:
|
|
197
|
-
// versions that passed before may now be in-window. Non-number
|
|
198
|
-
// cached values come from an older record shape and aren't trusted.
|
|
199
|
-
const past = cached.minimumReleaseAge;
|
|
200
|
-
const pastNumber = typeof past === 'number' ? past : 0;
|
|
201
|
-
if (pastNumber < minimumReleaseAge)
|
|
202
|
-
return false;
|
|
203
|
-
// Excludes: today's sorted-deduped lists must match the cached
|
|
204
|
-
// ones byte for byte. Older records (no field) fall back to an
|
|
205
|
-
// empty array, so they only trust today's empty policy.
|
|
206
|
-
const pastMinAgeExcludes = Array.isArray(cached.minimumReleaseAgeExclude)
|
|
207
|
-
? cached.minimumReleaseAgeExclude
|
|
208
|
-
: [];
|
|
209
|
-
if (JSON.stringify(pastMinAgeExcludes) !== JSON.stringify(sortedMinAgeExcludes))
|
|
210
|
-
return false;
|
|
211
|
-
// Trust policy: any change to `trustPolicy`, the exclude list, or
|
|
212
|
-
// the ignore-after cutoff invalidates the cached run. Older
|
|
213
|
-
// records (no trust field at all) treat the trust policy as
|
|
214
|
-
// absent and are only trusted under an unset-today policy.
|
|
215
|
-
const pastTrustPolicy = cached.trustPolicy ?? null;
|
|
216
|
-
const todayTrustPolicy = trustPolicy ?? null;
|
|
217
|
-
if (pastTrustPolicy !== todayTrustPolicy)
|
|
218
|
-
return false;
|
|
219
|
-
const pastTrustExcludes = Array.isArray(cached.trustPolicyExclude)
|
|
220
|
-
? cached.trustPolicyExclude
|
|
221
|
-
: [];
|
|
222
|
-
if (JSON.stringify(pastTrustExcludes) !== JSON.stringify(sortedTrustExcludes))
|
|
223
|
-
return false;
|
|
224
|
-
const pastIgnoreAfter = typeof cached.trustPolicyIgnoreAfter === 'number'
|
|
225
|
-
? cached.trustPolicyIgnoreAfter
|
|
226
|
-
: null;
|
|
227
|
-
const todayIgnoreAfter = trustPolicyIgnoreAfter ?? null;
|
|
228
|
-
if (pastIgnoreAfter !== todayIgnoreAfter)
|
|
229
|
-
return false;
|
|
230
|
-
return true;
|
|
231
|
-
},
|
|
232
|
-
};
|
|
233
|
-
}
|
|
234
|
-
async function runAgeCheck(context, registry, name, version, cutoff, ignoreMissingTimeField) {
|
|
235
|
-
// A transport failure (auth/network/5xx) propagates the registry's own fetch
|
|
236
|
-
// error (e.g. ERR_PNPM_FETCH_403); the gate aborts the install with it rather
|
|
237
|
-
// than folding it into a policy violation. A successful fetch that simply
|
|
238
|
-
// lacks a publish timestamp for this version is handled below.
|
|
239
|
-
const published = await fetchPublishedAt(context, registry, name, version);
|
|
240
|
-
if (!published) {
|
|
241
|
-
// No source — attestation, local mirror, or full metadata —
|
|
242
|
-
// surfaced a publish timestamp for this version. The resolver's
|
|
243
|
-
// pickMatchingVersionFinal honors `minimumReleaseAgeIgnoreMissingTime`
|
|
244
|
-
// for the same shape (some self-hosted registries strip per-version
|
|
245
|
-
// `time`); the verifier mirrors that so it can't be stricter than
|
|
246
|
-
// fresh resolution. Without the flag we still fail closed — better
|
|
247
|
-
// a false reject than silent bypass when the user hasn't opted in.
|
|
248
|
-
if (ignoreMissingTimeField) {
|
|
249
|
-
warnMissingTimeFieldOnce(name);
|
|
250
|
-
return undefined;
|
|
251
|
-
}
|
|
252
|
-
return {
|
|
253
|
-
ok: false,
|
|
254
|
-
code: MINIMUM_RELEASE_AGE_VIOLATION_CODE,
|
|
255
|
-
reason: uncheckable('minimumReleaseAge', 'version not present in registry manifest'),
|
|
256
|
-
};
|
|
257
|
-
}
|
|
258
|
-
const publishedAt = new Date(published);
|
|
259
|
-
const ts = publishedAt.getTime();
|
|
260
|
-
if (Number.isNaN(ts)) {
|
|
261
|
-
return {
|
|
262
|
-
ok: false,
|
|
263
|
-
code: MINIMUM_RELEASE_AGE_VIOLATION_CODE,
|
|
264
|
-
reason: 'publish timestamp is not a valid date',
|
|
265
|
-
};
|
|
266
|
-
}
|
|
267
|
-
if (ts > cutoff) {
|
|
268
|
-
return {
|
|
269
|
-
ok: false,
|
|
270
|
-
code: MINIMUM_RELEASE_AGE_VIOLATION_CODE,
|
|
271
|
-
reason: `was published at ${publishedAt.toISOString()}, within the minimumReleaseAge cutoff (${new Date(cutoff).toISOString()})`,
|
|
272
|
-
};
|
|
273
|
-
}
|
|
274
|
-
return undefined;
|
|
275
|
-
}
|
|
276
|
-
/**
|
|
277
|
-
* Confirm the lockfile-pinned tarball URL is the artifact the registry's
|
|
278
|
-
* own metadata lists for this exact `name@version`.
|
|
279
|
-
*
|
|
280
|
-
* Fail-closed: the entry passes only when the registry metadata
|
|
281
|
-
* affirmatively lists this version with a matching tarball URL. If the
|
|
282
|
-
* metadata can't be fetched, doesn't list the version, or omits
|
|
283
|
-
* `dist.tarball`, the entry can't be confirmed and is rejected — otherwise
|
|
284
|
-
* a tampered lockfile could smuggle a malicious URL past the check by
|
|
285
|
-
* pointing it at a `name@version` the registry can't vouch for.
|
|
286
|
-
*/
|
|
287
|
-
async function runTarballUrlCheck(context, registry, name, version, lockfileTarball) {
|
|
288
|
-
const { meta, error } = await fetchAbbreviatedMeta(context, registry, name);
|
|
289
|
-
if (error != null) {
|
|
290
|
-
// Couldn't reach the registry to verify (auth/network/5xx). Propagate the
|
|
291
|
-
// registry's own fetch error (e.g. ERR_PNPM_FETCH_403, which already
|
|
292
|
-
// explains the auth situation) instead of mislabeling a transport failure
|
|
293
|
-
// as a tampering-style URL mismatch. The gate aborts the install with that
|
|
294
|
-
// error — still fail-closed, the entry never reaches the filesystem.
|
|
295
|
-
throw error;
|
|
296
|
-
}
|
|
297
|
-
const registryTarball = meta?.versionTarballs?.get(version);
|
|
298
|
-
if (registryTarball != null && sameTarballUrl(lockfileTarball, registryTarball)) {
|
|
299
|
-
return undefined;
|
|
300
|
-
}
|
|
301
|
-
return {
|
|
302
|
-
ok: false,
|
|
303
|
-
code: TARBALL_URL_MISMATCH_VIOLATION_CODE,
|
|
304
|
-
reason: registryTarball == null
|
|
305
|
-
? "could not be verified against the registry's published metadata"
|
|
306
|
-
: `has a tarball URL (${lockfileTarball}) that does not match the registry's published metadata (${registryTarball})`,
|
|
307
|
-
};
|
|
308
|
-
}
|
|
309
|
-
function sameTarballUrl(a, b) {
|
|
310
|
-
return canonicalTarballUrl(a) === canonicalTarballUrl(b);
|
|
311
|
-
}
|
|
312
|
-
// Mirror the tolerance toLockfileResolution applies when it decides whether
|
|
313
|
-
// a tarball URL is "the expected one": ignore the protocol and `%2f` scope
|
|
314
|
-
// encoding so a benign http/https or encoding difference isn't read as
|
|
315
|
-
// tampering. The `%2f` match is case-insensitive because `normalizeRegistryUrl`
|
|
316
|
-
// (`new URL().toString()`) can upper-case percent-escapes to `%2F`.
|
|
317
|
-
function canonicalTarballUrl(url) {
|
|
318
|
-
const normalized = normalizeRegistryUrl(url).replace(/%2f/gi, '/');
|
|
319
|
-
const schemeEnd = normalized.indexOf('://');
|
|
320
|
-
return schemeEnd === -1 ? normalized : normalized.slice(schemeEnd + 3);
|
|
321
|
-
}
|
|
322
|
-
/**
|
|
323
|
-
* Run the resolver-time `failIfTrustDowngraded` check against the
|
|
324
|
-
* pinned lockfile version. The packument is fetched through a
|
|
325
|
-
* per-install cache so multiple versions of the same package share
|
|
326
|
-
* one fetch.
|
|
327
|
-
*
|
|
328
|
-
* No attestation fast-path here even though the per-version
|
|
329
|
-
* attestation endpoint is cheaper than the packument: presence of
|
|
330
|
-
* provenance on the current version is not sufficient to clear a
|
|
331
|
-
* downgrade. A package could have shipped earlier versions under a
|
|
332
|
-
* `trustedPublisher` with provenance (the higher-rank evidence) and
|
|
333
|
-
* then dropped back to plain provenance for the version we're verifying —
|
|
334
|
-
* `failIfTrustDowngraded` correctly flags that, and a "has any
|
|
335
|
-
* attestation → pass" shortcut would silently miss it.
|
|
336
|
-
*/
|
|
337
|
-
async function runTrustCheck(context, registry, name, version, opts) {
|
|
338
|
-
// A transport failure (auth/network/5xx) propagates the registry's own fetch
|
|
339
|
-
// error; the gate aborts the install with it rather than folding it into a
|
|
340
|
-
// policy violation. Still fail-closed: a missing manifest can't be mistaken
|
|
341
|
-
// for a passing trust check because the install never proceeds.
|
|
342
|
-
const meta = await fetchFullMetaForTrust(context, registry, name);
|
|
343
|
-
try {
|
|
344
|
-
failIfTrustDowngraded(meta, version, opts);
|
|
345
|
-
}
|
|
346
|
-
catch (err) {
|
|
347
|
-
return {
|
|
348
|
-
ok: false,
|
|
349
|
-
code: TRUST_DOWNGRADE_VIOLATION_CODE,
|
|
350
|
-
reason: err instanceof Error ? err.message : String(err),
|
|
351
|
-
};
|
|
352
|
-
}
|
|
353
|
-
return undefined;
|
|
354
|
-
}
|
|
355
|
-
function fetchFullMetaForTrust(context, registry, name) {
|
|
356
|
-
const cacheKey = `${registry}\x00${name}`;
|
|
357
|
-
let cachedPromise = context.fullMetaForTrustCache.get(cacheKey);
|
|
358
|
-
if (cachedPromise == null) {
|
|
359
|
-
// Fast path: if the resolver already upgraded to full meta for this
|
|
360
|
-
// (registry, name) during the same install (e.g. minimumReleaseAge
|
|
361
|
-
// active), reuse that document. Abbreviated meta is rejected here —
|
|
362
|
-
// it lacks per-version `time` and per-version trust evidence, both
|
|
363
|
-
// required by failIfTrustDowngraded. The read is registry-qualified
|
|
364
|
-
// (see `getPkgMetaCacheKey`), so a package of the same name served by
|
|
365
|
-
// a different registry can't be returned here.
|
|
366
|
-
const shared = readSharedMetaForTrust(context.sharedMetaCache, registry, name);
|
|
367
|
-
if (shared != null) {
|
|
368
|
-
cachedPromise = Promise.resolve(projectTrustMeta(shared));
|
|
369
|
-
}
|
|
370
|
-
else {
|
|
371
|
-
// Don't swallow the fetch rejection here — `runTrustCheck` catches it
|
|
372
|
-
// and surfaces the underlying message in the violation reason, which
|
|
373
|
-
// is more actionable than the generic "metadata is unavailable" the
|
|
374
|
-
// `!meta` fallback emits. The cache still holds the rejected promise
|
|
375
|
-
// so repeat verifier calls for the same (registry, name) within one
|
|
376
|
-
// install don't refetch a known-failing endpoint.
|
|
377
|
-
//
|
|
378
|
-
// The fetched packument is projected down to just the trust-relevant
|
|
379
|
-
// fields (per-version `_npmUser.trustedPublisher` and
|
|
380
|
-
// `dist.attestations.provenance`, plus the package-level `time` map)
|
|
381
|
-
// before being stored. The full document — dependency maps, scripts,
|
|
382
|
-
// READMEs for every version — would otherwise stay resident in this
|
|
383
|
-
// map for the entire install, which on multi-thousand-entry
|
|
384
|
-
// workspaces OOMs CI runners with a 2GB heap (see #11860).
|
|
385
|
-
cachedPromise = fetchFullMetadataCached(context.fetchOpts, name, {
|
|
386
|
-
registry,
|
|
387
|
-
authHeaderValue: context.getAuthHeaderValueByURI(registry, { pkgName: name }),
|
|
388
|
-
cacheDir: context.cacheDir,
|
|
389
|
-
}).then(projectTrustMeta);
|
|
390
|
-
}
|
|
391
|
-
context.fullMetaForTrustCache.set(cacheKey, cachedPromise);
|
|
392
|
-
}
|
|
393
|
-
return cachedPromise;
|
|
394
|
-
}
|
|
395
|
-
// Project the full packument to a minimal `PackageMeta`-shaped view
|
|
396
|
-
// that exposes only the fields `failIfTrustDowngraded` reads:
|
|
397
|
-
// • `name` and `modified` for error messages and cache keys
|
|
398
|
-
// • `time` for the per-version publish-date walk
|
|
399
|
-
// • `versions[v]._npmUser.trustedPublisher`
|
|
400
|
-
// • `versions[v].dist.attestations.provenance`
|
|
401
|
-
// The shape is still a valid `PackageMeta` so the downstream consumer
|
|
402
|
-
// doesn't have to special-case it — only the bulk fields (dependency
|
|
403
|
-
// graph, scripts, README, etc.) are dropped.
|
|
404
|
-
function projectTrustMeta(meta) {
|
|
405
|
-
const versions = {};
|
|
406
|
-
for (const [version, manifest] of Object.entries(meta.versions ?? {})) {
|
|
407
|
-
versions[version] = projectTrustManifest(manifest);
|
|
408
|
-
}
|
|
409
|
-
return {
|
|
410
|
-
name: meta.name,
|
|
411
|
-
'dist-tags': {},
|
|
412
|
-
versions,
|
|
413
|
-
time: meta.time,
|
|
414
|
-
modified: meta.modified,
|
|
415
|
-
etag: meta.etag,
|
|
416
|
-
};
|
|
417
|
-
}
|
|
418
|
-
function projectTrustManifest(manifest) {
|
|
419
|
-
// Drop everything except the trust-evidence fields. `PackageInRegistry.dist`
|
|
420
|
-
// is typed as requiring `shasum` and `tarball`, but the trust check never
|
|
421
|
-
// reads them; cast away the unsoundness so callers see the same nominal
|
|
422
|
-
// shape without the per-version dependency graph / scripts / README bulk
|
|
423
|
-
// carrying through. `_npmUser` is similarly narrowed to just
|
|
424
|
-
// `trustedPublisher` and `approver` — the only sub-fields the trust check
|
|
425
|
-
// inspects — so we don't keep maintainer name/email PII resident in the
|
|
426
|
-
// cache.
|
|
427
|
-
const approver = manifest._npmUser?.approver;
|
|
428
|
-
const trustedPublisher = manifest._npmUser?.trustedPublisher;
|
|
429
|
-
const provenance = manifest.dist?.attestations?.provenance;
|
|
430
|
-
let npmUser = undefined;
|
|
431
|
-
if (approver) {
|
|
432
|
-
npmUser ||= {};
|
|
433
|
-
npmUser.approver = {};
|
|
434
|
-
}
|
|
435
|
-
if (trustedPublisher) {
|
|
436
|
-
npmUser ||= {};
|
|
437
|
-
npmUser.trustedPublisher = trustedPublisher;
|
|
438
|
-
}
|
|
439
|
-
return {
|
|
440
|
-
_npmUser: npmUser,
|
|
441
|
-
dist: provenance != null
|
|
442
|
-
? { attestations: { provenance } }
|
|
443
|
-
: undefined,
|
|
444
|
-
};
|
|
445
|
-
}
|
|
446
|
-
/**
|
|
447
|
-
* Per-(registry, name, version) lookup with a layered fallback:
|
|
448
|
-
*
|
|
449
|
-
* 1. **Abbreviated metadata `modified` shortcut.** This is what the
|
|
450
|
-
* resolver already fetches by default; it's a small document with
|
|
451
|
-
* a package-level last-modified time but no per-version timestamps.
|
|
452
|
-
* If `modified` is older than the policy cutoff, every version in
|
|
453
|
-
* this package was published at least that long ago — return the
|
|
454
|
-
* `modified` timestamp as a conservative upper bound and skip the
|
|
455
|
-
* rest of the chain. Costs one conditional GET that the resolver
|
|
456
|
-
* has usually already paid for.
|
|
457
|
-
* 2. **On-disk full-metadata mirror.** If a previous verification
|
|
458
|
-
* populated `FULL_META_DIR`, take the per-version timestamp from
|
|
459
|
-
* there.
|
|
460
|
-
* 3. **npm attestation endpoint.** Small payload, just this version's
|
|
461
|
-
* Sigstore-anchored timestamp. Wins on cold cache when the package
|
|
462
|
-
* was published with provenance.
|
|
463
|
-
* 4. **Full metadata fetch.** Last resort — only paid when the
|
|
464
|
-
* abbreviated shortcut can't decide, the local full mirror is
|
|
465
|
-
* cold, and there's no attestation.
|
|
466
|
-
*/
|
|
467
|
-
async function fetchPublishedAt(context, registry, name, version) {
|
|
468
|
-
const cacheKey = `${registry}\x00${name}\x00${version}`;
|
|
469
|
-
let cachedPromise = context.publishedAtCache.get(cacheKey);
|
|
470
|
-
if (cachedPromise == null) {
|
|
471
|
-
cachedPromise = resolvePublishedAt(context, registry, name, version);
|
|
472
|
-
context.publishedAtCache.set(cacheKey, cachedPromise);
|
|
473
|
-
}
|
|
474
|
-
return cachedPromise;
|
|
475
|
-
}
|
|
476
|
-
async function resolvePublishedAt(context, registry, name, version) {
|
|
477
|
-
const abbreviatedShortcut = await tryAbbreviatedModifiedShortcut(context, registry, name, version);
|
|
478
|
-
if (abbreviatedShortcut != null)
|
|
479
|
-
return abbreviatedShortcut;
|
|
480
|
-
const localTime = await readLocalMetaTime(context, registry, name);
|
|
481
|
-
if (localTime?.[version])
|
|
482
|
-
return localTime[version];
|
|
483
|
-
const attestationTime = await fetchAttestationPublishedAt(context.fetchOpts, name, version, {
|
|
484
|
-
registry,
|
|
485
|
-
authHeaderValue: context.getAuthHeaderValueByURI(registry, { pkgName: name }),
|
|
486
|
-
});
|
|
487
|
-
if (attestationTime != null)
|
|
488
|
-
return attestationTime;
|
|
489
|
-
const fullMetaTime = await fetchFullMetaTime(context, registry, name);
|
|
490
|
-
return fullMetaTime?.[version];
|
|
491
|
-
}
|
|
492
|
-
/**
|
|
493
|
-
* Returns the abbreviated metadata's `modified` timestamp **iff** it
|
|
494
|
-
* proves the gate would pass — i.e. modified is strictly older than
|
|
495
|
-
* the policy cutoff *and* the pinned version still exists in the
|
|
496
|
-
* package's current versions map.
|
|
497
|
-
*
|
|
498
|
-
* The version check is the fail-closed contract: an unpublished or
|
|
499
|
-
* never-published version must not slip through on the package-level
|
|
500
|
-
* `modified` timestamp. When the version is missing here we fall
|
|
501
|
-
* through to the later layers so the caller eventually surfaces the
|
|
502
|
-
* "version not present in registry manifest" violation.
|
|
503
|
-
*
|
|
504
|
-
* Returns `undefined` otherwise (modified is too recent, the metadata
|
|
505
|
-
* lacks a parseable modified field, the version isn't in the abbreviated
|
|
506
|
-
* form, or the fetch failed) and the caller proceeds with per-version
|
|
507
|
-
* lookups.
|
|
508
|
-
*/
|
|
509
|
-
async function tryAbbreviatedModifiedShortcut(context, registry, name, version) {
|
|
510
|
-
// A fetch failure here is fine: ignore `error` and fall back to per-version
|
|
511
|
-
// lookups, the same as a successful-but-uninformative metadata response.
|
|
512
|
-
const { meta } = await fetchAbbreviatedMeta(context, registry, name);
|
|
513
|
-
const modified = meta?.modified;
|
|
514
|
-
if (typeof modified !== 'string')
|
|
515
|
-
return undefined;
|
|
516
|
-
const modifiedMs = Date.parse(modified);
|
|
517
|
-
if (Number.isNaN(modifiedMs))
|
|
518
|
-
return undefined;
|
|
519
|
-
if (modifiedMs >= context.cutoffMs)
|
|
520
|
-
return undefined;
|
|
521
|
-
// The shortcut treats `modified` as an upper bound on every version's
|
|
522
|
-
// publish time — but only for versions the registry currently lists.
|
|
523
|
-
// An unpublished or never-published pin would otherwise pass the gate
|
|
524
|
-
// on a stale package-level timestamp.
|
|
525
|
-
if (!meta?.versionTarballs?.has(version))
|
|
526
|
-
return undefined;
|
|
527
|
-
return modified;
|
|
528
|
-
}
|
|
529
|
-
function fetchAbbreviatedMeta(context, registry, name) {
|
|
530
|
-
const cacheKey = `${registry}\x00${name}`;
|
|
531
|
-
let cachedPromise = context.abbreviatedMetaCache.get(cacheKey);
|
|
532
|
-
if (cachedPromise == null) {
|
|
533
|
-
// Fast path: the resolver's per-install LRU already holds this
|
|
534
|
-
// packument from its own pickPackage pass — abbreviated or full.
|
|
535
|
-
// Project it for the shortcut and skip the disk/network round-trip.
|
|
536
|
-
// The read is registry-qualified (see `getPkgMetaCacheKey`), so it
|
|
537
|
-
// can only return this registry's own packument.
|
|
538
|
-
const shared = readSharedMeta(context.sharedMetaCache, registry, name);
|
|
539
|
-
if (shared != null) {
|
|
540
|
-
cachedPromise = Promise.resolve({ meta: projectAbbreviatedMeta(shared) });
|
|
541
|
-
}
|
|
542
|
-
else {
|
|
543
|
-
// Carry a fetch failure (auth/network/5xx) as `error` instead of
|
|
544
|
-
// collapsing it to `undefined`: the tarball-URL check rethrows it (so the
|
|
545
|
-
// registry's own error surfaces, not a tampering-style mismatch) while
|
|
546
|
-
// the age shortcut ignores it and falls back to per-version lookups.
|
|
547
|
-
// Keeping it a resolved value — not a rejected promise — lets the two
|
|
548
|
-
// callers share one cached promise without an unhandled rejection.
|
|
549
|
-
cachedPromise = fetchAbbreviatedMetadataCached(context.fetchOpts, name, {
|
|
550
|
-
registry,
|
|
551
|
-
authHeaderValue: context.getAuthHeaderValueByURI(registry, { pkgName: name }),
|
|
552
|
-
cacheDir: context.cacheDir,
|
|
553
|
-
}).then((meta) => ({ meta: projectAbbreviatedMeta(meta) }), (error) => ({ error }));
|
|
554
|
-
}
|
|
555
|
-
context.abbreviatedMetaCache.set(cacheKey, cachedPromise);
|
|
556
|
-
}
|
|
557
|
-
return cachedPromise;
|
|
558
|
-
}
|
|
559
|
-
function readSharedMeta(cache, registry, name) {
|
|
560
|
-
if (cache == null)
|
|
561
|
-
return undefined;
|
|
562
|
-
// Prefer a full entry — it carries every field the abbreviated form
|
|
563
|
-
// does, plus `time` and per-version trust evidence the trust check
|
|
564
|
-
// needs. The resolver only populates a full key when the install ran
|
|
565
|
-
// with `minimumReleaseAge` configured, otherwise the bare key holds
|
|
566
|
-
// the abbreviated form.
|
|
567
|
-
return readSharedFullMeta(cache, registry, name) ??
|
|
568
|
-
validateSharedMeta(cache.get(getPkgMetaCacheKey(registry, name, false, false)), name);
|
|
569
|
-
}
|
|
570
|
-
function readSharedMetaForTrust(cache, registry, name) {
|
|
571
|
-
if (cache == null)
|
|
572
|
-
return undefined;
|
|
573
|
-
// Abbreviated meta is rejected for the trust check — it lacks
|
|
574
|
-
// per-version `time` and per-version trust evidence.
|
|
575
|
-
return readSharedFullMeta(cache, registry, name);
|
|
576
|
-
}
|
|
577
|
-
// The resolver keys full metadata as either filtered or unfiltered
|
|
578
|
-
// depending on its own `filterMetadata` setting; the verifier doesn't
|
|
579
|
-
// know which, and a filtered full packument keeps everything the
|
|
580
|
-
// verifier reads (`time`, per-version `_npmUser`, `dist`), so try both.
|
|
581
|
-
function readSharedFullMeta(cache, registry, name) {
|
|
582
|
-
return validateSharedMeta(cache.get(getPkgMetaCacheKey(registry, name, true, false)), name) ??
|
|
583
|
-
validateSharedMeta(cache.get(getPkgMetaCacheKey(registry, name, true, true)), name);
|
|
584
|
-
}
|
|
585
|
-
// Defensive guard against the resolver's `metaCache` returning an
|
|
586
|
-
// unexpected entry. The cache key is registry-qualified (see
|
|
587
|
-
// `getPkgMetaCacheKey`), so a package of the same name from another
|
|
588
|
-
// registry can't be returned; this name check catches accidental
|
|
589
|
-
// returns of a different package (cache corruption, factory misuse)
|
|
590
|
-
// rather than silently feeding wrong data to the trust / age check.
|
|
591
|
-
function validateSharedMeta(meta, name) {
|
|
592
|
-
if (meta == null)
|
|
593
|
-
return undefined;
|
|
594
|
-
if (meta.name !== name)
|
|
595
|
-
return undefined;
|
|
596
|
-
return meta;
|
|
597
|
-
}
|
|
598
|
-
// Project the abbreviated packument down to the few fields the verifier
|
|
599
|
-
// actually reads — package-level `modified`, plus a per-version map of
|
|
600
|
-
// `dist.tarball` (whose keys double as the version-existence set for the
|
|
601
|
-
// `tryAbbreviatedModifiedShortcut` check and the tarball-URL binding). The
|
|
602
|
-
// resolver populates the abbreviated mirror with every version's
|
|
603
|
-
// dependency / engine / dist info, which can run to hundreds of KB per
|
|
604
|
-
// package and accumulate to many GB across a multi-thousand-entry
|
|
605
|
-
// lockfile (see #11860). The full document is GC-able as soon as this
|
|
606
|
-
// closure returns; only the short tarball-URL strings are retained.
|
|
607
|
-
function projectAbbreviatedMeta(meta) {
|
|
608
|
-
let versionTarballs;
|
|
609
|
-
if (meta.versions) {
|
|
610
|
-
versionTarballs = new Map();
|
|
611
|
-
for (const [version, manifest] of Object.entries(meta.versions)) {
|
|
612
|
-
versionTarballs.set(version, manifest.dist?.tarball);
|
|
613
|
-
}
|
|
614
|
-
}
|
|
615
|
-
return {
|
|
616
|
-
modified: meta.modified,
|
|
617
|
-
versionTarballs,
|
|
618
|
-
};
|
|
619
|
-
}
|
|
620
|
-
function readLocalMetaTime(context, registry, name) {
|
|
621
|
-
if (!context.cacheDir)
|
|
622
|
-
return Promise.resolve(undefined);
|
|
623
|
-
const cacheKey = `${registry}\x00${name}`;
|
|
624
|
-
let cachedPromise = context.localMetaCache.get(cacheKey);
|
|
625
|
-
if (cachedPromise == null) {
|
|
626
|
-
cachedPromise = loadLocalMetaTime(context.cacheDir, registry, name);
|
|
627
|
-
context.localMetaCache.set(cacheKey, cachedPromise);
|
|
628
|
-
}
|
|
629
|
-
return cachedPromise;
|
|
630
|
-
}
|
|
631
|
-
async function loadLocalMetaTime(cacheDir, registry, name) {
|
|
632
|
-
const pkgMirror = getPkgMirrorPath(cacheDir, FULL_META_DIR, registry, name);
|
|
633
|
-
const cached = await loadMeta(pkgMirror);
|
|
634
|
-
return cached?.time;
|
|
635
|
-
}
|
|
636
|
-
function fetchFullMetaTime(context, registry, name) {
|
|
637
|
-
const cacheKey = `${registry}\x00${name}`;
|
|
638
|
-
let cachedPromise = context.fullMetaCache.get(cacheKey);
|
|
639
|
-
if (cachedPromise == null) {
|
|
640
|
-
cachedPromise = fetchFullMetadataCached(context.fetchOpts, name, {
|
|
641
|
-
registry,
|
|
642
|
-
authHeaderValue: context.getAuthHeaderValueByURI(registry, { pkgName: name }),
|
|
643
|
-
cacheDir: context.cacheDir,
|
|
644
|
-
}).then((meta) => meta.time);
|
|
645
|
-
context.fullMetaCache.set(cacheKey, cachedPromise);
|
|
646
|
-
}
|
|
647
|
-
return cachedPromise;
|
|
648
|
-
}
|
|
649
|
-
function pickRegistryForVersion(registries, namedRegistryPrefixes, name, tarballUrl) {
|
|
650
|
-
// If the lockfile records where the tarball lives, prefer that — scope
|
|
651
|
-
// routing (`@scope:registry`) only covers scoped packages, but named
|
|
652
|
-
// registries (`gh:`, `jsr:` aliases, custom) ship un-scoped packages whose
|
|
653
|
-
// origin we'd otherwise miss. Match the longest prefix so that two named
|
|
654
|
-
// registries sharing a host but differing by path don't collide.
|
|
655
|
-
if (tarballUrl) {
|
|
656
|
-
// Match on the same canonical form the tarball comparison uses, so a
|
|
657
|
-
// named-registry tarball that differs from the configured base only by
|
|
658
|
-
// scheme or `%2f` encoding still routes to its registry instead of
|
|
659
|
-
// falling back (and then failing closed against the wrong packument).
|
|
660
|
-
const normalized = canonicalTarballUrl(tarballUrl);
|
|
661
|
-
for (const prefix of namedRegistryPrefixes) {
|
|
662
|
-
if (normalized.startsWith(canonicalTarballUrl(prefix)))
|
|
663
|
-
return prefix;
|
|
664
|
-
}
|
|
665
|
-
}
|
|
666
|
-
return pickRegistryForPackage(registries, name);
|
|
667
|
-
}
|
|
668
|
-
function tryParseUrl(url) {
|
|
669
|
-
try {
|
|
670
|
-
return new URL(url);
|
|
671
|
-
}
|
|
672
|
-
catch {
|
|
673
|
-
return null;
|
|
674
|
-
}
|
|
675
|
-
}
|
|
676
|
-
function uncheckable(policy, why) {
|
|
677
|
-
return `could not be checked against ${policy} (${why})`;
|
|
678
|
-
}
|
|
679
|
-
function createExcludePolicy(patterns, key) {
|
|
680
|
-
// Mirror the wrapping done by the full-resolution path
|
|
681
|
-
// (installing/deps-resolver/src/resolveDependencyTree.ts) so the error
|
|
682
|
-
// code is identical regardless of which path surfaced the invalid pattern.
|
|
683
|
-
try {
|
|
684
|
-
return createPackageVersionPolicy(patterns);
|
|
685
|
-
}
|
|
686
|
-
catch (err) {
|
|
687
|
-
if (!err || typeof err !== 'object' || !('message' in err))
|
|
688
|
-
throw err;
|
|
689
|
-
throw new PnpmError(`INVALID_${key.replace(/([A-Z])/g, '_$1').toUpperCase()}`, `Invalid value in ${key}: ${err.message}`);
|
|
690
|
-
}
|
|
691
|
-
}
|
|
692
|
-
function isExcluded(policy, name, version) {
|
|
693
|
-
if (!policy)
|
|
694
|
-
return false;
|
|
695
|
-
const result = policy(name);
|
|
696
|
-
if (result === true)
|
|
697
|
-
return true;
|
|
698
|
-
if (Array.isArray(result) && result.includes(version))
|
|
699
|
-
return true;
|
|
700
|
-
return false;
|
|
701
|
-
}
|
|
702
|
-
function isRegistryTarballResolution(resolution) {
|
|
703
|
-
if (resolution == null || typeof resolution !== 'object')
|
|
704
|
-
return false;
|
|
705
|
-
// Only plain tarball resolutions (npm registry / named registries) have no
|
|
706
|
-
// `type` field. Git / directory / binary / custom resolutions all carry one.
|
|
707
|
-
if ('type' in resolution && resolution.type != null)
|
|
708
|
-
return false;
|
|
709
|
-
const tarball = resolution.tarball;
|
|
710
|
-
if (typeof tarball === 'string') {
|
|
711
|
-
// Git-hosted tarballs (codeload/gitlab/bitbucket) are special-cased in
|
|
712
|
-
// the resolver and aren't subject to registry policy.
|
|
713
|
-
if (isGitHostedTarballUrl(tarball))
|
|
714
|
-
return false;
|
|
715
|
-
// Local/non-registry tarballs (for example `file:`) have no packument
|
|
716
|
-
// metadata, so minimumReleaseAge/trustPolicy verification cannot apply.
|
|
717
|
-
const protocol = tryParseUrl(tarball)?.protocol;
|
|
718
|
-
if (protocol != null && protocol !== 'http:' && protocol !== 'https:')
|
|
719
|
-
return false;
|
|
720
|
-
}
|
|
721
|
-
// Canonical registry entries may omit both `tarball` and `integrity`.
|
|
722
|
-
return true;
|
|
723
|
-
}
|
|
724
|
-
//# sourceMappingURL=createNpmResolutionVerifier.js.map
|