@dependably/npm-check 1.7.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/LICENSE +201 -0
- package/README.md +71 -0
- package/bin/cli.js +1577 -0
- package/package.json +103 -0
- package/src/audit-config.js +331 -0
- package/src/audit.js +805 -0
- package/src/backup.js +235 -0
- package/src/checker.js +771 -0
- package/src/checksum-fixer.js +419 -0
- package/src/deprecation.js +325 -0
- package/src/fixer.js +207 -0
- package/src/format-library.js +132 -0
- package/src/index.js +138 -0
- package/src/integrity.js +519 -0
- package/src/migrator.js +263 -0
- package/src/npmrc-validator.js +205 -0
- package/src/overrides.js +71 -0
- package/src/package-json-validator.js +356 -0
- package/src/parallel-processor.js +374 -0
- package/src/parser.js +133 -0
- package/src/performance.js +283 -0
- package/src/pinner.js +248 -0
- package/src/pnpm-format.js +229 -0
- package/src/pnpm-workspace-validator.js +121 -0
- package/src/progress-reporter.js +245 -0
- package/src/pruner.js +177 -0
- package/src/remediate.js +389 -0
- package/src/report.js +663 -0
- package/src/schema.js +76 -0
- package/src/streaming-parser.js +251 -0
- package/src/updater.js +251 -0
- package/src/usage-scanner.js +215 -0
- package/src/validator.js +282 -0
- package/src/vuln.js +618 -0
- package/src/workers/dedupe-worker.js +20 -0
- package/src/workers/hash-upgrade-worker.js +20 -0
- package/src/workers/migration-worker.js +21 -0
- package/src/workers/validation-worker.js +20 -0
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
// src/checksum-fixer.js
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { detectLockfileVersion, LOCKFILE_VERSIONS, forEachPackageEntry } from './format-library.js';
|
|
5
|
+
import {
|
|
6
|
+
fetchPackumentIntegrity,
|
|
7
|
+
generateIntegrityFromFile,
|
|
8
|
+
isPlaceholder,
|
|
9
|
+
deriveRegistryBase,
|
|
10
|
+
DEFAULT_REGISTRY
|
|
11
|
+
} from './integrity.js';
|
|
12
|
+
import { hashPackageDirectory, collectPackageFiles } from './checker.js';
|
|
13
|
+
|
|
14
|
+
// Re-exported for back-compat; the canonical definition now lives in integrity.js
|
|
15
|
+
export { deriveRegistryBase } from './integrity.js';
|
|
16
|
+
|
|
17
|
+
export class ChecksumFixError extends Error {
|
|
18
|
+
constructor(message, code, context = {}) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.name = 'ChecksumFixError';
|
|
21
|
+
this.code = code;
|
|
22
|
+
this.context = context;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Map items through an async fn with a concurrency cap.
|
|
28
|
+
* Results keep input order; rejections propagate.
|
|
29
|
+
*/
|
|
30
|
+
async function mapWithConcurrency(items, limit, fn) {
|
|
31
|
+
const results = new Array(items.length);
|
|
32
|
+
let next = 0;
|
|
33
|
+
const workers = Array.from({ length: Math.max(1, Math.min(limit, items.length)) }, async () => {
|
|
34
|
+
while (next < items.length) {
|
|
35
|
+
const index = next++;
|
|
36
|
+
results[index] = await fn(items[index], index);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
await Promise.all(workers);
|
|
40
|
+
return results;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const NEEDS_FIX_REASONS = {
|
|
44
|
+
missing: (integrity) => !integrity,
|
|
45
|
+
placeholder: (integrity) => isPlaceholder(integrity),
|
|
46
|
+
sha1: (integrity) => typeof integrity === 'string' && integrity.startsWith('sha1-')
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
function needsFix(integrity) {
|
|
50
|
+
if (NEEDS_FIX_REASONS.missing(integrity)) return 'missing';
|
|
51
|
+
if (NEEDS_FIX_REASONS.placeholder(integrity)) return 'placeholder';
|
|
52
|
+
if (NEEDS_FIX_REASONS.sha1(integrity)) return 'sha1';
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Build a change record for a resolved hash. Centralizes the common shape so
|
|
58
|
+
* each source (registry, local-file, local-directory) reads identically.
|
|
59
|
+
*/
|
|
60
|
+
function makeChange(key, entry, name, hash, source) {
|
|
61
|
+
return { packagePath: key, name, version: entry.version, from: entry.integrity || null, to: hash, source };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Classify a single lockfile entry into either a skip reason or a fix
|
|
66
|
+
* candidate. Returns the work item without recording it, so the caller owns
|
|
67
|
+
* the skipped/candidate buckets. file: tarballs become file-tarball
|
|
68
|
+
* candidates; file: directories are skipped.
|
|
69
|
+
*/
|
|
70
|
+
function classifyEntry(info, baseDir) {
|
|
71
|
+
const { key, entry, name, isRoot, isWorkspaceSource, isLink, isBundled, isGitDep, isFileDep } = info;
|
|
72
|
+
|
|
73
|
+
if (isRoot) return { skip: 'root' };
|
|
74
|
+
if (isWorkspaceSource) return { skip: 'workspace' };
|
|
75
|
+
if (isLink) return { skip: 'link' };
|
|
76
|
+
|
|
77
|
+
const reason = needsFix(entry.integrity);
|
|
78
|
+
if (!reason) return { skip: 'valid' };
|
|
79
|
+
|
|
80
|
+
if (isBundled) return { skip: 'bundled' };
|
|
81
|
+
if (isGitDep) return { skip: 'git' };
|
|
82
|
+
|
|
83
|
+
if (isFileDep) {
|
|
84
|
+
// file: specs are relative to the lockfile's directory, not the cwd
|
|
85
|
+
const filePath = path.resolve(baseDir, entry.resolved.slice('file:'.length).replace(/^\/\//, ''));
|
|
86
|
+
if (/\.(tgz|tar\.gz|tar)$/i.test(filePath)) {
|
|
87
|
+
return { candidate: { key, entry, name, fixReason: reason, source: 'file-tarball', filePath } };
|
|
88
|
+
}
|
|
89
|
+
return { skip: 'file-dir' };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return { candidate: { key, entry, name, fixReason: reason, source: 'registry' } };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Walk every package entry, sorting them into skips and fix candidates.
|
|
97
|
+
*/
|
|
98
|
+
function collectCandidates(lockfile, baseDir) {
|
|
99
|
+
const candidates = [];
|
|
100
|
+
const skipped = [];
|
|
101
|
+
let total = 0;
|
|
102
|
+
|
|
103
|
+
forEachPackageEntry(lockfile, (info) => {
|
|
104
|
+
total++;
|
|
105
|
+
const outcome = classifyEntry(info, baseDir);
|
|
106
|
+
if (outcome.skip) {
|
|
107
|
+
skipped.push({ packagePath: info.key, reason: outcome.skip });
|
|
108
|
+
} else {
|
|
109
|
+
candidates.push(outcome.candidate);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
return { candidates, skipped, total };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Resolve a hash for a file-tarball candidate from the local tarball, pushing
|
|
118
|
+
* either a change or an unresolved record.
|
|
119
|
+
*/
|
|
120
|
+
function resolveFileTarball(candidate, buckets) {
|
|
121
|
+
const { key, entry, name } = candidate;
|
|
122
|
+
const hash = generateIntegrityFromFile(candidate.filePath);
|
|
123
|
+
if (hash) {
|
|
124
|
+
buckets.changes.push(makeChange(key, entry, name, hash, 'local-file'));
|
|
125
|
+
} else {
|
|
126
|
+
buckets.unresolved.push({ packagePath: key, reason: `local tarball not readable: ${candidate.filePath}` });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Hash a registry candidate's local node_modules copy as a fallback. Records a
|
|
132
|
+
* local-directory change on success and returns true; returns false (leaving
|
|
133
|
+
* the entry unresolved) on any failure.
|
|
134
|
+
*
|
|
135
|
+
* Fix #17: derive the on-disk path from the lockfile key (the install path
|
|
136
|
+
* relative to the project root) rather than slicing at the last node_modules/
|
|
137
|
+
* segment. The old approach mapped nested packages (node_modules/a/node_modules/b)
|
|
138
|
+
* to the wrong hoisted location (node_modules/b). It also silently recorded a
|
|
139
|
+
* constant sha512-of-nothing when the directory was absent, because
|
|
140
|
+
* hashPackageDirectory swallows ENOENT rather than throwing. This function now
|
|
141
|
+
* enforces two invariants before delegating to the hasher:
|
|
142
|
+
* 1. Containment — the resolved path must stay inside baseDir (blocks traversal
|
|
143
|
+
* keys like node_modules/../../secret).
|
|
144
|
+
* 2. Existence — the directory must be present on disk; absent → unresolved.
|
|
145
|
+
*/
|
|
146
|
+
async function tryLocalFallback(candidate, baseDir, buckets) {
|
|
147
|
+
const { key, entry, name } = candidate;
|
|
148
|
+
|
|
149
|
+
// The lockfile key is the install path relative to the project root, so
|
|
150
|
+
// joining directly with baseDir gives the correct on-disk location for both
|
|
151
|
+
// top-level (node_modules/foo) and nested (node_modules/a/node_modules/b)
|
|
152
|
+
// packages without any segment-slicing.
|
|
153
|
+
const resolvedBase = path.resolve(baseDir);
|
|
154
|
+
const pkgDir = path.resolve(path.join(baseDir, key));
|
|
155
|
+
|
|
156
|
+
// Containment check (textual): reject any key that resolves outside the
|
|
157
|
+
// project root (e.g. node_modules/../../secret).
|
|
158
|
+
if (!pkgDir.startsWith(resolvedBase + path.sep)) {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Existence check: hashPackageDirectory digests zero bytes and returns a
|
|
163
|
+
// constant sha512-of-nothing for a missing directory rather than throwing —
|
|
164
|
+
// without this guard that garbage constant would be silently recorded.
|
|
165
|
+
if (!fs.existsSync(pkgDir)) {
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Symlink-aware containment: the textual resolve above can't see through a
|
|
170
|
+
// symlink at the package path (node_modules/x -> ../../outside). Resolve the
|
|
171
|
+
// real on-disk location and re-check it stays inside the (real) project root.
|
|
172
|
+
let realDir;
|
|
173
|
+
try {
|
|
174
|
+
realDir = fs.realpathSync(pkgDir);
|
|
175
|
+
} catch {
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
let realBase = resolvedBase;
|
|
179
|
+
try {
|
|
180
|
+
realBase = fs.realpathSync(resolvedBase);
|
|
181
|
+
} catch { /* base unreadable — fall back to the textual base */ }
|
|
182
|
+
// Require STRICT containment (a subpath), matching the textual check above. A
|
|
183
|
+
// package dir is always node_modules/... — never the project root itself — so
|
|
184
|
+
// a symlink resolving TO the root (which would hash the whole project) is
|
|
185
|
+
// correctly rejected rather than allowed by an equality carve-out.
|
|
186
|
+
if (!realDir.startsWith(realBase + path.sep)) {
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// A non-directory at the path can't be a package dir.
|
|
191
|
+
try {
|
|
192
|
+
if (!fs.statSync(realDir).isDirectory()) return false;
|
|
193
|
+
} catch {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// An empty or interrupted-install directory yields zero hashable files, which
|
|
198
|
+
// hashPackageDirectory digests to the constant sha512-of-nothing — recording
|
|
199
|
+
// that as a "fix" is worse than leaving the entry unresolved. Use the same
|
|
200
|
+
// file-collection the hasher uses, so the emptiness check matches exactly.
|
|
201
|
+
const hashableFiles = await collectPackageFiles(realDir);
|
|
202
|
+
if (hashableFiles.length === 0) {
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
try {
|
|
207
|
+
const localHash = await hashPackageDirectory(realDir);
|
|
208
|
+
buckets.changes.push(makeChange(key, entry, name, localHash, 'local-directory'));
|
|
209
|
+
return true;
|
|
210
|
+
} catch {
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Record why a registry candidate could not be resolved, distinguishing a
|
|
217
|
+
* network failure, a versionless entry, and a registry with no sha512 hash.
|
|
218
|
+
*/
|
|
219
|
+
function recordUnresolved(candidate, networkError, localFallback, buckets) {
|
|
220
|
+
const { key, entry, name, fixReason } = candidate;
|
|
221
|
+
if (networkError) {
|
|
222
|
+
buckets.unresolved.push({ packagePath: key, reason: `registry unreachable (${networkError.message})${localFallback ? '' : '; consider --local-fallback'}` });
|
|
223
|
+
} else if (!entry.version) {
|
|
224
|
+
buckets.unresolved.push({ packagePath: key, reason: `cannot fix ${fixReason} integrity: entry has no version` });
|
|
225
|
+
} else {
|
|
226
|
+
buckets.unresolved.push({ packagePath: key, reason: `registry has no sha512 integrity for ${name}@${entry.version}` });
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Resolve a hash for a registry candidate: try the registry, then the optional
|
|
232
|
+
* local fallback, then record an unresolved reason.
|
|
233
|
+
*/
|
|
234
|
+
async function resolveRegistryCandidate(candidate, settings, buckets) {
|
|
235
|
+
const { key, entry, name } = candidate;
|
|
236
|
+
const { fetcher, defaultRegistry, localFallback, baseDir } = settings;
|
|
237
|
+
|
|
238
|
+
const registryBase = deriveRegistryBase(entry.resolved, name) || defaultRegistry;
|
|
239
|
+
let hash = null;
|
|
240
|
+
let networkError = null;
|
|
241
|
+
try {
|
|
242
|
+
hash = entry.version ? await fetcher(name, entry.version, registryBase) : null;
|
|
243
|
+
} catch (e) {
|
|
244
|
+
networkError = e;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (hash) {
|
|
248
|
+
buckets.changes.push(makeChange(key, entry, name, hash, 'registry'));
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (localFallback && await tryLocalFallback(candidate, baseDir, buckets)) {
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
recordUnresolved(candidate, networkError, localFallback, buckets);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Navigate to the node in the legacy v2 dependencies tree that corresponds to
|
|
261
|
+
* a packages-map key, returning the leaf object or null when the path is
|
|
262
|
+
* absent.
|
|
263
|
+
*
|
|
264
|
+
* "node_modules/foo" → dependencies["foo"]
|
|
265
|
+
* "node_modules/a/node_modules/b" → dependencies["a"]["dependencies"]["b"]
|
|
266
|
+
* "node_modules/@scope/pkg" → dependencies["@scope/pkg"]
|
|
267
|
+
*/
|
|
268
|
+
function getDepsNode(dependencies, pkgKey) {
|
|
269
|
+
// Only handle keys rooted at "node_modules/…"; workspace-level keys are skipped.
|
|
270
|
+
if (!pkgKey.startsWith('node_modules/')) return null;
|
|
271
|
+
// Slice off the leading "node_modules/" then split the remainder by the
|
|
272
|
+
// nested-package delimiter to obtain the name chain.
|
|
273
|
+
// "node_modules/foo" → rest="foo" → names=["foo"]
|
|
274
|
+
// "node_modules/a/node_modules/b" → rest="a/node_modules/b" → names=["a","b"]
|
|
275
|
+
const names = pkgKey.slice('node_modules/'.length).split('/node_modules/');
|
|
276
|
+
let current = dependencies;
|
|
277
|
+
for (let i = 0; i < names.length - 1; i++) {
|
|
278
|
+
current = current[names[i]]?.dependencies;
|
|
279
|
+
if (!current) return null;
|
|
280
|
+
}
|
|
281
|
+
return current[names[names.length - 1]] ?? null;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Apply the collected integrity changes to a shallow copy of the lockfile.
|
|
286
|
+
*
|
|
287
|
+
* Fix #19: v2 lockfiles carry both a packages map and a legacy dependencies
|
|
288
|
+
* tree. The old code only updated packages, leaving the dependencies tree with
|
|
289
|
+
* stale hashes — the exact inconsistency the validator flags as an error.
|
|
290
|
+
* When lockfile.dependencies is present, each change is now mirrored into the
|
|
291
|
+
* corresponding node of the legacy tree so both sections stay consistent.
|
|
292
|
+
*/
|
|
293
|
+
function applyChanges(lockfile, changes) {
|
|
294
|
+
const updated = {
|
|
295
|
+
...lockfile,
|
|
296
|
+
packages: { ...lockfile.packages }
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
// Deep-copy the legacy dependencies tree once so we can mutate the copy
|
|
300
|
+
// without touching the input lockfile.
|
|
301
|
+
const updatedDeps = lockfile.dependencies
|
|
302
|
+
? JSON.parse(JSON.stringify(lockfile.dependencies))
|
|
303
|
+
: null;
|
|
304
|
+
|
|
305
|
+
for (const change of changes) {
|
|
306
|
+
updated.packages[change.packagePath] = {
|
|
307
|
+
...updated.packages[change.packagePath],
|
|
308
|
+
integrity: change.to
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
// Mirror into the v2 legacy dependencies tree when present.
|
|
312
|
+
if (updatedDeps) {
|
|
313
|
+
const node = getDepsNode(updatedDeps, change.packagePath);
|
|
314
|
+
if (node) {
|
|
315
|
+
node.integrity = change.to;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (updatedDeps) {
|
|
321
|
+
updated.dependencies = updatedDeps;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return updated;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Fill missing, placeholder, or weak (sha1) integrity hashes with real ones.
|
|
329
|
+
* Fetches the authoritative dist.integrity from the package's registry
|
|
330
|
+
* (derived per-package from its resolved URL); optionally falls back to
|
|
331
|
+
* hashing the local node_modules copy — those hashes are flagged because
|
|
332
|
+
* they are NOT npm tarball hashes and npm ci will not verify them.
|
|
333
|
+
*
|
|
334
|
+
* @param {object} lockfile - Parsed lockfile (v2/v3)
|
|
335
|
+
* @param {object} options
|
|
336
|
+
* @returns {Promise<{lockfile, changes, unresolved, skipped, warnings, summary}>}
|
|
337
|
+
*/
|
|
338
|
+
export async function fixChecksums(lockfile, options = {}) {
|
|
339
|
+
const {
|
|
340
|
+
onProgress = null,
|
|
341
|
+
concurrency = 8,
|
|
342
|
+
timeoutMs = 10000,
|
|
343
|
+
localFallback = false,
|
|
344
|
+
// `nodeModulesPath` is still accepted in options for API compat but no longer
|
|
345
|
+
// used — the on-disk path is derived from baseDir + the lockfile key (fix #17).
|
|
346
|
+
defaultRegistry = DEFAULT_REGISTRY,
|
|
347
|
+
fetchIntegrity = null,
|
|
348
|
+
baseDir = '.'
|
|
349
|
+
} = options;
|
|
350
|
+
|
|
351
|
+
if (detectLockfileVersion(lockfile) === LOCKFILE_VERSIONS.V1) {
|
|
352
|
+
throw new ChecksumFixError(
|
|
353
|
+
'v1 lockfiles are not supported; run `npm-check migrate 3` first',
|
|
354
|
+
'UNSUPPORTED_VERSION'
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const fetcher = fetchIntegrity ||
|
|
359
|
+
((name, ver, registryBase) => fetchPackumentIntegrity(name, ver, { registryBase, timeoutMs }));
|
|
360
|
+
|
|
361
|
+
const { candidates, skipped, total } = collectCandidates(lockfile, baseDir);
|
|
362
|
+
|
|
363
|
+
const buckets = { changes: [], unresolved: [], warnings: [] };
|
|
364
|
+
let completed = 0;
|
|
365
|
+
|
|
366
|
+
const reportProgress = (stage) => {
|
|
367
|
+
if (onProgress) {
|
|
368
|
+
onProgress({
|
|
369
|
+
current: completed,
|
|
370
|
+
total: candidates.length,
|
|
371
|
+
percentage: candidates.length === 0 ? 100 : Math.round((completed / candidates.length) * 100),
|
|
372
|
+
stage
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
reportProgress('Fetching integrity hashes');
|
|
378
|
+
|
|
379
|
+
const settings = { fetcher, defaultRegistry, localFallback, baseDir };
|
|
380
|
+
await mapWithConcurrency(candidates, concurrency, async (candidate) => {
|
|
381
|
+
try {
|
|
382
|
+
if (candidate.source === 'file-tarball') {
|
|
383
|
+
resolveFileTarball(candidate, buckets);
|
|
384
|
+
} else {
|
|
385
|
+
await resolveRegistryCandidate(candidate, settings, buckets);
|
|
386
|
+
}
|
|
387
|
+
} finally {
|
|
388
|
+
completed++;
|
|
389
|
+
reportProgress('Fetching integrity hashes');
|
|
390
|
+
}
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
const { changes, unresolved, warnings } = buckets;
|
|
394
|
+
|
|
395
|
+
const localDirCount = changes.filter((c) => c.source === 'local-directory').length;
|
|
396
|
+
if (localDirCount > 0) {
|
|
397
|
+
warnings.push(
|
|
398
|
+
`${localDirCount} hash(es) were computed from node_modules directories; these are NOT npm ` +
|
|
399
|
+
'tarball hashes and `npm ci` will fail integrity verification against the registry. ' +
|
|
400
|
+
'Use only for air-gapped/internal verification.'
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
return {
|
|
405
|
+
lockfile: applyChanges(lockfile, changes),
|
|
406
|
+
changes,
|
|
407
|
+
unresolved,
|
|
408
|
+
skipped,
|
|
409
|
+
warnings,
|
|
410
|
+
summary: {
|
|
411
|
+
total,
|
|
412
|
+
candidates: candidates.length,
|
|
413
|
+
fixedFromRegistry: changes.filter((c) => c.source === 'registry').length,
|
|
414
|
+
fixedFromLocal: changes.filter((c) => c.source === 'local-directory' || c.source === 'local-file').length,
|
|
415
|
+
unresolved: unresolved.length,
|
|
416
|
+
skipped: skipped.length
|
|
417
|
+
}
|
|
418
|
+
};
|
|
419
|
+
}
|