@pnpm/resolving.npm-resolver 1103.2.1 → 1104.1.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/CHANGELOG.md +93 -0
- package/lib/createNpmResolutionVerifier.d.ts +13 -10
- package/lib/createNpmResolutionVerifier.js +171 -45
- package/lib/fetch.js +5 -3
- package/lib/index.d.ts +21 -7
- package/lib/index.js +224 -38
- package/lib/parseBareSpecifier.d.ts +2 -1
- package/lib/parseBareSpecifier.js +31 -7
- package/lib/pickPackage.d.ts +20 -1
- package/lib/pickPackage.js +100 -13
- package/lib/pickPackageFromMeta.d.ts +7 -1
- package/lib/pickPackageFromMeta.js +82 -3
- package/lib/publishTimes.d.ts +24 -0
- package/lib/publishTimes.js +35 -0
- package/lib/trustChecks.d.ts +14 -0
- package/lib/trustChecks.js +5 -0
- package/lib/violationCodes.d.ts +1 -0
- package/lib/violationCodes.js +1 -0
- package/package.json +22 -21
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,98 @@
|
|
|
1
1
|
# @pnpm/npm-resolver
|
|
2
2
|
|
|
3
|
+
## 1104.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Added explicit registry revision selection with `<version>+rN` and `pnpm update --patches` for refreshing revision artifacts without changing package versions. Registry-backed lockfile policy checks recognize historical revisions, and pnpr now preserves safe revision histories from upstream registries.
|
|
8
|
+
|
|
9
|
+
- Added support for registry replacement tarballs using standard integrity values, explicit revision fields, registry routing from the `registries` setting, non-redirecting integrity-addressed URLs, canonical safe-integer revision numbers, and pnpr proxying for immutable upstream revision artifacts.
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- Updated dependencies:
|
|
14
|
+
- @pnpm/config.normalize-registries@1101.0.1
|
|
15
|
+
- @pnpm/config.pick-registry-for-package@1101.0.1
|
|
16
|
+
- @pnpm/config.version-policy@1100.2.2
|
|
17
|
+
- @pnpm/core-loggers@1100.3.4
|
|
18
|
+
- @pnpm/crypto.hash@1100.0.3
|
|
19
|
+
- @pnpm/deps.path@1101.0.1
|
|
20
|
+
- @pnpm/fs.graceful-fs@1100.2.0
|
|
21
|
+
- @pnpm/pkg-manifest.utils@1100.4.2
|
|
22
|
+
- @pnpm/resolving.registry.pkg-metadata-filter@1100.0.18
|
|
23
|
+
- @pnpm/resolving.registry.types@1100.2.0
|
|
24
|
+
- @pnpm/resolving.resolver-base@1101.2.0
|
|
25
|
+
- @pnpm/resolving.tarball-url@1101.1.0
|
|
26
|
+
- @pnpm/store.cafs@1100.3.0
|
|
27
|
+
- @pnpm/store.index@1100.3.0
|
|
28
|
+
- @pnpm/types@1102.1.0
|
|
29
|
+
|
|
30
|
+
## 1104.0.0
|
|
31
|
+
|
|
32
|
+
### Major Changes
|
|
33
|
+
|
|
34
|
+
- The three registry lookups are now named for what they are keyed by, so that none of them is called `registries` — a name the `registries` setting itself has taken:
|
|
35
|
+
|
|
36
|
+
| before | after |
|
|
37
|
+
|---|---|
|
|
38
|
+
| `Config.registries` | `Config.registriesByScope` |
|
|
39
|
+
| `Config.namedRegistries` | `Config.registriesByPrefix` |
|
|
40
|
+
| `Config.registryOptions` | `Config.registryOptionsByUrl` |
|
|
41
|
+
|
|
42
|
+
The same rename applies to the `RegistryContext` fields, the `Registries` and `NamedRegistries` types (now `RegistriesByScope` and `RegistriesByPrefix`), `normalizeRegistries` / `normalizeNamedRegistries` (now `normalizeRegistriesByScope` / `normalizeRegistriesByPrefix`), and the `BUILTIN_NAMED_REGISTRIES` constant (now `BUILTIN_REGISTRIES_BY_PREFIX`).
|
|
43
|
+
|
|
44
|
+
This is an internal rename: no setting, error code, lockfile field, or `.pnpmfile.cjs` hook field changes. A `preResolution` hook still reads `ctx.registries`, which is the name pacquet passes as well. The `registries` and `namedRegistries` settings are read under the names users write them.
|
|
45
|
+
|
|
46
|
+
The pnpr resolve request sends `registriesByPrefix` where it sent `namedRegistries`. A pnpr server and its clients must be on matching versions, which is already the case for an experimental server.
|
|
47
|
+
|
|
48
|
+
### Minor Changes
|
|
49
|
+
|
|
50
|
+
- A registry can now declare that its abbreviated metadata carries the `time` field, so `resolutionMode: time-based` reads the full metadata document only from the registries that need it:
|
|
51
|
+
|
|
52
|
+
```yaml
|
|
53
|
+
resolutionMode: time-based
|
|
54
|
+
registries:
|
|
55
|
+
https://npm.internal.example/:
|
|
56
|
+
supportsTimeField: true
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
`registry.npmjs.org` omits `time` from abbreviated metadata, so a time-based resolution has to fall back to the much larger full document. That fallback used to be all-or-nothing: `registrySupportsTimeField` answered for every registry at once, so a project resolving from both the public registry and a Verdaccio instance either paid for full metadata everywhere or claimed a `time` field npmjs does not serve. The answer is now per registry, and `registrySupportsTimeField` remains the answer for every registry that does not declare one.
|
|
60
|
+
|
|
61
|
+
The declaration is also sent to a pnpr server, which applies it to the resolution it runs on the client's behalf.
|
|
62
|
+
|
|
63
|
+
### Patch Changes
|
|
64
|
+
|
|
65
|
+
- Re-fetch full registry metadata when `minimumReleaseAge` is enabled and an abbreviated packument's `time` map omits timestamps for some versions. This prevents mature versions from being filtered out and resolution from falling back to the lowest matching version [pnpm/pnpm#13741](https://github.com/pnpm/pnpm/issues/13741).
|
|
66
|
+
|
|
67
|
+
- Reduced registry metadata requests during dependency resolution by reusing cached metadata when lockfile preferences prove that no uncached version can win [pnpm/pnpm#13976](https://github.com/pnpm/pnpm/issues/13976).
|
|
68
|
+
|
|
69
|
+
- Installs are faster in workspaces that declare inter-workspace dependencies with plain ranges (`"*"`, `"^1.2.3"`) rather than the `workspace:` protocol. With `preferWorkspacePackages` enabled, linking such a dependency no longer makes a registry request that cannot change the outcome — and workspace packages that were never published no longer cost a 404 on every install.
|
|
70
|
+
|
|
71
|
+
- Added `fetchWarnTimeoutMs` and `fetchMinSpeedKiBps` to the Rust pnpm CLI and its N-API bindings. Slow registry metadata requests and tarball downloads now emit pnpm-compatible warnings without exposing URL credentials, query parameters, fragments, or control characters [pnpm/pnpm#12042](https://github.com/pnpm/pnpm/issues/12042).
|
|
72
|
+
|
|
73
|
+
- `trustPolicy: no-downgrade` no longer aborts the install with `ERR_PNPM_MISSING_TIME` on registries that serve no per-version `time` field when `minimumReleaseAgeIgnoreMissingTime` is set. The trust check reads the same publish dates the `minimumReleaseAge` check does, so it now honors the same opt-in and skips the affected package with a warning [#12446](https://github.com/pnpm/pnpm/issues/12446).
|
|
74
|
+
|
|
75
|
+
`minimumReleaseAgeIgnoreMissingTime` no longer lets a lockfile entry the registry does not list pass the `minimumReleaseAge` check during lockfile verification. The opt-in covers a registry that cannot date its releases; a packument that does date every version it lists is saying it never published this one, which stays a hard failure.
|
|
76
|
+
|
|
77
|
+
The missing-`time` warning now names the check it is reporting on, so a package whose `minimumReleaseAge` and `trustPolicy` checks are both skipped warns about both instead of only the first.
|
|
78
|
+
|
|
79
|
+
- Updated dependencies:
|
|
80
|
+
- @pnpm/config.normalize-registries@1101.0.0
|
|
81
|
+
- @pnpm/config.pick-registry-for-package@1101.0.0
|
|
82
|
+
- @pnpm/config.version-policy@1100.2.1
|
|
83
|
+
- @pnpm/constants@1102.0.0
|
|
84
|
+
- @pnpm/core-loggers@1100.3.3
|
|
85
|
+
- @pnpm/deps.path@1101.0.0
|
|
86
|
+
- @pnpm/error@1100.1.3
|
|
87
|
+
- @pnpm/pkg-manifest.utils@1100.4.1
|
|
88
|
+
- @pnpm/resolving.jsr-specifier-parser@1100.0.6
|
|
89
|
+
- @pnpm/resolving.registry.pkg-metadata-filter@1100.0.17
|
|
90
|
+
- @pnpm/resolving.registry.types@1100.1.10
|
|
91
|
+
- @pnpm/resolving.resolver-base@1101.1.1
|
|
92
|
+
- @pnpm/store.cafs@1100.2.0
|
|
93
|
+
- @pnpm/store.index@1100.2.5
|
|
94
|
+
- @pnpm/types@1102.0.0
|
|
95
|
+
|
|
3
96
|
## 1103.2.1
|
|
4
97
|
|
|
5
98
|
### Patch Changes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { GetAuthHeader } from '@pnpm/fetching.types';
|
|
2
2
|
import { type ResolutionVerifier } from '@pnpm/resolving.resolver-base';
|
|
3
|
-
import type {
|
|
3
|
+
import type { RegistriesByScope, TrustPolicy } from '@pnpm/types';
|
|
4
4
|
import type { FetchMetadataFromFromRegistryOptions } from './fetch.js';
|
|
5
5
|
import { type FetchFullMetadataCachedOptions } from './fetchFullMetadataCached.js';
|
|
6
6
|
import type { PackageMetaCache } from './pickPackage.js';
|
|
@@ -23,12 +23,15 @@ export interface CreateNpmResolutionVerifierOptions {
|
|
|
23
23
|
/**
|
|
24
24
|
* When the registry's metadata lacks the per-version `time` field
|
|
25
25
|
* (some self-hosted registries strip it), the verifier can't apply
|
|
26
|
-
* the maturity cutoff
|
|
27
|
-
* `
|
|
28
|
-
* passes the entry with a one-time
|
|
29
|
-
* closed. Defaults to `false` so
|
|
30
|
-
* the resolver only when the user
|
|
31
|
-
* skip on the resolver side.
|
|
26
|
+
* the maturity cutoff, and the trust check has no publish order to
|
|
27
|
+
* walk. Set this to `true` to mirror the resolver's warn-and-skip
|
|
28
|
+
* behavior for both — the verifier passes the entry with a one-time
|
|
29
|
+
* `globalWarn`, instead of failing closed. Defaults to `false` so
|
|
30
|
+
* the verifier stays stricter than the resolver only when the user
|
|
31
|
+
* has explicitly opted in to the skip on the resolver side. Scoped
|
|
32
|
+
* to a packument with no usable `time` map: one that dates every
|
|
33
|
+
* version it lists is saying it never published this pin, which
|
|
34
|
+
* fails closed either way.
|
|
32
35
|
*/
|
|
33
36
|
ignoreMissingTimeField?: boolean;
|
|
34
37
|
/**
|
|
@@ -42,14 +45,14 @@ export interface CreateNpmResolutionVerifierOptions {
|
|
|
42
45
|
trustPolicy?: TrustPolicy;
|
|
43
46
|
trustPolicyExclude?: string[];
|
|
44
47
|
trustPolicyIgnoreAfter?: number;
|
|
45
|
-
|
|
48
|
+
registriesByScope: RegistriesByScope;
|
|
46
49
|
/**
|
|
47
|
-
*
|
|
50
|
+
* RegistriesByScope reached via the named-registry resolver chain (e.g. `gh:` →
|
|
48
51
|
* GitHub Packages). When a lockfile entry's tarball URL falls under one of
|
|
49
52
|
* these registry base URLs, route the manifest fetch there instead of the
|
|
50
53
|
* scope-derived default.
|
|
51
54
|
*/
|
|
52
|
-
|
|
55
|
+
registriesByPrefix?: Record<string, string>;
|
|
53
56
|
/**
|
|
54
57
|
* Cache-aware full-metadata fetcher. Decoupled from the resolver pipeline
|
|
55
58
|
* so abbreviated metadata and `peekManifestFromStore` fast paths cannot
|
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
-
import {
|
|
2
|
+
import { normalizeRegistriesByPrefix } from '@pnpm/config.normalize-registries';
|
|
3
3
|
import { namedRegistryTarballPrefixes, pickRegistryForPackage } from '@pnpm/config.pick-registry-for-package';
|
|
4
4
|
import { createPackageVersionPolicy } from '@pnpm/config.version-policy';
|
|
5
5
|
import { FULL_META_DIR } from '@pnpm/constants';
|
|
6
6
|
import { PnpmError } from '@pnpm/error';
|
|
7
7
|
import { isGitHostedTarballUrl, } from '@pnpm/resolving.resolver-base';
|
|
8
|
+
import { isIntegrityAddressedRegistryTarballUrl, isValidTarballRevision, } from '@pnpm/resolving.tarball-url';
|
|
8
9
|
import semver from 'semver';
|
|
9
10
|
import { fetchAttestationPublishedAt } from './fetchAttestationPublishedAt.js';
|
|
10
11
|
import { fetchAbbreviatedMetadataCached, fetchFullMetadataCached, } from './fetchFullMetadataCached.js';
|
|
11
12
|
import { normalizeRegistryUrl } from './normalizeRegistryUrl.js';
|
|
12
13
|
import { getPkgMetaCacheKey, getPkgMirrorPath, loadMeta, warnMissingTimeFieldOnce } from './pickPackage.js';
|
|
13
14
|
import { failIfTrustDowngraded } from './trustChecks.js';
|
|
14
|
-
import { MINIMUM_RELEASE_AGE_VIOLATION_CODE, MISSING_NAMED_REGISTRY_VIOLATION_CODE, MISSING_TARBALL_INTEGRITY_VIOLATION_CODE, TARBALL_URL_MISMATCH_VIOLATION_CODE, TRUST_DOWNGRADE_VIOLATION_CODE, } from './violationCodes.js';
|
|
15
|
+
import { MINIMUM_RELEASE_AGE_VIOLATION_CODE, MISSING_NAMED_REGISTRY_VIOLATION_CODE, MISSING_TARBALL_INTEGRITY_VIOLATION_CODE, TARBALL_REVISION_MISMATCH_VIOLATION_CODE, TARBALL_URL_MISMATCH_VIOLATION_CODE, TRUST_DOWNGRADE_VIOLATION_CODE, } from './violationCodes.js';
|
|
15
16
|
/**
|
|
16
17
|
* Returns a `ResolutionVerifier` for npm-registry-resolved lockfile
|
|
17
18
|
* entries. It always binds each entry's recorded tarball URL to the
|
|
@@ -39,8 +40,8 @@ export function createNpmResolutionVerifier(opts) {
|
|
|
39
40
|
const trustExcludePolicy = opts.trustPolicyExclude?.length
|
|
40
41
|
? createExcludePolicy(opts.trustPolicyExclude, 'trustPolicyExclude')
|
|
41
42
|
: undefined;
|
|
42
|
-
const
|
|
43
|
-
const namedRegistryPrefixes = namedRegistryTarballPrefixes(
|
|
43
|
+
const mergedRegistriesByPrefix = normalizeRegistriesByPrefix(opts.registriesByPrefix);
|
|
44
|
+
const namedRegistryPrefixes = namedRegistryTarballPrefixes(mergedRegistriesByPrefix);
|
|
44
45
|
// Per-install dedup of every network/disk fetch the verifier issues.
|
|
45
46
|
// The maturity check uses the layered `fetchPublishedAt` lookup; the
|
|
46
47
|
// trust check uses an attestation fast-path before falling back to
|
|
@@ -65,6 +66,7 @@ export function createNpmResolutionVerifier(opts) {
|
|
|
65
66
|
const minimumReleaseAge = opts.minimumReleaseAge ?? 0;
|
|
66
67
|
const trustPolicy = opts.trustPolicy;
|
|
67
68
|
const trustPolicyIgnoreAfter = opts.trustPolicyIgnoreAfter;
|
|
69
|
+
const ignoreMissingTimeField = opts.ignoreMissingTimeField === true;
|
|
68
70
|
const verify = async (resolution, { name, version, nonSemverVersion, registryName }) => {
|
|
69
71
|
if (!isRegistryTarballResolution(resolution))
|
|
70
72
|
return { ok: true };
|
|
@@ -105,20 +107,20 @@ export function createNpmResolutionVerifier(opts) {
|
|
|
105
107
|
// Registry-qualified entries name their registry in the dep path, so
|
|
106
108
|
// routing does not depend on a recorded tarball URL (canonical URLs
|
|
107
109
|
// are omitted from the lockfile in the 12.0 format).
|
|
108
|
-
const namedRegistry =
|
|
110
|
+
const namedRegistry = mergedRegistriesByPrefix[registryName];
|
|
109
111
|
if (!namedRegistry) {
|
|
110
112
|
// Fail closed: without the registry URL, none of the metadata-backed
|
|
111
113
|
// checks below can vouch for this entry.
|
|
112
114
|
return {
|
|
113
115
|
ok: false,
|
|
114
116
|
code: MISSING_NAMED_REGISTRY_VIOLATION_CODE,
|
|
115
|
-
reason: `
|
|
117
|
+
reason: `has registry prefix '${registryName}:', which is not declared by the registries setting`,
|
|
116
118
|
};
|
|
117
119
|
}
|
|
118
120
|
registry = namedRegistry;
|
|
119
121
|
}
|
|
120
122
|
else {
|
|
121
|
-
registry = pickRegistryForVersion(opts.
|
|
123
|
+
registry = pickRegistryForVersion(opts.registriesByScope, namedRegistryPrefixes, name, tarballUrl);
|
|
122
124
|
}
|
|
123
125
|
// A registry entry that pins an explicit tarball URL must point at the
|
|
124
126
|
// artifact the registry's own metadata lists. Otherwise a trusted
|
|
@@ -128,17 +130,18 @@ export function createNpmResolutionVerifier(opts) {
|
|
|
128
130
|
// narrowed by their exclude lists, since it guards integrity rather
|
|
129
131
|
// than maturity/trust. Registry entries with no tarball URL reconstruct
|
|
130
132
|
// it from name+version+registry, so they're inherently bound.
|
|
131
|
-
|
|
132
|
-
const urlViolation = await runTarballUrlCheck(lookupContext, registry, name, version, tarballUrl);
|
|
133
|
-
if (urlViolation)
|
|
134
|
-
return urlViolation;
|
|
135
|
-
}
|
|
133
|
+
const rawRevision = resolution.revision;
|
|
136
134
|
const ageApplies = ageCheckActive && !isExcluded(excludePolicy, name, version);
|
|
137
135
|
const trustApplies = trustCheckActive && !isExcluded(trustExcludePolicy, name, version);
|
|
136
|
+
if (tarballUrl != null || (rawRevision != null && (ageApplies || trustApplies))) {
|
|
137
|
+
const artifactViolation = await runRegistryArtifactCheck(lookupContext, registry, name, version, integrity, rawRevision, tarballUrl);
|
|
138
|
+
if (artifactViolation)
|
|
139
|
+
return artifactViolation;
|
|
140
|
+
}
|
|
138
141
|
if (!ageApplies && !trustApplies)
|
|
139
142
|
return { ok: true };
|
|
140
143
|
if (ageApplies) {
|
|
141
|
-
const ageViolation = await runAgeCheck(lookupContext, registry, name, version, cutoff,
|
|
144
|
+
const ageViolation = await runAgeCheck(lookupContext, registry, name, version, cutoff, ignoreMissingTimeField);
|
|
142
145
|
if (ageViolation)
|
|
143
146
|
return ageViolation;
|
|
144
147
|
}
|
|
@@ -146,6 +149,7 @@ export function createNpmResolutionVerifier(opts) {
|
|
|
146
149
|
const trustViolation = await runTrustCheck(lookupContext, registry, name, version, {
|
|
147
150
|
trustPolicyExclude: trustExcludePolicy,
|
|
148
151
|
trustPolicyIgnoreAfter,
|
|
152
|
+
ignoreMissingTimeField,
|
|
149
153
|
});
|
|
150
154
|
if (trustViolation)
|
|
151
155
|
return trustViolation;
|
|
@@ -163,9 +167,9 @@ export function createNpmResolutionVerifier(opts) {
|
|
|
163
167
|
// stays trusted after its exclude entry has been pulled.
|
|
164
168
|
const sortedMinAgeExcludes = [...new Set(opts.minimumReleaseAgeExclude ?? [])].sort();
|
|
165
169
|
const sortedTrustExcludes = [...new Set(opts.trustPolicyExclude ?? [])].sort();
|
|
166
|
-
const
|
|
170
|
+
const sortedRegistriesByPrefix = Object.fromEntries(Object.entries(mergedRegistriesByPrefix).sort(([aliasA], [aliasB]) => aliasA.localeCompare(aliasB)));
|
|
167
171
|
const namedRegistriesRouting = createHash('sha256')
|
|
168
|
-
.update(JSON.stringify(
|
|
172
|
+
.update(JSON.stringify(sortedRegistriesByPrefix))
|
|
169
173
|
.digest('hex');
|
|
170
174
|
return {
|
|
171
175
|
verify,
|
|
@@ -176,6 +180,7 @@ export function createNpmResolutionVerifier(opts) {
|
|
|
176
180
|
// applies the binding — otherwise an upgrade could keep trusting a
|
|
177
181
|
// lockfile that was only ever age/trust-checked.
|
|
178
182
|
tarballUrlBinding: true,
|
|
183
|
+
revisionHistoryBinding: true,
|
|
179
184
|
// Same cache identity rule for the missing-integrity structural check.
|
|
180
185
|
integrityRequired: true,
|
|
181
186
|
namedRegistriesRouting,
|
|
@@ -184,12 +189,15 @@ export function createNpmResolutionVerifier(opts) {
|
|
|
184
189
|
trustPolicy: trustPolicy ?? null,
|
|
185
190
|
trustPolicyExclude: sortedTrustExcludes,
|
|
186
191
|
trustPolicyIgnoreAfter: trustPolicyIgnoreAfter ?? null,
|
|
192
|
+
minimumReleaseAgeIgnoreMissingTime: ignoreMissingTimeField,
|
|
187
193
|
},
|
|
188
194
|
canTrustPastCheck: (cached) => {
|
|
189
195
|
// The tarball-URL binding is unconditional today; a cached run that
|
|
190
196
|
// didn't record it can't be trusted to have enforced it.
|
|
191
197
|
if (cached.tarballUrlBinding !== true)
|
|
192
198
|
return false;
|
|
199
|
+
if (cached.revisionHistoryBinding !== true)
|
|
200
|
+
return false;
|
|
193
201
|
// The missing-integrity check is also unconditional; older cache records
|
|
194
202
|
// without the flag cannot prove they rejected unverifiable tarballs.
|
|
195
203
|
if (cached.integrityRequired !== true)
|
|
@@ -233,6 +241,14 @@ export function createNpmResolutionVerifier(opts) {
|
|
|
233
241
|
const todayIgnoreAfter = trustPolicyIgnoreAfter ?? null;
|
|
234
242
|
if (pastIgnoreAfter !== todayIgnoreAfter)
|
|
235
243
|
return false;
|
|
244
|
+
// Missing-time tolerance: a cached run that failed closed on an
|
|
245
|
+
// absent `time` field accepted a subset of what today's tolerant
|
|
246
|
+
// policy accepts, so it stays trustworthy. Turning the tolerance
|
|
247
|
+
// off invalidates it — entries the past run waved through are the
|
|
248
|
+
// ones today's policy exists to reject. Older records (no field)
|
|
249
|
+
// read as intolerant, which is the safe direction.
|
|
250
|
+
if (cached.minimumReleaseAgeIgnoreMissingTime === true && !ignoreMissingTimeField)
|
|
251
|
+
return false;
|
|
236
252
|
return true;
|
|
237
253
|
},
|
|
238
254
|
};
|
|
@@ -245,15 +261,23 @@ async function runAgeCheck(context, registry, name, version, cutoff, ignoreMissi
|
|
|
245
261
|
const published = await fetchPublishedAt(context, registry, name, version);
|
|
246
262
|
if (!published) {
|
|
247
263
|
// No source — attestation, local mirror, or full metadata —
|
|
248
|
-
// surfaced a publish timestamp for this version.
|
|
249
|
-
//
|
|
250
|
-
//
|
|
251
|
-
// `time`
|
|
252
|
-
//
|
|
253
|
-
//
|
|
264
|
+
// surfaced a publish timestamp for this version. What
|
|
265
|
+
// `minimumReleaseAgeIgnoreMissingTime` opts out of is a registry that
|
|
266
|
+
// cannot date its releases, so the skip is granted only when the
|
|
267
|
+
// packument carries no usable `time` map at all — the same shape the
|
|
268
|
+
// resolver's `pickMatchingVersionFinal` warns and skips on, so the
|
|
269
|
+
// verifier can't be stricter than fresh resolution. A packument that
|
|
270
|
+
// does date every version it lists is instead telling us this pin is
|
|
271
|
+
// not one of them (`dropIncompletePublishTimes` leaves no partial maps
|
|
272
|
+
// for that to be ambiguous), and an unpublished or never-published pin
|
|
273
|
+
// must fail closed however the flag is set.
|
|
254
274
|
if (ignoreMissingTimeField) {
|
|
255
|
-
|
|
256
|
-
|
|
275
|
+
// Already awaited by the lookup above, so this is a cache hit.
|
|
276
|
+
const timeMap = await fetchFullMetaTime(context, registry, name);
|
|
277
|
+
if (timeMap == null) {
|
|
278
|
+
warnMissingTimeFieldOnce(name, 'minimumReleaseAge');
|
|
279
|
+
return undefined;
|
|
280
|
+
}
|
|
257
281
|
}
|
|
258
282
|
return {
|
|
259
283
|
ok: false,
|
|
@@ -290,7 +314,7 @@ async function runAgeCheck(context, registry, name, version, cutoff, ignoreMissi
|
|
|
290
314
|
* a tampered lockfile could smuggle a malicious URL past the check by
|
|
291
315
|
* pointing it at a `name@version` the registry can't vouch for.
|
|
292
316
|
*/
|
|
293
|
-
async function
|
|
317
|
+
async function runRegistryArtifactCheck(context, registry, name, version, lockfileIntegrity, rawRevision, lockfileTarball) {
|
|
294
318
|
const { meta, error } = await fetchAbbreviatedMeta(context, registry, name);
|
|
295
319
|
if (error != null) {
|
|
296
320
|
// Couldn't reach the registry to verify (auth/network/5xx). Propagate the
|
|
@@ -300,25 +324,114 @@ async function runTarballUrlCheck(context, registry, name, version, lockfileTarb
|
|
|
300
324
|
// error — still fail-closed, the entry never reaches the filesystem.
|
|
301
325
|
throw error;
|
|
302
326
|
}
|
|
303
|
-
const
|
|
304
|
-
if (
|
|
305
|
-
|
|
327
|
+
const artifact = meta?.versionArtifacts?.get(version);
|
|
328
|
+
if (artifact == null) {
|
|
329
|
+
if (lockfileTarball == null && rawRevision == null)
|
|
330
|
+
return undefined;
|
|
331
|
+
return {
|
|
332
|
+
ok: false,
|
|
333
|
+
code: lockfileTarball == null
|
|
334
|
+
? TARBALL_REVISION_MISMATCH_VIOLATION_CODE
|
|
335
|
+
: TARBALL_URL_MISMATCH_VIOLATION_CODE,
|
|
336
|
+
reason: "could not be verified against the registry's published metadata",
|
|
337
|
+
};
|
|
306
338
|
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
339
|
+
const metadataRevision = artifact.current.revision;
|
|
340
|
+
const revisionAware = rawRevision != null || metadataRevision !== undefined || artifact.revisions.length > 0;
|
|
341
|
+
if (!revisionAware) {
|
|
342
|
+
if (lockfileTarball == null)
|
|
343
|
+
return undefined;
|
|
344
|
+
if (typeof artifact.current.tarball === 'string' && sameTarballUrl(lockfileTarball, artifact.current.tarball)) {
|
|
345
|
+
return undefined;
|
|
346
|
+
}
|
|
347
|
+
return {
|
|
348
|
+
ok: false,
|
|
349
|
+
code: TARBALL_URL_MISMATCH_VIOLATION_CODE,
|
|
350
|
+
reason: typeof artifact.current.tarball !== 'string'
|
|
351
|
+
? "could not be verified against the registry's published metadata"
|
|
352
|
+
: `has a tarball URL (${lockfileTarball}) that does not match the registry's published metadata (${artifact.current.tarball})`,
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
if (rawRevision != null && !isValidTarballRevision(rawRevision)) {
|
|
356
|
+
return {
|
|
357
|
+
ok: false,
|
|
358
|
+
code: TARBALL_REVISION_MISMATCH_VIOLATION_CODE,
|
|
359
|
+
reason: `has an invalid revision (${String(rawRevision)})`,
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
let currentRevision = 0;
|
|
363
|
+
if (metadataRevision !== undefined) {
|
|
364
|
+
if (!isValidTarballRevision(metadataRevision)) {
|
|
365
|
+
return {
|
|
366
|
+
ok: false,
|
|
367
|
+
code: TARBALL_REVISION_MISMATCH_VIOLATION_CODE,
|
|
368
|
+
reason: `registry metadata has an invalid current revision (${String(metadataRevision)})`,
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
currentRevision = metadataRevision;
|
|
372
|
+
const currentHistory = artifact.revisions.filter(candidate => candidate.revision === currentRevision);
|
|
373
|
+
if (currentHistory.length !== 1 ||
|
|
374
|
+
currentHistory[0].integrity !== artifact.current.integrity ||
|
|
375
|
+
typeof currentHistory[0].tarball !== 'string' ||
|
|
376
|
+
typeof artifact.current.tarball !== 'string' ||
|
|
377
|
+
typeof artifact.current.integrity !== 'string' ||
|
|
378
|
+
!isIntegrityAddressedRegistryTarballUrl(normalizeRegistryUrl(artifact.current.tarball), artifact.current.integrity, registry) ||
|
|
379
|
+
!sameTarballUrl(currentHistory[0].tarball, artifact.current.tarball)) {
|
|
380
|
+
return {
|
|
381
|
+
ok: false,
|
|
382
|
+
code: TARBALL_REVISION_MISMATCH_VIOLATION_CODE,
|
|
383
|
+
reason: `registry metadata revision ${currentRevision} does not have exactly one matching history entry`,
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
const revision = typeof rawRevision === 'number' ? rawRevision : 0;
|
|
388
|
+
const currentMatches = currentRevision === revision;
|
|
389
|
+
const historicalCandidates = artifact.revisions.filter(candidate => candidate.revision === revision);
|
|
390
|
+
if (historicalCandidates.length > 1) {
|
|
391
|
+
return {
|
|
392
|
+
ok: false,
|
|
393
|
+
code: TARBALL_REVISION_MISMATCH_VIOLATION_CODE,
|
|
394
|
+
reason: `revision ${revision} is advertised more than once in the registry's history`,
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
const historical = historicalCandidates[0];
|
|
398
|
+
const selected = currentMatches ? artifact.current : historical;
|
|
399
|
+
if (selected == null ||
|
|
400
|
+
selected.integrity !== lockfileIntegrity ||
|
|
401
|
+
(currentMatches && historical != null && historical.integrity !== lockfileIntegrity)) {
|
|
402
|
+
return {
|
|
403
|
+
ok: false,
|
|
404
|
+
code: TARBALL_REVISION_MISMATCH_VIOLATION_CODE,
|
|
405
|
+
reason: `has revision ${revision} with an integrity that does not match the registry's current or historical metadata`,
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
if (revision > 0 || !currentMatches) {
|
|
409
|
+
if (typeof selected.tarball !== 'string' ||
|
|
410
|
+
!isIntegrityAddressedRegistryTarballUrl(normalizeRegistryUrl(selected.tarball), lockfileIntegrity, registry)) {
|
|
411
|
+
return {
|
|
412
|
+
ok: false,
|
|
413
|
+
code: TARBALL_REVISION_MISMATCH_VIOLATION_CODE,
|
|
414
|
+
reason: `has revision ${revision} that is not addressed by its complete sha512 integrity`,
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
if (lockfileTarball != null && (typeof selected.tarball !== 'string' || !sameTarballUrl(lockfileTarball, selected.tarball))) {
|
|
419
|
+
return {
|
|
420
|
+
ok: false,
|
|
421
|
+
code: TARBALL_URL_MISMATCH_VIOLATION_CODE,
|
|
422
|
+
reason: typeof selected.tarball !== 'string'
|
|
423
|
+
? "could not be verified against the registry's published metadata"
|
|
424
|
+
: `has a tarball URL (${lockfileTarball}) that does not match the registry's published metadata (${selected.tarball})`,
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
return undefined;
|
|
314
428
|
}
|
|
315
429
|
function sameTarballUrl(a, b) {
|
|
316
430
|
return canonicalTarballUrl(a) === canonicalTarballUrl(b);
|
|
317
431
|
}
|
|
318
|
-
//
|
|
319
|
-
// a
|
|
320
|
-
//
|
|
321
|
-
// tampering. The `%2f` match is case-insensitive because `normalizeRegistryUrl`
|
|
432
|
+
// Both URLs come from the registry, so ignore the protocol and `%2f` scope
|
|
433
|
+
// encoding: a benign http/https or encoding difference isn't tampering. The
|
|
434
|
+
// `%2f` match is case-insensitive because `normalizeRegistryUrl`
|
|
322
435
|
// (`new URL().toString()`) can upper-case percent-escapes to `%2F`.
|
|
323
436
|
function canonicalTarballUrl(url) {
|
|
324
437
|
const normalized = normalizeRegistryUrl(url).replace(/%2f/gi, '/');
|
|
@@ -531,7 +644,7 @@ async function tryAbbreviatedModifiedShortcut(context, registry, name, version)
|
|
|
531
644
|
// publish time — but only for versions the registry currently lists.
|
|
532
645
|
// An unpublished or never-published pin would otherwise pass the gate
|
|
533
646
|
// on a stale package-level timestamp.
|
|
534
|
-
if (!meta?.
|
|
647
|
+
if (!meta?.versionArtifacts?.has(version))
|
|
535
648
|
return undefined;
|
|
536
649
|
return modified;
|
|
537
650
|
}
|
|
@@ -615,16 +728,29 @@ function validateSharedMeta(meta, name) {
|
|
|
615
728
|
// lockfile (see #11860). The full document is GC-able as soon as this
|
|
616
729
|
// closure returns; only the short tarball-URL strings are retained.
|
|
617
730
|
function projectAbbreviatedMeta(meta) {
|
|
618
|
-
let
|
|
731
|
+
let versionArtifacts;
|
|
619
732
|
if (meta.versions) {
|
|
620
|
-
|
|
733
|
+
versionArtifacts = new Map();
|
|
621
734
|
for (const [version, manifest] of Object.entries(meta.versions)) {
|
|
622
|
-
|
|
735
|
+
versionArtifacts.set(version, {
|
|
736
|
+
current: {
|
|
737
|
+
revision: manifest.dist?.revision,
|
|
738
|
+
integrity: manifest.dist?.integrity,
|
|
739
|
+
tarball: manifest.dist?.tarball,
|
|
740
|
+
},
|
|
741
|
+
revisions: Array.isArray(manifest.dist?.revisions)
|
|
742
|
+
? manifest.dist.revisions.map(revision => ({
|
|
743
|
+
revision: revision.revision,
|
|
744
|
+
integrity: revision.integrity,
|
|
745
|
+
tarball: revision.tarball,
|
|
746
|
+
}))
|
|
747
|
+
: [],
|
|
748
|
+
});
|
|
623
749
|
}
|
|
624
750
|
}
|
|
625
751
|
return {
|
|
626
752
|
modified: meta.modified,
|
|
627
|
-
|
|
753
|
+
versionArtifacts,
|
|
628
754
|
};
|
|
629
755
|
}
|
|
630
756
|
function readLocalMetaTime(context, registry, name) {
|
|
@@ -657,7 +783,7 @@ function fetchFullMetaTime(context, registry, name) {
|
|
|
657
783
|
}
|
|
658
784
|
return cachedPromise;
|
|
659
785
|
}
|
|
660
|
-
function pickRegistryForVersion(
|
|
786
|
+
function pickRegistryForVersion(registriesByScope, namedRegistryPrefixes, name, tarballUrl) {
|
|
661
787
|
// If the lockfile records where the tarball lives, prefer that — scope
|
|
662
788
|
// routing (`@scope:registry`) only covers scoped packages, but named
|
|
663
789
|
// registries (`gh:`, `jsr:` aliases, custom) ship un-scoped packages whose
|
|
@@ -674,7 +800,7 @@ function pickRegistryForVersion(registries, namedRegistryPrefixes, name, tarball
|
|
|
674
800
|
return prefix;
|
|
675
801
|
}
|
|
676
802
|
}
|
|
677
|
-
return pickRegistryForPackage(
|
|
803
|
+
return pickRegistryForPackage(registriesByScope, name);
|
|
678
804
|
}
|
|
679
805
|
function tryParseUrl(url) {
|
|
680
806
|
try {
|
package/lib/fetch.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import url from 'node:url';
|
|
2
2
|
import util from 'node:util';
|
|
3
3
|
import { requestRetryLogger } from '@pnpm/core-loggers';
|
|
4
|
-
import { FetchError, PnpmError, redactUrlCredentials, } from '@pnpm/error';
|
|
4
|
+
import { FetchError, PnpmError, redactUrlCredentials, redactUrlForDisplay, } from '@pnpm/error';
|
|
5
5
|
import { globalWarn } from '@pnpm/logger';
|
|
6
6
|
import * as retry from '@zkochan/retry';
|
|
7
7
|
import semver from 'semver';
|
|
8
8
|
import { clearMeta } from './clearMeta.js';
|
|
9
|
+
import { dropIncompletePublishTimes } from './publishTimes.js';
|
|
9
10
|
/**
|
|
10
11
|
* Content type of an abbreviated (install-oriented) package metadata document.
|
|
11
12
|
* A spec-compliant registry echoes this in the response `Content-Type` when it
|
|
@@ -147,10 +148,11 @@ export async function fetchMetadataFromFromRegistry(fetchOpts, pkgName, { authHe
|
|
|
147
148
|
try {
|
|
148
149
|
const jsonText = await response.text();
|
|
149
150
|
const meta = JSON.parse(jsonText);
|
|
151
|
+
dropIncompletePublishTimes(meta);
|
|
150
152
|
// Check if request took longer than expected
|
|
151
153
|
const elapsedMs = Date.now() - startTime;
|
|
152
154
|
if (elapsedMs > fetchOpts.fetchWarnTimeoutMs) {
|
|
153
|
-
globalWarn(`Request took ${elapsedMs}ms: ${uri}`);
|
|
155
|
+
globalWarn(`Request took ${elapsedMs}ms: ${redactUrlForDisplay(uri)}`);
|
|
154
156
|
}
|
|
155
157
|
resolve({
|
|
156
158
|
...normalizeAbbreviatedResponse({ fullMetadata, meta, jsonText, response }),
|
|
@@ -200,7 +202,7 @@ export function notModifiedWithoutCacheError(pkgName) {
|
|
|
200
202
|
* carry the megabytes of install-irrelevant data (scripts, exports, readme,
|
|
201
203
|
* custom fields) that a full document contains.
|
|
202
204
|
*
|
|
203
|
-
*
|
|
205
|
+
* RegistriesByScope that honor the header (e.g. the npm registry) echo the abbreviated
|
|
204
206
|
* `Content-Type`, so this is a no-op for them: no re-serialization, no field
|
|
205
207
|
* stripping — the happy path pays nothing.
|
|
206
208
|
*/
|
package/lib/index.d.ts
CHANGED
|
@@ -2,9 +2,9 @@ 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
4
|
import type { DirectoryResolution, LatestInfo, LatestQuery, PkgResolutionId, PreferredVersions, ResolveOptions, ResolveResult, TarballResolution, WantedDependency, WorkspacePackages } from '@pnpm/resolving.resolver-base';
|
|
5
|
-
import type { DependencyManifest, PackageVersionPolicy, RangeSpecStyle,
|
|
5
|
+
import type { DependencyManifest, PackageVersionPolicy, RangeSpecStyle, RegistriesByScope, TrustPolicy } from '@pnpm/types';
|
|
6
6
|
import { fetchMetadataFromFromRegistry, type FetchMetadataFromFromRegistryOptions, RegistryResponseError } from './fetch.js';
|
|
7
|
-
import {
|
|
7
|
+
import { BUILTIN_REGISTRIES_BY_PREFIX, parseBareSpecifier, type RegistryPackageSpec } from './parseBareSpecifier.js';
|
|
8
8
|
import { type PackageMetaCache, pickPackage, type PickPackageOptions } from './pickPackage.js';
|
|
9
9
|
import { pickPackageFromMeta, pickVersionByVersionRange } from './pickPackageFromMeta.js';
|
|
10
10
|
import { workspacePrefToNpm } from './workspacePrefToNpm.js';
|
|
@@ -18,7 +18,7 @@ export declare class NoMatchingVersionError extends PnpmError {
|
|
|
18
18
|
constructor(opts: NoMatchingVersionErrorOptions);
|
|
19
19
|
}
|
|
20
20
|
export declare function formatTimeAgo(date: Date): string | null;
|
|
21
|
-
export {
|
|
21
|
+
export { BUILTIN_REGISTRIES_BY_PREFIX, fetchMetadataFromFromRegistry, type FetchMetadataFromFromRegistryOptions, type PackageMeta, type PackageMetaCache, parseBareSpecifier, pickPackageFromMeta, pickVersionByVersionRange, type RegistryPackageSpec, RegistryResponseError, workspacePrefToNpm, };
|
|
22
22
|
export { createNpmResolutionVerifier, type CreateNpmResolutionVerifierOptions } from './createNpmResolutionVerifier.js';
|
|
23
23
|
export { MINIMUM_RELEASE_AGE_VIOLATION_CODE, TRUST_DOWNGRADE_VIOLATION_CODE, } from './violationCodes.js';
|
|
24
24
|
export interface ResolverFactoryOptions {
|
|
@@ -26,13 +26,20 @@ export interface ResolverFactoryOptions {
|
|
|
26
26
|
storeDir?: string;
|
|
27
27
|
frozenStore?: boolean;
|
|
28
28
|
fullMetadata?: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Asked instead of {@link ResolverFactoryOptions.fullMetadata} when the
|
|
31
|
+
* caller can answer per registry — a registry that declares
|
|
32
|
+
* `supportsTimeField` needs no full metadata for a time-based resolution
|
|
33
|
+
* even when the others do.
|
|
34
|
+
*/
|
|
35
|
+
needsFullMetadataFor?: (registry: string) => boolean;
|
|
29
36
|
filterMetadata?: boolean;
|
|
30
37
|
offline?: boolean;
|
|
31
38
|
preferOffline?: boolean;
|
|
32
39
|
retry?: RetryTimeoutOptions;
|
|
33
40
|
timeout?: number;
|
|
34
|
-
|
|
35
|
-
|
|
41
|
+
registriesByScope: RegistriesByScope;
|
|
42
|
+
registriesByPrefix?: Record<string, string>;
|
|
36
43
|
saveWorkspaceProtocol?: boolean | 'rolling';
|
|
37
44
|
preserveAbsolutePaths?: boolean;
|
|
38
45
|
ignoreMissingTimeField?: boolean;
|
|
@@ -83,10 +90,16 @@ export declare function createNpmResolver(fetchFromRegistry: FetchFromRegistry,
|
|
|
83
90
|
export interface ResolveFromNpmContext {
|
|
84
91
|
pickPackage: (spec: RegistryPackageSpec, opts: PickPackageOptions) => ReturnType<typeof pickPackage>;
|
|
85
92
|
getAuthHeaderValueByURI: GetAuthHeader;
|
|
86
|
-
|
|
87
|
-
|
|
93
|
+
registriesByScope: RegistriesByScope;
|
|
94
|
+
registriesByPrefix: Record<string, string>;
|
|
88
95
|
namedRegistryNames: ReadonlySet<string>;
|
|
89
96
|
saveWorkspaceProtocol?: boolean | 'rolling';
|
|
97
|
+
/**
|
|
98
|
+
* The `minimumReleaseAgeIgnoreMissingTime` opt-in, reaching the trust
|
|
99
|
+
* check as well as the version pick: both read the same per-version
|
|
100
|
+
* `time`, so a registry that strips it takes both down together.
|
|
101
|
+
*/
|
|
102
|
+
ignoreMissingTimeField?: boolean;
|
|
90
103
|
peekManifestFromStore?: (opts: {
|
|
91
104
|
id: PkgResolutionId;
|
|
92
105
|
integrity: string;
|
|
@@ -110,6 +123,7 @@ export type ResolveFromNpmOptions = {
|
|
|
110
123
|
preferredVersions?: PreferredVersions;
|
|
111
124
|
preferWorkspacePackages?: boolean;
|
|
112
125
|
update?: false | 'compatible' | 'latest';
|
|
126
|
+
updatePatches?: boolean;
|
|
113
127
|
updateRequested?: boolean;
|
|
114
128
|
updateChecksums?: boolean;
|
|
115
129
|
injectWorkspacePackages?: boolean;
|