@loomhelix/deptrail 0.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.
Files changed (103) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +421 -0
  3. package/dist/actions/action-error.js +11 -0
  4. package/dist/actions/action-jobs.js +199 -0
  5. package/dist/actions/action-service.js +268 -0
  6. package/dist/actions/apply.js +120 -0
  7. package/dist/actions/availability.js +79 -0
  8. package/dist/actions/change-sets.js +117 -0
  9. package/dist/actions/dry-run.js +287 -0
  10. package/dist/actions/edge-moves.js +108 -0
  11. package/dist/actions/edit-plan.js +179 -0
  12. package/dist/actions/file-scope.js +118 -0
  13. package/dist/actions/fix-options.js +141 -0
  14. package/dist/actions/goal-check.js +80 -0
  15. package/dist/actions/install.js +27 -0
  16. package/dist/actions/job-registry.js +104 -0
  17. package/dist/actions/json-edit.js +189 -0
  18. package/dist/actions/labels.js +35 -0
  19. package/dist/actions/node-actions.js +102 -0
  20. package/dist/actions/overrides-location.js +59 -0
  21. package/dist/actions/parent-chain.js +210 -0
  22. package/dist/actions/range-style.js +38 -0
  23. package/dist/actions/recipes.js +31 -0
  24. package/dist/actions/root-files.js +30 -0
  25. package/dist/actions/sandbox.js +175 -0
  26. package/dist/actions/set-range.js +59 -0
  27. package/dist/actions/shared-packages.js +50 -0
  28. package/dist/actions/text-diff.js +203 -0
  29. package/dist/actions/types.js +1 -0
  30. package/dist/actions/update-options.js +232 -0
  31. package/dist/actions/workspace-edit.js +33 -0
  32. package/dist/adapters/command-runner.js +127 -0
  33. package/dist/adapters/manifest-only.js +20 -0
  34. package/dist/adapters/node-modules.js +148 -0
  35. package/dist/adapters/npm-lock.js +149 -0
  36. package/dist/adapters/package-manifest.js +57 -0
  37. package/dist/adapters/pnpm-lock.js +177 -0
  38. package/dist/adapters/resolve-today.js +93 -0
  39. package/dist/cli/cli.js +227 -0
  40. package/dist/cli/open-browser.js +14 -0
  41. package/dist/cli/package-version.js +6 -0
  42. package/dist/cli/usage-text.js +27 -0
  43. package/dist/config/glob.js +119 -0
  44. package/dist/config/npmrc.js +116 -0
  45. package/dist/config/root-config.js +103 -0
  46. package/dist/config/secrets.js +18 -0
  47. package/dist/discovery/find-roots.js +107 -0
  48. package/dist/enrich/credentials.js +143 -0
  49. package/dist/enrich/enrichment-cache.js +42 -0
  50. package/dist/enrich/enrichment-keys.js +71 -0
  51. package/dist/enrich/enrichment-service.js +399 -0
  52. package/dist/enrich/enrichment-status.js +86 -0
  53. package/dist/enrich/enrichment-store.js +143 -0
  54. package/dist/enrich/http.js +201 -0
  55. package/dist/enrich/on-demand.js +220 -0
  56. package/dist/enrich/osv-client.js +260 -0
  57. package/dist/enrich/registry-client.js +123 -0
  58. package/dist/enrich/types.js +1 -0
  59. package/dist/git/blob-reader.js +180 -0
  60. package/dist/git/git-commands.js +32 -0
  61. package/dist/git/git-process.js +71 -0
  62. package/dist/git/git-repository.js +136 -0
  63. package/dist/git/ref-list.js +78 -0
  64. package/dist/git/tree-index.js +87 -0
  65. package/dist/graph/global-index.js +19 -0
  66. package/dist/graph/root-graph-builder.js +114 -0
  67. package/dist/graph/serialize.js +19 -0
  68. package/dist/model.js +2 -0
  69. package/dist/query/children.js +38 -0
  70. package/dist/query/compare-shape.js +13 -0
  71. package/dist/query/compare.js +245 -0
  72. package/dist/query/decorations.js +121 -0
  73. package/dist/query/dependents.js +135 -0
  74. package/dist/query/duplicates.js +23 -0
  75. package/dist/query/enrichment-lookup.js +78 -0
  76. package/dist/query/fixes.js +60 -0
  77. package/dist/query/graph-diff.js +42 -0
  78. package/dist/query/issues.js +235 -0
  79. package/dist/query/lookup.js +49 -0
  80. package/dist/query/node-fix.js +46 -0
  81. package/dist/query/package-insight.js +112 -0
  82. package/dist/query/search.js +61 -0
  83. package/dist/query/subgraph.js +107 -0
  84. package/dist/query/versions.js +103 -0
  85. package/dist/scan/build-root.js +137 -0
  86. package/dist/scan/cache-shape.js +52 -0
  87. package/dist/scan/scan-cache.js +32 -0
  88. package/dist/scan/scan-session.js +127 -0
  89. package/dist/scan/source-registry.js +55 -0
  90. package/dist/server/auth.js +45 -0
  91. package/dist/server/branches.js +105 -0
  92. package/dist/server/error-log.js +9 -0
  93. package/dist/server/root-segment.js +13 -0
  94. package/dist/server/router.js +474 -0
  95. package/dist/server/server.js +166 -0
  96. package/dist/server/source-key.js +1 -0
  97. package/dist/server/static.js +59 -0
  98. package/dist/sources/file-reader.js +55 -0
  99. package/dist/sources/git-reader.js +45 -0
  100. package/dist/ui/assets/index-C4uyB5v3.css +1 -0
  101. package/dist/ui/assets/index-DYAG2LM-.js +51 -0
  102. package/dist/ui/index.html +15 -0
  103. package/package.json +72 -0
@@ -0,0 +1,399 @@
1
+ import { posix } from 'node:path';
2
+ import { createSecretSet } from "../config/secrets.js";
3
+ import { readRegistryCredentials } from "./credentials.js";
4
+ import { isFresh } from "./enrichment-cache.js";
5
+ import { hasKnownVersion, inPrivateScope, isPlaceholder, isRegistryPackage, planKeys, registryKeyOf, registryUrlOf, } from "./enrichment-keys.js";
6
+ import { createRevisionCounter, offlineStatus, problemsOf, progressOf } from "./enrichment-status.js";
7
+ import { ADVISORIES_VIEWS, advisoriesCover, createCacheAccess, createLimiter, createRecordStore, isCachedAdvisories, isCachedMatch, isCachedRegistry, MATCH_VIEWS, REGISTRY_VIEWS, registryOk, } from "./enrichment-store.js";
8
+ import { HTTP_DEFAULTS, HttpError } from "./http.js";
9
+ import { createOnDemandLookups } from "./on-demand.js";
10
+ import { OSV_DEFAULT_URL, queryPackageAdvisories, queryVersions, versionKey } from "./osv-client.js";
11
+ import { failureReason, fetchPackageMeta, metaCovers, packumentUrl } from "./registry-client.js";
12
+ const NO_AUTHORIZATION_MARKER = 'no-authorization';
13
+ const DEFAULT_REVISION_INTERVAL_MS = 1000;
14
+ const CACHE_CONCURRENCY = 64;
15
+ const REGISTRY_REQUESTS_PER_URL = HTTP_DEFAULTS.concurrencyPerOrigin;
16
+ const OSV_PARALLEL_PACKAGE_QUERIES = 8;
17
+ const NPMRC_FILE = '.npmrc';
18
+ const NOT_MODIFIED_WITHOUT_VALIDATOR = 'responded with HTTP 304';
19
+ const MISSING_OSV_RESULT = 'returned a malformed response';
20
+ const PENDING = { state: 'pending' };
21
+ const OFFLINE = { state: 'offline' };
22
+ const NOT_APPLICABLE = { state: 'not-applicable' };
23
+ const PRIVATE_SCOPE_SKIP = { state: 'skipped', reason: 'private-scope' };
24
+ const NON_REGISTRY_SKIP = { state: 'skipped', reason: 'non-registry' };
25
+ const UNKNOWN_VERSION_SKIP = { state: 'skipped', reason: 'unknown-version' };
26
+ const EMPTY_READER = {
27
+ readText: async () => null,
28
+ exists: async () => false,
29
+ listDir: async () => [],
30
+ realpath: async () => null,
31
+ };
32
+ const NO_NETWORK = {
33
+ request: async () => {
34
+ throw new HttpError('network');
35
+ },
36
+ };
37
+ function omitsVersionInUse(record, versionsInUse) {
38
+ if (record.state !== 'ok')
39
+ return false;
40
+ const listed = new Set(record.meta.versions);
41
+ return versionsInUse.some((version) => hasKnownVersion(version) && !listed.has(version));
42
+ }
43
+ export function createEnrichmentService(options) {
44
+ const { reader, env, userNpmrc, http, osvUrl, secrets, offline } = options;
45
+ const now = options.now ?? Date.now;
46
+ const cache = createCacheAccess(options.cache, CACHE_CONCURRENCY);
47
+ const limitPackageQueries = createLimiter(OSV_PARALLEL_PACKAGE_QUERIES);
48
+ const registryLimiters = new Map();
49
+ const refetchedForUnlistedVersion = new Set();
50
+ const revisions = createRevisionCounter(options.revisionIntervalMs ?? DEFAULT_REVISION_INTERVAL_MS);
51
+ const landed = () => revisions.resultLanded();
52
+ const registryStore = createRecordStore(REGISTRY_VIEWS, landed);
53
+ const matchStore = createRecordStore(MATCH_VIEWS, landed);
54
+ const advisoriesStore = createRecordStore(ADVISORIES_VIEWS, landed);
55
+ const activePasses = new Set();
56
+ const versionsWantedInFlight = new Map();
57
+ const credentialsByRootPath = new Map();
58
+ let trackedRoots = [];
59
+ let currentPlan = planKeys([], new Set());
60
+ let finishedAt;
61
+ let closed = false;
62
+ const nowIso = () => new Date(now()).toISOString();
63
+ const isFreshNow = (fetchedAt) => isFresh(fetchedAt, now());
64
+ function awaitsRefetchForUnlistedVersion(key, record, versionsInUse) {
65
+ return !refetchedForUnlistedVersion.has(key) && omitsVersionInUse(record, versionsInUse);
66
+ }
67
+ function registryRecordUsable(key, record, versionsInUse) {
68
+ if (!isFreshNow(record.fetchedAt))
69
+ return false;
70
+ if (record.state === 'not-found')
71
+ return true;
72
+ return metaCovers(record.meta, versionsInUse) && !awaitsRefetchForUnlistedVersion(key, record, versionsInUse);
73
+ }
74
+ function advisoriesUsable(record, reported) {
75
+ return isFreshNow(record.fetchedAt) && advisoriesCover(record.advisories, reported);
76
+ }
77
+ function credentialsOf(root) {
78
+ const known = credentialsByRootPath.get(root.path);
79
+ if (known !== undefined)
80
+ return known;
81
+ const reading = reader
82
+ .readText(posix.join(root.path, NPMRC_FILE))
83
+ .catch(() => null)
84
+ .then((projectNpmrc) => readRegistryCredentials({ projectNpmrc, userNpmrc, env }, secrets));
85
+ credentialsByRootPath.set(root.path, reading);
86
+ return reading;
87
+ }
88
+ async function storeRegistry(target, record) {
89
+ registryStore.succeed(target.key, record);
90
+ await cache.write('registry', [target.registryUrl, target.name], record);
91
+ }
92
+ function registryLimiterOf(registryUrl) {
93
+ const known = registryLimiters.get(registryUrl);
94
+ if (known !== undefined)
95
+ return known;
96
+ const limiter = createLimiter(REGISTRY_REQUESTS_PER_URL);
97
+ registryLimiters.set(registryUrl, limiter);
98
+ return limiter;
99
+ }
100
+ function credentialsKeyOf(target, authorizationFor) {
101
+ const authorization = authorizationFor(packumentUrl(target.registryUrl, target.name));
102
+ return `${target.registryUrl}\n${authorization ?? NO_AUTHORIZATION_MARKER}`;
103
+ }
104
+ function rememberRejectedCredentials(pass, credentialsKey, result) {
105
+ if (result.state !== 'failed' || !result.authRejected || pass.rejectedCredentials.has(credentialsKey))
106
+ return;
107
+ pass.rejectedCredentials.set(credentialsKey, result.reason);
108
+ }
109
+ async function fetchRegistryRecord(target, versionsInUse, validatorCandidate, authorizationFor, credentialsKey, pass) {
110
+ const validator = validatorCandidate?.state === 'ok' && metaCovers(validatorCandidate.meta, versionsInUse) ? validatorCandidate : undefined;
111
+ const result = await fetchPackageMeta(http, {
112
+ registry: target.registryUrl,
113
+ name: target.name,
114
+ etag: validator?.etag,
115
+ versionsInUse,
116
+ authorizationFor,
117
+ });
118
+ rememberRejectedCredentials(pass, credentialsKey, result);
119
+ const fetchedAt = nowIso();
120
+ if (result.state === 'ok')
121
+ return { record: registryOk(result.meta, fetchedAt, result.etag) };
122
+ if (result.state === 'not-found')
123
+ return { record: { state: 'not-found', fetchedAt } };
124
+ if (result.state === 'not-modified' && validator !== undefined) {
125
+ return { record: registryOk(validator.meta, fetchedAt, validator.etag) };
126
+ }
127
+ return { failure: result.state === 'failed' ? result.reason : NOT_MODIFIED_WITHOUT_VALIDATOR };
128
+ }
129
+ function fetchUnlessCredentialsRejected(target, versionsInUse, validatorCandidate, authorizationFor, pass) {
130
+ const credentialsKey = credentialsKeyOf(target, authorizationFor);
131
+ return registryLimiterOf(target.registryUrl)(async () => {
132
+ const rejection = pass.rejectedCredentials.get(credentialsKey);
133
+ if (rejection !== undefined)
134
+ return { failure: rejection };
135
+ return fetchRegistryRecord(target, versionsInUse, validatorCandidate, authorizationFor, credentialsKey, pass);
136
+ });
137
+ }
138
+ function missesVersionsAddedMeanwhile(record, requested, wanted) {
139
+ return wanted.size > requested.length && record.state === 'ok' && !metaCovers(record.meta, [...wanted]);
140
+ }
141
+ async function resolveRegistryTarget(target, pass) {
142
+ const wanted = new Set(target.versionsInUse);
143
+ versionsWantedInFlight.set(target.key, wanted);
144
+ let lastGood;
145
+ try {
146
+ const cached = await cache.read('registry', [target.registryUrl, target.name], isCachedRegistry);
147
+ if (cached !== undefined && !pass.forceExpired && registryRecordUsable(target.key, cached, [...wanted])) {
148
+ registryStore.succeed(target.key, cached);
149
+ return;
150
+ }
151
+ lastGood = cached ?? registryStore.lastGoodOf(target.key);
152
+ const { authorizationFor } = await credentialsOf(target.credentialsRoot);
153
+ for (;;) {
154
+ const requested = [...wanted];
155
+ const refetchesUnlistedVersion = lastGood !== undefined && omitsVersionInUse(lastGood, requested);
156
+ if (refetchesUnlistedVersion)
157
+ refetchedForUnlistedVersion.add(target.key);
158
+ const validatorCandidate = refetchesUnlistedVersion ? undefined : lastGood;
159
+ const fetched = await fetchUnlessCredentialsRejected(target, requested, validatorCandidate, authorizationFor, pass);
160
+ if ('failure' in fetched) {
161
+ registryStore.fail(target.key, fetched.failure, lastGood);
162
+ return;
163
+ }
164
+ await storeRegistry(target, fetched.record);
165
+ const refetchNeeded = missesVersionsAddedMeanwhile(fetched.record, requested, wanted) ||
166
+ awaitsRefetchForUnlistedVersion(target.key, fetched.record, [...wanted]);
167
+ if (!refetchNeeded)
168
+ return;
169
+ lastGood = fetched.record;
170
+ }
171
+ }
172
+ catch (error) {
173
+ registryStore.fail(target.key, failureReason(error), lastGood);
174
+ }
175
+ finally {
176
+ registryStore.release(target.key);
177
+ versionsWantedInFlight.delete(target.key);
178
+ }
179
+ }
180
+ function addVersionsToFetchesInFlight(plan) {
181
+ for (const target of plan.registry.values()) {
182
+ const wanted = versionsWantedInFlight.get(target.key);
183
+ if (wanted === undefined)
184
+ continue;
185
+ for (const version of target.versionsInUse)
186
+ wanted.add(version);
187
+ }
188
+ }
189
+ async function queryUnresolved(unresolved) {
190
+ if (unresolved.length === 0)
191
+ return;
192
+ let outcome;
193
+ try {
194
+ outcome = await queryVersions(http, osvUrl, unresolved.map(({ target }) => ({ name: target.name, version: target.version })));
195
+ }
196
+ catch (error) {
197
+ const reason = failureReason(error);
198
+ outcome = { matches: new Map(), failed: new Map(unresolved.map(({ target }) => [target.key, reason])) };
199
+ }
200
+ const fetchedAt = nowIso();
201
+ const writes = unresolved.map(({ target, lastGood }) => {
202
+ const vulns = outcome.matches.get(target.key);
203
+ if (vulns === undefined) {
204
+ matchStore.fail(target.key, outcome.failed.get(target.key) ?? MISSING_OSV_RESULT, lastGood);
205
+ return undefined;
206
+ }
207
+ const record = { state: 'ok', vulns, fetchedAt };
208
+ matchStore.succeed(target.key, record);
209
+ return cache.write('osv-matches', [target.key], record);
210
+ });
211
+ await Promise.all(writes);
212
+ }
213
+ async function resolveVersionTargets(targets, pass) {
214
+ try {
215
+ const unresolved = [];
216
+ await Promise.all(targets.map(async (target) => {
217
+ const cached = await cache.read('osv-matches', [target.key], isCachedMatch);
218
+ if (cached !== undefined && !pass.forceExpired && isFreshNow(cached.fetchedAt))
219
+ matchStore.succeed(target.key, cached);
220
+ else
221
+ unresolved.push({ target, lastGood: cached ?? matchStore.lastGoodOf(target.key) });
222
+ }));
223
+ await queryUnresolved(unresolved);
224
+ }
225
+ finally {
226
+ for (const target of targets)
227
+ matchStore.release(target.key);
228
+ }
229
+ }
230
+ function reportedVulnsByName(plan) {
231
+ const reported = new Map();
232
+ for (const target of plan.versions.values()) {
233
+ const state = matchStore.stateOf(target.key);
234
+ if (state === undefined || (state.state !== 'ok' && state.state !== 'stale') || state.vulns.length === 0)
235
+ continue;
236
+ const refs = reported.get(target.name) ?? [];
237
+ refs.push(...state.vulns);
238
+ reported.set(target.name, refs);
239
+ }
240
+ return reported;
241
+ }
242
+ async function resolveAdvisoriesTarget(target, pass) {
243
+ let lastGood;
244
+ try {
245
+ const cached = await cache.read('osv-packages', [target.key], isCachedAdvisories);
246
+ if (cached !== undefined && !pass.forceExpired && advisoriesUsable(cached, target.reported)) {
247
+ advisoriesStore.succeed(target.key, cached);
248
+ return;
249
+ }
250
+ lastGood = cached ?? advisoriesStore.lastGoodOf(target.key);
251
+ const result = await limitPackageQueries(() => queryPackageAdvisories(http, osvUrl, target.key));
252
+ if (result.state === 'failed') {
253
+ advisoriesStore.fail(target.key, result.reason, lastGood);
254
+ return;
255
+ }
256
+ const record = { fetchedAt: nowIso(), advisories: result.advisories };
257
+ advisoriesStore.succeed(target.key, record);
258
+ await cache.write('osv-packages', [target.key], record);
259
+ }
260
+ catch (error) {
261
+ advisoriesStore.fail(target.key, failureReason(error), lastGood);
262
+ }
263
+ finally {
264
+ advisoriesStore.release(target.key);
265
+ }
266
+ }
267
+ async function resolveOsv(targets, pass) {
268
+ await resolveVersionTargets(targets, pass);
269
+ const advisoryTargets = advisoriesStore.claim([...reportedVulnsByName(pass.plan)].map(([key, reported]) => ({ key, reported })), (target) => !pass.forceExpired && advisoriesStore.isKnown(target.key, (record) => advisoriesUsable(record, target.reported)));
270
+ await Promise.all(advisoryTargets.map((target) => resolveAdvisoriesTarget(target, pass)));
271
+ }
272
+ function onlineWork(pass) {
273
+ const { plan, forceExpired } = pass;
274
+ addVersionsToFetchesInFlight(plan);
275
+ const registryTargets = registryStore.claim(plan.registry.values(), (target) => !forceExpired && registryStore.isKnown(target.key, (record) => registryRecordUsable(target.key, record, [...target.versionsInUse])));
276
+ const work = registryTargets.map((target) => resolveRegistryTarget(target, pass));
277
+ if (plan.waitsForScanningRoots)
278
+ return work;
279
+ const versionTargets = matchStore.claim(plan.versions.values(), (target) => !forceExpired && matchStore.isKnown(target.key, (record) => isFreshNow(record.fetchedAt)));
280
+ return [...work, resolveOsv(versionTargets, pass)];
281
+ }
282
+ function runPass(roots, forceExpired) {
283
+ const plan = planKeys(roots, currentPlan.privateScopes);
284
+ const pass = { plan, forceExpired, rejectedCredentials: new Map() };
285
+ const credentialReads = plan.roots.map(credentialsOf);
286
+ if (!offline)
287
+ currentPlan = plan;
288
+ const work = Promise.allSettled([...credentialReads, ...(offline ? [] : onlineWork(pass))]);
289
+ const running = work.finally(() => {
290
+ activePasses.delete(running);
291
+ if (activePasses.size > 0)
292
+ return;
293
+ revisions.drained();
294
+ finishedAt = nowIso();
295
+ });
296
+ activePasses.add(running);
297
+ }
298
+ function matchOfVersion(name, version) {
299
+ if (offline)
300
+ return OFFLINE;
301
+ if (inPrivateScope(name, currentPlan.privateScopes))
302
+ return PRIVATE_SCOPE_SKIP;
303
+ const state = matchStore.stateOf(versionKey(name, version));
304
+ if (state !== undefined)
305
+ return state;
306
+ return hasKnownVersion(version) ? PENDING : UNKNOWN_VERSION_SKIP;
307
+ }
308
+ function onlineStatus(anyRootScanning) {
309
+ const running = activePasses.size > 0 || anyRootScanning;
310
+ const osv = progressOf(currentPlan.versions.keys(), matchStore);
311
+ return {
312
+ state: running ? 'running' : 'done',
313
+ revision: revisions.current,
314
+ registry: progressOf(currentPlan.registry.keys(), registryStore),
315
+ osv: { total: osv.total, done: osv.done, skipped: currentPlan.skippedVersions, failed: osv.failed },
316
+ problems: problemsOf(currentPlan, {
317
+ registry: registryStore,
318
+ matches: matchStore,
319
+ advisories: advisoriesStore,
320
+ osvUrl,
321
+ secrets,
322
+ }),
323
+ ...(running || finishedAt === undefined ? {} : { finishedAt }),
324
+ };
325
+ }
326
+ const onDemand = createOnDemandLookups({
327
+ http,
328
+ osvUrl,
329
+ offline,
330
+ cache,
331
+ now,
332
+ credentialsOf,
333
+ registryLimiterOf,
334
+ limitPackageQueries,
335
+ privateScopes: () => currentPlan.privateScopes,
336
+ storedMatchOf: matchStore.stateOf,
337
+ storedAdvisoriesOf: advisoriesStore.stateOf,
338
+ });
339
+ return {
340
+ get revision() {
341
+ return revisions.current;
342
+ },
343
+ registryOf(root, node) {
344
+ if (!isRegistryPackage(node))
345
+ return NOT_APPLICABLE;
346
+ if (offline)
347
+ return OFFLINE;
348
+ if (root.config === undefined)
349
+ return PENDING;
350
+ return registryStore.stateOf(registryKeyOf(registryUrlOf(root.config, node.name), node.name)) ?? PENDING;
351
+ },
352
+ matchOf(_root, node) {
353
+ if (isRegistryPackage(node))
354
+ return matchOfVersion(node.name, node.version);
355
+ return isPlaceholder(node) ? NOT_APPLICABLE : NON_REGISTRY_SKIP;
356
+ },
357
+ matchOfVersion,
358
+ advisoriesOf(name) {
359
+ return advisoriesStore.stateOf(name) ?? PENDING;
360
+ },
361
+ track(roots) {
362
+ if (closed)
363
+ return;
364
+ trackedRoots = roots;
365
+ runPass(roots, false);
366
+ },
367
+ refresh() {
368
+ if (closed)
369
+ return;
370
+ credentialsByRootPath.clear();
371
+ runPass(trackedRoots, true);
372
+ },
373
+ status(anyRootScanning) {
374
+ return offline ? offlineStatus(revisions.current) : onlineStatus(anyRootScanning);
375
+ },
376
+ async settled() {
377
+ while (activePasses.size > 0)
378
+ await Promise.all(activePasses);
379
+ },
380
+ close() {
381
+ closed = true;
382
+ options.abort?.abort();
383
+ },
384
+ publishedVersions: onDemand.publishedVersions,
385
+ assess: onDemand.assess,
386
+ };
387
+ }
388
+ export function offlineEnrichment() {
389
+ return createEnrichmentService({
390
+ reader: EMPTY_READER,
391
+ env: {},
392
+ userNpmrc: null,
393
+ http: NO_NETWORK,
394
+ osvUrl: OSV_DEFAULT_URL,
395
+ cache: null,
396
+ secrets: createSecretSet(),
397
+ offline: true,
398
+ });
399
+ }
@@ -0,0 +1,86 @@
1
+ const UNPARSABLE_ORIGIN = 'invalid URL';
2
+ const GROUP_KEY_SEPARATOR = '\0';
3
+ export function originOf(url) {
4
+ try {
5
+ return new URL(url).origin;
6
+ }
7
+ catch {
8
+ return UNPARSABLE_ORIGIN;
9
+ }
10
+ }
11
+ export function offlineStatus(revision) {
12
+ return {
13
+ state: 'offline',
14
+ revision,
15
+ registry: { total: 0, done: 0, failed: 0 },
16
+ osv: { total: 0, done: 0, skipped: 0, failed: 0 },
17
+ problems: [],
18
+ };
19
+ }
20
+ export function progressOf(keys, store) {
21
+ const progress = { total: 0, done: 0, failed: 0 };
22
+ for (const key of keys) {
23
+ progress.total++;
24
+ const state = store.settledStateOf(key);
25
+ if (state === undefined)
26
+ continue;
27
+ progress.done++;
28
+ if (state.state === 'failed')
29
+ progress.failed++;
30
+ }
31
+ return progress;
32
+ }
33
+ export function problemsOf(plan, sources) {
34
+ const groups = new Map();
35
+ const count = (source, origin, failure) => {
36
+ if (failure === undefined)
37
+ return;
38
+ const groupKey = [source, origin, failure].join(GROUP_KEY_SEPARATOR);
39
+ const group = groups.get(groupKey) ?? { source, origin, message: sources.secrets.redact(failure), count: 0 };
40
+ group.count++;
41
+ groups.set(groupKey, group);
42
+ };
43
+ for (const target of plan.registry.values()) {
44
+ const failure = sources.registry.settledFailureOf(target.key);
45
+ if (failure !== undefined)
46
+ count('registry', originOf(target.registryUrl), failure);
47
+ }
48
+ const osvOrigin = originOf(sources.osvUrl);
49
+ for (const key of plan.versions.keys())
50
+ count('osv', osvOrigin, sources.matches.settledFailureOf(key));
51
+ for (const name of new Set([...plan.versions.values()].map((target) => target.name))) {
52
+ count('osv', osvOrigin, sources.advisories.settledFailureOf(name));
53
+ }
54
+ return [...groups.values()];
55
+ }
56
+ export function createRevisionCounter(intervalMs) {
57
+ let revision = 0;
58
+ let landedSinceIncrement = false;
59
+ let throttle;
60
+ function increment() {
61
+ revision++;
62
+ landedSinceIncrement = false;
63
+ throttle = setTimeout(() => {
64
+ throttle = undefined;
65
+ if (landedSinceIncrement)
66
+ increment();
67
+ }, intervalMs);
68
+ throttle.unref();
69
+ }
70
+ return {
71
+ get current() {
72
+ return revision;
73
+ },
74
+ resultLanded() {
75
+ landedSinceIncrement = true;
76
+ if (throttle === undefined)
77
+ increment();
78
+ },
79
+ drained() {
80
+ clearTimeout(throttle);
81
+ throttle = undefined;
82
+ landedSinceIncrement = false;
83
+ revision++;
84
+ },
85
+ };
86
+ }
@@ -0,0 +1,143 @@
1
+ import { isPackageMeta } from "./registry-client.js";
2
+ function isPlainObject(value) {
3
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
4
+ }
5
+ export function registryOk(meta, fetchedAt, etag) {
6
+ return etag === undefined ? { state: 'ok', meta, fetchedAt } : { state: 'ok', meta, fetchedAt, etag };
7
+ }
8
+ export function isCachedRegistry(value) {
9
+ if (!isPlainObject(value) || typeof value.fetchedAt !== 'string')
10
+ return false;
11
+ if (value.state === 'not-found')
12
+ return true;
13
+ return value.state === 'ok' && isPackageMeta(value.meta) && (value.etag === undefined || typeof value.etag === 'string');
14
+ }
15
+ function isVulnRef(value) {
16
+ return isPlainObject(value) && typeof value.id === 'string' && typeof value.modified === 'string';
17
+ }
18
+ export function isCachedMatch(value) {
19
+ return (isPlainObject(value) &&
20
+ value.state === 'ok' &&
21
+ typeof value.fetchedAt === 'string' &&
22
+ Array.isArray(value.vulns) &&
23
+ value.vulns.every(isVulnRef));
24
+ }
25
+ function isAdvisory(value) {
26
+ return (isPlainObject(value) &&
27
+ typeof value.id === 'string' &&
28
+ typeof value.summary === 'string' &&
29
+ typeof value.severity === 'string' &&
30
+ typeof value.malicious === 'boolean' &&
31
+ typeof value.url === 'string' &&
32
+ typeof value.modified === 'string' &&
33
+ Array.isArray(value.aliases) &&
34
+ Array.isArray(value.fixedIn) &&
35
+ Array.isArray(value.affected));
36
+ }
37
+ export function isCachedAdvisories(value) {
38
+ return (isPlainObject(value) &&
39
+ typeof value.fetchedAt === 'string' &&
40
+ Array.isArray(value.advisories) &&
41
+ value.advisories.every(isAdvisory));
42
+ }
43
+ function coversReport(advisory, reported) {
44
+ if (advisory.aliases.includes(reported.id))
45
+ return true;
46
+ return advisory.id === reported.id && !(Date.parse(reported.modified) > Date.parse(advisory.modified));
47
+ }
48
+ export function advisoriesCover(advisories, reported) {
49
+ return reported.every((ref) => advisories.some((advisory) => coversReport(advisory, ref)));
50
+ }
51
+ export const REGISTRY_VIEWS = {
52
+ ofSuccess: (record) => record.state === 'ok'
53
+ ? { state: 'ok', meta: record.meta, fetchedAt: record.fetchedAt }
54
+ : { state: 'not-found', fetchedAt: record.fetchedAt },
55
+ ofFailure: (reason, lastGood) => lastGood?.state === 'ok'
56
+ ? { state: 'stale', meta: lastGood.meta, fetchedAt: lastGood.fetchedAt }
57
+ : { state: 'failed', reason },
58
+ };
59
+ export const MATCH_VIEWS = {
60
+ ofSuccess: ({ vulns, fetchedAt }) => ({ state: 'ok', vulns, fetchedAt }),
61
+ ofFailure: (reason, lastGood) => lastGood === undefined ? { state: 'failed', reason } : { state: 'stale', vulns: lastGood.vulns, fetchedAt: lastGood.fetchedAt },
62
+ };
63
+ export const ADVISORIES_VIEWS = {
64
+ ofSuccess: ({ advisories, fetchedAt }) => ({ state: 'ok', advisories, fetchedAt }),
65
+ ofFailure: (reason, lastGood) => lastGood === undefined
66
+ ? { state: 'failed', reason }
67
+ : { state: 'stale', advisories: lastGood.advisories, fetchedAt: lastGood.fetchedAt },
68
+ };
69
+ export function createRecordStore(views, onLanded) {
70
+ const entries = new Map();
71
+ const inFlight = new Set();
72
+ return {
73
+ stateOf: (key) => entries.get(key)?.state,
74
+ settledStateOf: (key) => (inFlight.has(key) ? undefined : entries.get(key)?.state),
75
+ settledFailureOf: (key) => (inFlight.has(key) ? undefined : entries.get(key)?.failure),
76
+ lastGoodOf: (key) => entries.get(key)?.lastGood,
77
+ isKnown(key, usable) {
78
+ const entry = entries.get(key);
79
+ if (entry === undefined)
80
+ return false;
81
+ if (entry.failure !== undefined)
82
+ return true;
83
+ return entry.lastGood !== undefined && usable(entry.lastGood);
84
+ },
85
+ claim(targets, isKnown) {
86
+ const claimed = [];
87
+ for (const target of targets) {
88
+ if (inFlight.has(target.key) || isKnown(target))
89
+ continue;
90
+ inFlight.add(target.key);
91
+ claimed.push(target);
92
+ }
93
+ return claimed;
94
+ },
95
+ release: (key) => {
96
+ inFlight.delete(key);
97
+ },
98
+ succeed(key, record) {
99
+ entries.set(key, { state: views.ofSuccess(record), lastGood: record });
100
+ onLanded();
101
+ },
102
+ fail(key, reason, lastGood) {
103
+ entries.set(key, { state: views.ofFailure(reason, lastGood), lastGood, failure: reason });
104
+ onLanded();
105
+ },
106
+ };
107
+ }
108
+ export function createLimiter(limit) {
109
+ let active = 0;
110
+ const waiting = [];
111
+ return async function limited(task) {
112
+ if (active < limit)
113
+ active++;
114
+ else
115
+ await new Promise((resolve) => waiting.push(resolve));
116
+ try {
117
+ return await task();
118
+ }
119
+ finally {
120
+ const next = waiting.shift();
121
+ if (next)
122
+ next();
123
+ else
124
+ active--;
125
+ }
126
+ };
127
+ }
128
+ export function createCacheAccess(cache, concurrency) {
129
+ const limited = createLimiter(concurrency);
130
+ return {
131
+ async read(kind, keyParts, isShape) {
132
+ if (cache === null)
133
+ return undefined;
134
+ const value = await limited(() => cache.read(kind, keyParts)).catch(() => null);
135
+ return isShape(value) ? value : undefined;
136
+ },
137
+ async write(kind, keyParts, value) {
138
+ if (cache === null)
139
+ return;
140
+ await limited(() => cache.write(kind, keyParts, value)).catch(() => undefined);
141
+ },
142
+ };
143
+ }