@pnpm/installing.deps-installer 1101.1.2 → 1101.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,59 @@
1
+ import type { LockfileObject } from '@pnpm/lockfile.fs';
2
+ import type { ResolutionPolicyViolation, ResolutionVerifier } from '@pnpm/resolving.resolver-base';
3
+ export type { ResolutionPolicyViolation };
4
+ export interface VerifyLockfileResolutionsOptions {
5
+ concurrency?: number;
6
+ /**
7
+ * pnpm's on-disk cache directory. When set together with
8
+ * `lockfilePath`, verification results are memoized in
9
+ * `<cacheDir>/lockfile-verified.jsonl` and the gate short-circuits on
10
+ * a repeat run against an unchanged lockfile + same-or-stricter
11
+ * policy. Omit to disable the cache entirely (every call rehashes
12
+ * the lockfile and re-verifies).
13
+ */
14
+ cacheDir?: string;
15
+ /** Absolute path of the lockfile being verified. Used by the cache's stat shortcut. */
16
+ lockfilePath?: string;
17
+ }
18
+ /**
19
+ * Policy-neutral pass that asks every resolver-supplied
20
+ * {@link ResolutionVerifier} to check every entry in a lockfile loaded
21
+ * from disk. Iteration runs before resolution decisions are touched and
22
+ * before any tarball is fetched, so a lockfile whose entries were
23
+ * resolved elsewhere (committed to the repo, restored from a cache,
24
+ * etc.) under a weaker or absent policy cannot reach the filesystem.
25
+ * Fresh local resolution is covered by the resolver's own per-version
26
+ * filter.
27
+ *
28
+ * Each verifier handles its own protocol short-circuit inside `verify`
29
+ * (returning `{ ok: true }` for resolutions outside its scope), so the
30
+ * fan-out is policy-neutral and dispatch-free at this layer.
31
+ *
32
+ * Designed for fail-closed semantics at the verifier level: a verifier
33
+ * that can't confirm a resolution is expected to return `{ ok: false }`
34
+ * rather than passing silently — otherwise a registry hiccup or an
35
+ * unpublished version would re-open the bypass.
36
+ *
37
+ * No-op when `verifiers` is empty.
38
+ *
39
+ * When `options.cacheDir` and `options.lockfilePath` are both
40
+ * provided, an unchanged lockfile that has already been verified
41
+ * under the same (or stricter) policy short-circuits the registry
42
+ * round-trip entirely — see {@link tryLockfileVerificationCache} for
43
+ * the lookup logic.
44
+ */
45
+ export declare function verifyLockfileResolutions(lockfile: LockfileObject, verifiers: ResolutionVerifier[], options?: VerifyLockfileResolutionsOptions): Promise<void>;
46
+ /**
47
+ * Collect-mode sibling of {@link verifyLockfileResolutions}: runs the
48
+ * same fan-out over every verifier and every lockfile entry, but
49
+ * returns the violations as data instead of throwing on the first batch.
50
+ * No cache lookup or write — the throw-mode `verifyLockfileResolutions`
51
+ * is what populates / honors the cache; this is for callers that need
52
+ * to inspect violations (auto-collect into `minimumReleaseAgeExclude`,
53
+ * the strict-mode interactive prompt, future resolver-specific
54
+ * policies).
55
+ *
56
+ * Returns an empty array when `verifiers` is empty or the lockfile has
57
+ * no packages, so callers don't need a separate emptiness check.
58
+ */
59
+ export declare function collectResolutionPolicyViolations(lockfile: LockfileObject, verifiers: ResolutionVerifier[], options?: Pick<VerifyLockfileResolutionsOptions, 'concurrency'>): Promise<ResolutionPolicyViolation[]>;
@@ -0,0 +1,225 @@
1
+ import { lockfileVerificationLogger } from '@pnpm/core-loggers';
2
+ import { hashObject } from '@pnpm/crypto.object-hasher';
3
+ import { PnpmError } from '@pnpm/error';
4
+ import { nameVerFromPkgSnapshot } from '@pnpm/lockfile.utils';
5
+ import pLimit from 'p-limit';
6
+ import { recordVerification, tryLockfileVerificationCache, } from './verifyLockfileResolutionsCache.js';
7
+ // Cap the per-entry breakdown so a verifier rejecting hundreds of entries
8
+ // (e.g. a poisoned lockfile) doesn't flood the terminal / CI log; the full
9
+ // count is in the header and the remainder is summarized at the end.
10
+ const MAX_VIOLATIONS_TO_PRINT = 20;
11
+ // 16 mirrors the floor of pnpm's package-requester network-concurrency
12
+ // (Math.min(64, Math.max(workers*3, 16))); keep them aligned so the
13
+ // verification pass doesn't push past what the rest of the install respects.
14
+ const DEFAULT_CONCURRENCY = 16;
15
+ /**
16
+ * Policy-neutral pass that asks every resolver-supplied
17
+ * {@link ResolutionVerifier} to check every entry in a lockfile loaded
18
+ * from disk. Iteration runs before resolution decisions are touched and
19
+ * before any tarball is fetched, so a lockfile whose entries were
20
+ * resolved elsewhere (committed to the repo, restored from a cache,
21
+ * etc.) under a weaker or absent policy cannot reach the filesystem.
22
+ * Fresh local resolution is covered by the resolver's own per-version
23
+ * filter.
24
+ *
25
+ * Each verifier handles its own protocol short-circuit inside `verify`
26
+ * (returning `{ ok: true }` for resolutions outside its scope), so the
27
+ * fan-out is policy-neutral and dispatch-free at this layer.
28
+ *
29
+ * Designed for fail-closed semantics at the verifier level: a verifier
30
+ * that can't confirm a resolution is expected to return `{ ok: false }`
31
+ * rather than passing silently — otherwise a registry hiccup or an
32
+ * unpublished version would re-open the bypass.
33
+ *
34
+ * No-op when `verifiers` is empty.
35
+ *
36
+ * When `options.cacheDir` and `options.lockfilePath` are both
37
+ * provided, an unchanged lockfile that has already been verified
38
+ * under the same (or stricter) policy short-circuits the registry
39
+ * round-trip entirely — see {@link tryLockfileVerificationCache} for
40
+ * the lookup logic.
41
+ */
42
+ export async function verifyLockfileResolutions(lockfile, verifiers, options) {
43
+ if (verifiers.length === 0)
44
+ return;
45
+ if (!lockfile.packages)
46
+ return;
47
+ // Caching kicks in only when the caller surfaced both a writable
48
+ // cache directory and the lockfile's absolute path — that's the
49
+ // production wiring; unit tests that skip them get the gate without
50
+ // memoization and still exercise the same code path.
51
+ const cache = options?.cacheDir && options?.lockfilePath
52
+ ? { cacheDir: options.cacheDir, lockfilePath: options.lockfilePath }
53
+ : undefined;
54
+ let cachePrecomputed;
55
+ // hashObject streams and is key-order-stable, unlike JSON.stringify.
56
+ let cachedHash;
57
+ const hashLockfile = () => {
58
+ if (cachedHash == null)
59
+ cachedHash = hashObject(lockfile);
60
+ return cachedHash;
61
+ };
62
+ if (cache) {
63
+ const result = tryLockfileVerificationCache(cache.cacheDir, {
64
+ lockfilePath: cache.lockfilePath,
65
+ verifiers,
66
+ hashLockfile,
67
+ });
68
+ if (result.hit)
69
+ return;
70
+ cachePrecomputed = result.precomputed;
71
+ }
72
+ // Emit started/done around the actual verification pass — the
73
+ // round-trip can be slow on a cold registry cache, and the cached
74
+ // short-circuit above doesn't reach this branch, so a user only
75
+ // sees these messages on installs that are doing real work.
76
+ // A degenerate lockfile where every snapshot fails the
77
+ // name/version extraction (so candidates is empty) skips emission
78
+ // entirely — no work, no noise.
79
+ const candidates = collectCandidates(lockfile);
80
+ if (candidates.size === 0) {
81
+ if (cache) {
82
+ recordVerification(cache.cacheDir, {
83
+ lockfilePath: cache.lockfilePath,
84
+ verifiers,
85
+ hashLockfile,
86
+ }, cachePrecomputed);
87
+ }
88
+ return;
89
+ }
90
+ const startedAt = Date.now();
91
+ lockfileVerificationLogger.debug({
92
+ status: 'started',
93
+ entries: candidates.size,
94
+ lockfilePath: options?.lockfilePath,
95
+ });
96
+ // Guarantee a terminal `done` or `failed` event on every exit path
97
+ // that emitted `started`. Without this, an unexpected throw from the
98
+ // registry fan-out (or the policy-violation throw below) would leave
99
+ // the transient "Verifying lockfile…" line as the last frame the
100
+ // reporter rendered for this block, hanging spinner-style above the
101
+ // failure output.
102
+ let terminalStatus = 'failed';
103
+ try {
104
+ const violations = await iterateLockfileViolations(candidates, verifiers, options?.concurrency);
105
+ if (violations.length === 0) {
106
+ terminalStatus = 'done';
107
+ // Persist the success so the next install can stat-only the lockfile.
108
+ if (cache) {
109
+ recordVerification(cache.cacheDir, {
110
+ lockfilePath: cache.lockfilePath,
111
+ verifiers,
112
+ hashLockfile,
113
+ }, cachePrecomputed);
114
+ }
115
+ return;
116
+ }
117
+ throw buildVerificationError(violations);
118
+ }
119
+ finally {
120
+ lockfileVerificationLogger.debug({
121
+ status: terminalStatus,
122
+ entries: candidates.size,
123
+ elapsedMs: Date.now() - startedAt,
124
+ lockfilePath: options?.lockfilePath,
125
+ });
126
+ }
127
+ }
128
+ function buildVerificationError(violations) {
129
+ // Stable order so the error output is deterministic.
130
+ violations.sort((a, b) => `${a.name}@${a.version}`.localeCompare(`${b.name}@${b.version}`));
131
+ // Pick the throw code: a single-code batch keeps the per-policy code
132
+ // (so existing handlers / docs / search keywords still route correctly);
133
+ // a mixed batch (e.g. minimumReleaseAge + trust-downgrade on the same
134
+ // lockfile) escalates to the generic `LOCKFILE_RESOLUTION_VERIFICATION`
135
+ // and the per-entry code goes into the breakdown so the user can see
136
+ // which policy each entry tripped.
137
+ const distinctCodes = new Set(violations.map((v) => v.code));
138
+ const isMixed = distinctCodes.size > 1;
139
+ const errorCode = isMixed ? 'LOCKFILE_RESOLUTION_VERIFICATION' : violations[0].code;
140
+ const visible = violations.slice(0, MAX_VIOLATIONS_TO_PRINT);
141
+ const omitted = violations.length - visible.length;
142
+ const formatEntry = isMixed
143
+ ? (v) => ` ${v.name}@${v.version} [${v.code}] ${v.reason}`
144
+ : (v) => ` ${v.name}@${v.version} ${v.reason}`;
145
+ const breakdown = visible.map(formatEntry).join('\n');
146
+ const details = omitted > 0
147
+ ? `${breakdown}\n …and ${omitted} more`
148
+ : breakdown;
149
+ return new PnpmError(errorCode, `${violations.length} lockfile entries failed verification:\n${details}`, {
150
+ hint: 'The lockfile contains entries that the active policies reject. ' +
151
+ 'This can mean the lockfile is stale, or that someone committed a ' +
152
+ 'lockfile that bypassed the policy locally — inspect recent changes ' +
153
+ 'to pnpm-lock.yaml before trusting it. If the changes look expected, ' +
154
+ 'run "pnpm clean --lockfile" and then "pnpm install" to rebuild from ' +
155
+ 'a fresh resolution. Alternatively, relax the policy that flagged ' +
156
+ 'them.',
157
+ });
158
+ }
159
+ /**
160
+ * Collect-mode sibling of {@link verifyLockfileResolutions}: runs the
161
+ * same fan-out over every verifier and every lockfile entry, but
162
+ * returns the violations as data instead of throwing on the first batch.
163
+ * No cache lookup or write — the throw-mode `verifyLockfileResolutions`
164
+ * is what populates / honors the cache; this is for callers that need
165
+ * to inspect violations (auto-collect into `minimumReleaseAgeExclude`,
166
+ * the strict-mode interactive prompt, future resolver-specific
167
+ * policies).
168
+ *
169
+ * Returns an empty array when `verifiers` is empty or the lockfile has
170
+ * no packages, so callers don't need a separate emptiness check.
171
+ */
172
+ export async function collectResolutionPolicyViolations(lockfile, verifiers, options) {
173
+ if (verifiers.length === 0)
174
+ return [];
175
+ if (!lockfile.packages)
176
+ return [];
177
+ return iterateLockfileViolations(collectCandidates(lockfile), verifiers, options?.concurrency);
178
+ }
179
+ // depPath can include peer-dependency and patch_hash suffixes (e.g.
180
+ // `react@18.0.0(peer)(patch_hash=…)`); the same (name, version) pair may
181
+ // therefore appear multiple times. Dedupe so we issue at most one
182
+ // verification per package version.
183
+ //
184
+ // Include a serialization of `resolution` in the key so two entries that
185
+ // share a (name, version) but differ in *what* was resolved (e.g. one
186
+ // pinned via npm, another via a git URL under the same alias) don't
187
+ // collapse: if the wrong shape wins the dedup, a protocol-scoped
188
+ // verifier short-circuits on the surviving entry and the real one is
189
+ // never checked.
190
+ function collectCandidates(lockfile) {
191
+ const candidates = new Map();
192
+ for (const [depPath, snapshot] of Object.entries(lockfile.packages ?? {})) {
193
+ const { name, version } = nameVerFromPkgSnapshot(depPath, snapshot);
194
+ if (!name || !version)
195
+ continue;
196
+ const key = `${name}@${version}@${JSON.stringify(snapshot.resolution)}`;
197
+ candidates.set(key, {
198
+ name,
199
+ version,
200
+ resolution: snapshot.resolution,
201
+ });
202
+ }
203
+ return candidates;
204
+ }
205
+ async function iterateLockfileViolations(candidates, verifiers, concurrency) {
206
+ const violations = [];
207
+ const limit = pLimit(concurrency ?? DEFAULT_CONCURRENCY);
208
+ await Promise.all(Array.from(candidates.values(), ({ name, version, resolution }) => limit(async () => {
209
+ // Fan out across every active verifier; each handles its own
210
+ // protocol short-circuit (e.g. the npm verifier returns ok:true for
211
+ // git resolutions). We stop at the first failure per entry so a
212
+ // multi-verifier setup doesn't produce duplicate violations for the
213
+ // same (name, version).
214
+ for (const verifier of verifiers) {
215
+ // eslint-disable-next-line no-await-in-loop
216
+ const result = await verifier.verify(resolution, { name, version });
217
+ if (!result.ok) {
218
+ violations.push({ name, version, resolution, code: result.code, reason: result.reason });
219
+ break;
220
+ }
221
+ }
222
+ })));
223
+ return violations;
224
+ }
225
+ //# sourceMappingURL=verifyLockfileResolutions.js.map
@@ -0,0 +1,80 @@
1
+ import type { ResolutionVerifier } from '@pnpm/resolving.resolver-base';
2
+ /**
3
+ * Subset of {@link ResolutionVerifier} the cache layer needs: the
4
+ * verifier's `policy` contribution plus the `canTrustPastCheck`
5
+ * comparator. `verify` is intentionally absent — the cache never runs
6
+ * verifiers, it just decides whether a previous run is still
7
+ * trustworthy.
8
+ */
9
+ export type VerifierCacheIdentity = Pick<ResolutionVerifier, 'policy' | 'canTrustPastCheck'>;
10
+ export interface CacheLookupResult {
11
+ hit: boolean;
12
+ /**
13
+ * stat + hash already computed during the lookup. When the caller
14
+ * follows up with {@link recordVerification} after running the gate,
15
+ * passing these back avoids re-stat'ing and (especially) re-hashing
16
+ * the lockfile a second time. Fields are undefined when the lookup
17
+ * couldn't (or didn't need to) compute them — `recordVerification`
18
+ * falls back to computing what's missing.
19
+ */
20
+ precomputed: {
21
+ stat?: LockfileStat;
22
+ hash?: string;
23
+ };
24
+ }
25
+ interface LockfileStat {
26
+ size: number;
27
+ mtimeNs: string;
28
+ inode: string;
29
+ }
30
+ export interface LockfileVerificationCacheKey {
31
+ lockfilePath: string;
32
+ verifiers: readonly VerifierCacheIdentity[];
33
+ /**
34
+ * Lazy: returns a stable hex hash of the in-memory lockfile. The
35
+ * cache invokes this only when the stat shortcut doesn't apply (the
36
+ * lockfile is at a new path, or its stat has drifted from the
37
+ * cached record). When the stat shortcut hits, the in-memory hash is
38
+ * never computed.
39
+ */
40
+ hashLockfile: () => string;
41
+ }
42
+ /**
43
+ * Try to confirm a cached verification covers the lockfile as it
44
+ * currently sits on disk and the policies currently in effect. Returns
45
+ * `{ hit: true }` to skip the gate; `{ hit: false }` means the caller
46
+ * should run the verifier and persist the result with
47
+ * {@link recordVerification}.
48
+ *
49
+ * Lookup order:
50
+ *
51
+ * 1. **Stat shortcut** — if we've previously verified this exact path
52
+ * with these exact stat values, trust the cached hash and skip
53
+ * reading the lockfile.
54
+ * 2. **Content lookup** — hash the lockfile and look up by hash.
55
+ * Catches the worktree case (same content, different path) and
56
+ * CI checkouts where stat fields got reset. Refreshes the
57
+ * stat-shortcut entry on hit so the next install at this path
58
+ * skips the hash.
59
+ *
60
+ * Every active verifier must agree the cached policy snapshot is still
61
+ * trustworthy under what it currently demands; if any rejects, the
62
+ * full gate runs.
63
+ */
64
+ export declare function tryLockfileVerificationCache(cacheDir: string, key: LockfileVerificationCacheKey): CacheLookupResult;
65
+ /**
66
+ * Persist a successful verification. Called after the gate passes; the
67
+ * lockfile is hashed once and the resulting record is appended to the
68
+ * cache file. If the file is past {@link MAX_CACHE_ENTRIES}, it is
69
+ * rewritten keeping the most recent entries.
70
+ *
71
+ * Reuses `precomputed` values from a prior
72
+ * {@link tryLockfileVerificationCache} lookup so we don't re-stat or
73
+ * (especially) re-hash the lockfile a second time on the miss-then-
74
+ * record path.
75
+ */
76
+ export declare function recordVerification(cacheDir: string, key: LockfileVerificationCacheKey, precomputed?: {
77
+ stat?: LockfileStat;
78
+ hash?: string;
79
+ }): void;
80
+ export {};