@cdxgen/cdxgen 13.0.1 → 13.2.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 (286) hide show
  1. package/README.md +30 -16
  2. package/bin/audit.js +6 -0
  3. package/bin/cdxgen.js +141 -7
  4. package/bin/evinse.js +3 -1
  5. package/data/component-tags.json +8 -1
  6. package/data/crypto-oid.json +88 -12
  7. package/data/frameworks-list.json +86 -1
  8. package/data/remediations.json +1167 -0
  9. package/data/rules/ai-bom.yaml +78 -0
  10. package/data/rules/asar-archives.yaml +1 -1
  11. package/data/rules/build-fidelity.yaml +782 -0
  12. package/data/rules/cargo-rust.yaml +132 -0
  13. package/data/rules/mcp-servers.yaml +122 -0
  14. package/data/rules/package-integrity.yaml +13 -13
  15. package/lib/audit/index.js +48 -4
  16. package/lib/audit/scoring.js +2 -8
  17. package/lib/audit/targets.js +6 -1
  18. package/lib/cli/bomAssembly.js +32 -1
  19. package/lib/cli/cliOptions.js +94 -7
  20. package/lib/cli/index.js +268 -49
  21. package/lib/cli/jsBom.js +91 -5
  22. package/lib/cli/jvmBom.js +1515 -1314
  23. package/lib/cli/managedBom.js +249 -28
  24. package/lib/cli/nativeBom.js +986 -10
  25. package/lib/core/activity.js +120 -2
  26. package/lib/core/buildLedger.js +799 -0
  27. package/lib/core/deferredExit.js +141 -0
  28. package/lib/core/env.js +108 -2
  29. package/lib/core/fs.js +60 -0
  30. package/lib/core/logger.js +12 -99
  31. package/lib/core/severity.js +16 -0
  32. package/lib/core/syncFileWriter.js +106 -0
  33. package/lib/core/userAgent.js +151 -0
  34. package/lib/ecosystems/cargoCache.js +364 -0
  35. package/lib/ecosystems/caxa.js +13 -5
  36. package/lib/ecosystems/cppEvidence.js +40 -2
  37. package/lib/ecosystems/ecosystems.js +250 -91
  38. package/lib/ecosystems/golem.js +11 -1
  39. package/lib/ecosystems/gradleutils.js +498 -17
  40. package/lib/ecosystems/kosi.js +1256 -0
  41. package/lib/ecosystems/parsers-crystal.js +179 -0
  42. package/lib/ecosystems/parsers-dotnet.js +69 -39
  43. package/lib/ecosystems/parsers-elm.js +816 -0
  44. package/lib/ecosystems/parsers-gleam.js +1 -1
  45. package/lib/ecosystems/parsers-js.js +26 -0
  46. package/lib/ecosystems/parsers-julia.js +268 -0
  47. package/lib/ecosystems/parsers-jvm.js +9 -0
  48. package/lib/ecosystems/parsers-lua.js +218 -0
  49. package/lib/ecosystems/parsers-misc.js +188 -2
  50. package/lib/ecosystems/parsers-nim.js +221 -0
  51. package/lib/ecosystems/parsers-opam.js +180 -0
  52. package/lib/ecosystems/parsers-perl.js +277 -0
  53. package/lib/ecosystems/parsers-python.js +172 -36
  54. package/lib/ecosystems/parsers-r.js +148 -0
  55. package/lib/ecosystems/parsers-rebar.js +411 -0
  56. package/lib/ecosystems/parsers-rust.js +944 -19
  57. package/lib/ecosystems/parsers-spack.js +204 -0
  58. package/lib/ecosystems/parsers-stack.js +264 -0
  59. package/lib/ecosystems/parsers-terraform.js +194 -0
  60. package/lib/ecosystems/pypiClassifiers.js +326 -0
  61. package/lib/ecosystems/pythonutils.js +49 -1
  62. package/lib/ecosystems/rubyutils.js +13 -0
  63. package/lib/ecosystems/rusi.js +126 -6
  64. package/lib/evinser/evinser.js +141 -13
  65. package/lib/helpers/agenticToolutils.js +925 -0
  66. package/lib/helpers/remote/dependency-track.js +81 -14
  67. package/lib/inventory/agentFormulationParser.js +57 -4
  68. package/lib/inventory/aiCollector.js +504 -10
  69. package/lib/inventory/aiInventory.js +12 -0
  70. package/lib/inventory/analyzer.js +488 -30
  71. package/lib/inventory/astWorker.js +6 -0
  72. package/lib/inventory/auditCategories.js +17 -0
  73. package/lib/inventory/cbomutils.js +30 -0
  74. package/lib/inventory/cdxrs.js +7 -2
  75. package/lib/inventory/communityAiConfigParser.js +215 -2
  76. package/lib/inventory/depsUtils.js +72 -0
  77. package/lib/inventory/dosai.js +508 -22
  78. package/lib/inventory/envcontext.js +215 -24
  79. package/lib/inventory/evidenceUtils.js +13 -0
  80. package/lib/inventory/fetchBatch.js +131 -2
  81. package/lib/inventory/fetchRate.js +74 -11
  82. package/lib/inventory/forgeEnricher.js +3 -2
  83. package/lib/inventory/jvmToolEnv.js +594 -0
  84. package/lib/inventory/mcpConfigParser.js +166 -2
  85. package/lib/inventory/mcpDiscovery.js +11 -0
  86. package/lib/inventory/mcpServerManifest.js +389 -0
  87. package/lib/inventory/mcpbBundle.js +419 -0
  88. package/lib/inventory/osinfo.js +1 -0
  89. package/lib/inventory/plugins.js +3 -0
  90. package/lib/inventory/protobom.js +54 -88
  91. package/lib/inventory/protobomLoader.js +4 -0
  92. package/lib/inventory/rustFormulationParser.js +1 -1
  93. package/lib/inventory/source.js +10 -4
  94. package/lib/inventory/stylePackageRefs.js +41 -0
  95. package/lib/inventory/svelteUtils.js +524 -0
  96. package/lib/inventory/toolRequirements.js +903 -0
  97. package/lib/managers/binary.js +24 -0
  98. package/lib/managers/docker.js +26 -5
  99. package/lib/parsers/huggingfaceUtils.js +15 -7
  100. package/lib/parsers/onnx.js +294 -0
  101. package/lib/parsers/safetensors.js +114 -0
  102. package/lib/server/server.js +1 -1
  103. package/lib/stages/postgen/annotationBuilder.js +97 -0
  104. package/lib/stages/postgen/auditBom.js +76 -79
  105. package/lib/stages/postgen/introspection/annotate.js +320 -0
  106. package/lib/stages/postgen/introspection/commandFacts.js +293 -0
  107. package/lib/stages/postgen/introspection/emit.js +192 -0
  108. package/lib/stages/postgen/introspection/formulationEvidence.js +427 -0
  109. package/lib/stages/postgen/introspection/markers.js +250 -0
  110. package/lib/stages/postgen/introspection/reflect.js +1214 -0
  111. package/lib/stages/postgen/introspection/report.js +1208 -0
  112. package/lib/stages/postgen/introspection/score.js +1067 -0
  113. package/lib/stages/postgen/introspection/shapeCommand.js +304 -0
  114. package/lib/stages/postgen/parentEdge.js +156 -0
  115. package/lib/stages/postgen/postgen.js +284 -3
  116. package/lib/stages/postgen/ruleEngine.js +17 -0
  117. package/lib/stages/pregen/envAudit.js +42 -2
  118. package/lib/stages/pregen/pregen.js +1171 -6
  119. package/lib/third-party/arborist/snapshots/calc-dep-flags.test.cjs +462 -0
  120. package/lib/validator/bomValidator.js +11 -2
  121. package/package.json +26 -26
  122. package/types/lib/audit/index.d.ts.map +1 -1
  123. package/types/lib/audit/scoring.d.ts +2 -8
  124. package/types/lib/audit/scoring.d.ts.map +1 -1
  125. package/types/lib/audit/targets.d.ts.map +1 -1
  126. package/types/lib/cli/bomAssembly.d.ts.map +1 -1
  127. package/types/lib/cli/cliOptions.d.ts +2 -1
  128. package/types/lib/cli/cliOptions.d.ts.map +1 -1
  129. package/types/lib/cli/index.d.ts +10 -2
  130. package/types/lib/cli/index.d.ts.map +1 -1
  131. package/types/lib/cli/jsBom.d.ts +23 -0
  132. package/types/lib/cli/jsBom.d.ts.map +1 -1
  133. package/types/lib/cli/jvmBom.d.ts.map +1 -1
  134. package/types/lib/cli/managedBom.d.ts +33 -0
  135. package/types/lib/cli/managedBom.d.ts.map +1 -1
  136. package/types/lib/cli/nativeBom.d.ts +88 -8
  137. package/types/lib/cli/nativeBom.d.ts.map +1 -1
  138. package/types/lib/core/activity.d.ts.map +1 -1
  139. package/types/lib/core/buildLedger.d.ts +272 -0
  140. package/types/lib/core/buildLedger.d.ts.map +1 -0
  141. package/types/lib/core/deferredExit.d.ts +84 -0
  142. package/types/lib/core/deferredExit.d.ts.map +1 -0
  143. package/types/lib/core/env.d.ts +43 -1
  144. package/types/lib/core/env.d.ts.map +1 -1
  145. package/types/lib/core/fs.d.ts +21 -0
  146. package/types/lib/core/fs.d.ts.map +1 -1
  147. package/types/lib/core/logger.d.ts +2 -11
  148. package/types/lib/core/logger.d.ts.map +1 -1
  149. package/types/lib/core/severity.d.ts +11 -0
  150. package/types/lib/core/severity.d.ts.map +1 -0
  151. package/types/lib/core/syncFileWriter.d.ts +33 -0
  152. package/types/lib/core/syncFileWriter.d.ts.map +1 -0
  153. package/types/lib/core/userAgent.d.ts +56 -0
  154. package/types/lib/core/userAgent.d.ts.map +1 -0
  155. package/types/lib/ecosystems/cargoCache.d.ts +75 -0
  156. package/types/lib/ecosystems/cargoCache.d.ts.map +1 -0
  157. package/types/lib/ecosystems/caxa.d.ts.map +1 -1
  158. package/types/lib/ecosystems/cppEvidence.d.ts +1 -1
  159. package/types/lib/ecosystems/cppEvidence.d.ts.map +1 -1
  160. package/types/lib/ecosystems/ecosystems.d.ts +15 -1
  161. package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
  162. package/types/lib/ecosystems/gradleutils.d.ts +53 -0
  163. package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
  164. package/types/lib/ecosystems/kosi.d.ts +73 -0
  165. package/types/lib/ecosystems/kosi.d.ts.map +1 -0
  166. package/types/lib/ecosystems/parsers-crystal.d.ts +27 -0
  167. package/types/lib/ecosystems/parsers-crystal.d.ts.map +1 -0
  168. package/types/lib/ecosystems/parsers-dotnet.d.ts +19 -0
  169. package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -1
  170. package/types/lib/ecosystems/parsers-elm.d.ts +79 -0
  171. package/types/lib/ecosystems/parsers-elm.d.ts.map +1 -0
  172. package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
  173. package/types/lib/ecosystems/parsers-julia.d.ts +57 -0
  174. package/types/lib/ecosystems/parsers-julia.d.ts.map +1 -0
  175. package/types/lib/ecosystems/parsers-jvm.d.ts.map +1 -1
  176. package/types/lib/ecosystems/parsers-lua.d.ts +50 -0
  177. package/types/lib/ecosystems/parsers-lua.d.ts.map +1 -0
  178. package/types/lib/ecosystems/parsers-misc.d.ts +23 -0
  179. package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
  180. package/types/lib/ecosystems/parsers-nim.d.ts +43 -0
  181. package/types/lib/ecosystems/parsers-nim.d.ts.map +1 -0
  182. package/types/lib/ecosystems/parsers-opam.d.ts +31 -0
  183. package/types/lib/ecosystems/parsers-opam.d.ts.map +1 -0
  184. package/types/lib/ecosystems/parsers-perl.d.ts +36 -0
  185. package/types/lib/ecosystems/parsers-perl.d.ts.map +1 -0
  186. package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
  187. package/types/lib/ecosystems/parsers-r.d.ts +35 -0
  188. package/types/lib/ecosystems/parsers-r.d.ts.map +1 -0
  189. package/types/lib/ecosystems/parsers-rebar.d.ts +42 -0
  190. package/types/lib/ecosystems/parsers-rebar.d.ts.map +1 -0
  191. package/types/lib/ecosystems/parsers-rust.d.ts +199 -0
  192. package/types/lib/ecosystems/parsers-rust.d.ts.map +1 -1
  193. package/types/lib/ecosystems/parsers-spack.d.ts +33 -0
  194. package/types/lib/ecosystems/parsers-spack.d.ts.map +1 -0
  195. package/types/lib/ecosystems/parsers-stack.d.ts +35 -0
  196. package/types/lib/ecosystems/parsers-stack.d.ts.map +1 -0
  197. package/types/lib/ecosystems/parsers-terraform.d.ts +34 -0
  198. package/types/lib/ecosystems/parsers-terraform.d.ts.map +1 -0
  199. package/types/lib/ecosystems/pypiClassifiers.d.ts +80 -0
  200. package/types/lib/ecosystems/pypiClassifiers.d.ts.map +1 -0
  201. package/types/lib/ecosystems/pythonutils.d.ts +21 -1
  202. package/types/lib/ecosystems/pythonutils.d.ts.map +1 -1
  203. package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
  204. package/types/lib/ecosystems/rusi.d.ts.map +1 -1
  205. package/types/lib/evinser/evinser.d.ts +34 -1
  206. package/types/lib/evinser/evinser.d.ts.map +1 -1
  207. package/types/lib/helpers/agenticToolutils.d.ts +87 -0
  208. package/types/lib/helpers/agenticToolutils.d.ts.map +1 -0
  209. package/types/lib/helpers/remote/dependency-track.d.ts +45 -4
  210. package/types/lib/helpers/remote/dependency-track.d.ts.map +1 -1
  211. package/types/lib/inventory/agentFormulationParser.d.ts.map +1 -1
  212. package/types/lib/inventory/aiCollector.d.ts.map +1 -1
  213. package/types/lib/inventory/aiInventory.d.ts.map +1 -1
  214. package/types/lib/inventory/analyzer.d.ts.map +1 -1
  215. package/types/lib/inventory/auditCategories.d.ts +12 -0
  216. package/types/lib/inventory/auditCategories.d.ts.map +1 -1
  217. package/types/lib/inventory/cbomutils.d.ts.map +1 -1
  218. package/types/lib/inventory/communityAiConfigParser.d.ts.map +1 -1
  219. package/types/lib/inventory/depsUtils.d.ts.map +1 -1
  220. package/types/lib/inventory/dosai.d.ts +43 -0
  221. package/types/lib/inventory/dosai.d.ts.map +1 -1
  222. package/types/lib/inventory/envcontext.d.ts.map +1 -1
  223. package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
  224. package/types/lib/inventory/fetchBatch.d.ts +43 -0
  225. package/types/lib/inventory/fetchBatch.d.ts.map +1 -1
  226. package/types/lib/inventory/fetchRate.d.ts +9 -6
  227. package/types/lib/inventory/fetchRate.d.ts.map +1 -1
  228. package/types/lib/inventory/forgeEnricher.d.ts.map +1 -1
  229. package/types/lib/inventory/jvmToolEnv.d.ts +190 -0
  230. package/types/lib/inventory/jvmToolEnv.d.ts.map +1 -0
  231. package/types/lib/inventory/mcpConfigParser.d.ts.map +1 -1
  232. package/types/lib/inventory/mcpDiscovery.d.ts.map +1 -1
  233. package/types/lib/inventory/mcpServerManifest.d.ts +19 -0
  234. package/types/lib/inventory/mcpServerManifest.d.ts.map +1 -0
  235. package/types/lib/inventory/mcpbBundle.d.ts +19 -0
  236. package/types/lib/inventory/mcpbBundle.d.ts.map +1 -0
  237. package/types/lib/inventory/osinfo.d.ts.map +1 -1
  238. package/types/lib/inventory/plugins.d.ts.map +1 -1
  239. package/types/lib/inventory/protobom.d.ts +18 -13
  240. package/types/lib/inventory/protobom.d.ts.map +1 -1
  241. package/types/lib/inventory/protobomLoader.d.ts.map +1 -1
  242. package/types/lib/inventory/source.d.ts.map +1 -1
  243. package/types/lib/inventory/stylePackageRefs.d.ts +17 -0
  244. package/types/lib/inventory/stylePackageRefs.d.ts.map +1 -0
  245. package/types/lib/inventory/svelteUtils.d.ts +124 -0
  246. package/types/lib/inventory/svelteUtils.d.ts.map +1 -0
  247. package/types/lib/inventory/toolRequirements.d.ts +216 -0
  248. package/types/lib/inventory/toolRequirements.d.ts.map +1 -0
  249. package/types/lib/managers/binary.d.ts.map +1 -1
  250. package/types/lib/managers/docker.d.ts.map +1 -1
  251. package/types/lib/parsers/huggingfaceUtils.d.ts +8 -1
  252. package/types/lib/parsers/huggingfaceUtils.d.ts.map +1 -1
  253. package/types/lib/parsers/onnx.d.ts +28 -0
  254. package/types/lib/parsers/onnx.d.ts.map +1 -0
  255. package/types/lib/parsers/safetensors.d.ts +22 -0
  256. package/types/lib/parsers/safetensors.d.ts.map +1 -0
  257. package/types/lib/stages/postgen/annotationBuilder.d.ts +53 -0
  258. package/types/lib/stages/postgen/annotationBuilder.d.ts.map +1 -0
  259. package/types/lib/stages/postgen/auditBom.d.ts.map +1 -1
  260. package/types/lib/stages/postgen/introspection/annotate.d.ts +81 -0
  261. package/types/lib/stages/postgen/introspection/annotate.d.ts.map +1 -0
  262. package/types/lib/stages/postgen/introspection/commandFacts.d.ts +32 -0
  263. package/types/lib/stages/postgen/introspection/commandFacts.d.ts.map +1 -0
  264. package/types/lib/stages/postgen/introspection/emit.d.ts +71 -0
  265. package/types/lib/stages/postgen/introspection/emit.d.ts.map +1 -0
  266. package/types/lib/stages/postgen/introspection/formulationEvidence.d.ts +154 -0
  267. package/types/lib/stages/postgen/introspection/formulationEvidence.d.ts.map +1 -0
  268. package/types/lib/stages/postgen/introspection/markers.d.ts +58 -0
  269. package/types/lib/stages/postgen/introspection/markers.d.ts.map +1 -0
  270. package/types/lib/stages/postgen/introspection/reflect.d.ts +77 -0
  271. package/types/lib/stages/postgen/introspection/reflect.d.ts.map +1 -0
  272. package/types/lib/stages/postgen/introspection/report.d.ts +85 -0
  273. package/types/lib/stages/postgen/introspection/report.d.ts.map +1 -0
  274. package/types/lib/stages/postgen/introspection/score.d.ts +149 -0
  275. package/types/lib/stages/postgen/introspection/score.d.ts.map +1 -0
  276. package/types/lib/stages/postgen/introspection/shapeCommand.d.ts +79 -0
  277. package/types/lib/stages/postgen/introspection/shapeCommand.d.ts.map +1 -0
  278. package/types/lib/stages/postgen/parentEdge.d.ts +35 -0
  279. package/types/lib/stages/postgen/parentEdge.d.ts.map +1 -0
  280. package/types/lib/stages/postgen/postgen.d.ts +35 -2
  281. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  282. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
  283. package/types/lib/stages/pregen/envAudit.d.ts.map +1 -1
  284. package/types/lib/stages/pregen/pregen.d.ts +131 -0
  285. package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
  286. package/types/lib/validator/bomValidator.d.ts.map +1 -1
@@ -0,0 +1,816 @@
1
+ import { readdirSync, readFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { basename, dirname, join, resolve } from "node:path";
4
+ import process from "node:process";
5
+
6
+ import semver from "semver";
7
+
8
+ import {
9
+ cdxgenAgent,
10
+ DEBUG_MODE,
11
+ readEnvironmentVariable,
12
+ } from "../core/activity.js";
13
+ import { safeExistsSync } from "../core/fs.js";
14
+ import {
15
+ prefetchEnabled,
16
+ prefetchedResponse,
17
+ prefetchJson,
18
+ } from "../inventory/fetchBatch.js";
19
+ import { tryBuildPurl } from "../inventory/purl.js";
20
+
21
+ /**
22
+ * Elm project parser.
23
+ *
24
+ * Three files are consulted, spanning the two manifest generations of the
25
+ * ecosystem:
26
+ *
27
+ * - `elm.json` (0.19) — `type: "application"` projects pin **exact**
28
+ * versions for direct, indirect, and test dependencies, so the file
29
+ * doubles as the lock file. `type: "package"` projects declare bounded
30
+ * ranges (`"1.0.0 <= v < 2.0.0"`) and carry their own name, version,
31
+ * license, and summary.
32
+ * - `elm-package.json` (0.18) — the legacy manifest, declaring ranges only.
33
+ * - `elm-stuff/exact-dependencies.json` (0.18) — the lock file the old
34
+ * `elm-package` tool wrote with the exact version of every package.
35
+ *
36
+ * Package metadata (license, summary, and the dependencies of a dependency)
37
+ * is enriched from the local Elm cache when it is populated. The cache layout
38
+ * follows the compiler's own resolution (see `builder/src/Stuff.hs` in
39
+ * elm/compiler): `$ELM_HOME` (defaulting to `~/.elm`, or `%APPDATA%\elm` on
40
+ * Windows) contains one directory per compiler version, and each downloaded
41
+ * package lives at `<version>/packages/<author>/<project>/<version>/elm.json`.
42
+ * Parsing itself never reaches the network; {@link getElmMetadata} fills the
43
+ * gaps an empty cache leaves from the registry, and only when the run has
44
+ * asked for registry metadata.
45
+ *
46
+ * No `elm` purl type is registered in purl-spec, so packages are identified
47
+ * as generic packages carrying a `cdx:purl:proposedType=elm` property,
48
+ * following the convention used for crystal and nim. The `author/project`
49
+ * name maps to the purl namespace and the CycloneDX group so the full
50
+ * identity survives.
51
+ */
52
+
53
+ /**
54
+ * One path segment of an Elm package name (`author` or `project`). Names are
55
+ * GitHub `user/repo` pairs; the shape check doubles as path-traversal
56
+ * protection because segments are joined into cache paths.
57
+ */
58
+ const ELM_NAME_SEGMENT = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
59
+
60
+ /**
61
+ * The bounded-interval constraint syntax Elm uses in manifests:
62
+ * `"<lower> <= v < <upper>"`.
63
+ */
64
+ const ELM_RANGE_PATTERN = /^(\S+)\s*(<=|<)\s*v\s*(<=|<)\s*(\S+)$/;
65
+
66
+ /**
67
+ * Parse an Elm project from its 0.19 `elm.json`.
68
+ *
69
+ * @param {string} elmJsonFile Path to `elm.json`
70
+ * @param {object} [cacheReader] Reader returned by {@link createElmCacheReader};
71
+ * when omitted, one is derived from the manifest's pinned compiler version
72
+ * @returns {{ pkgList: object[], dependencies: object[], parentComponent: object, rootInputs: string[] }}
73
+ */
74
+ export function parseElmProject(elmJsonFile, cacheReader) {
75
+ const manifest = readJsonFile(elmJsonFile);
76
+ if (
77
+ !manifest ||
78
+ (manifest.type !== "application" && manifest.type !== "package")
79
+ ) {
80
+ return {
81
+ pkgList: [],
82
+ dependencies: [],
83
+ parentComponent: {},
84
+ rootInputs: [],
85
+ };
86
+ }
87
+
88
+ const elmVersion =
89
+ typeof manifest["elm-version"] === "string"
90
+ ? manifest["elm-version"]
91
+ : undefined;
92
+ const cache =
93
+ cacheReader ||
94
+ createElmCacheReader(
95
+ manifest.type === "application" && semver.valid(elmVersion)
96
+ ? elmVersion
97
+ : undefined,
98
+ );
99
+
100
+ const parentComponent = {};
101
+ const properties = [{ name: "internal:SrcFile", value: elmJsonFile }];
102
+ if (elmVersion) {
103
+ properties.push({ name: "cdx:elm:elmVersion", value: elmVersion });
104
+ }
105
+ if (manifest.type === "package") {
106
+ const split = splitElmName(manifest.name);
107
+ parentComponent.type = "application";
108
+ if (split) {
109
+ parentComponent.group = split.author;
110
+ parentComponent.name = split.repo;
111
+ } else {
112
+ parentComponent.name = projectDirectoryName(elmJsonFile);
113
+ }
114
+ if (typeof manifest.version === "string" && manifest.version) {
115
+ parentComponent.version = manifest.version;
116
+ }
117
+ if (typeof manifest.summary === "string" && manifest.summary) {
118
+ parentComponent.description = manifest.summary;
119
+ }
120
+ if (typeof manifest.license === "string" && manifest.license) {
121
+ parentComponent.license = manifest.license;
122
+ }
123
+ } else {
124
+ // Applications carry no name of their own; the directory stands in.
125
+ parentComponent.type = "application";
126
+ parentComponent.name = projectDirectoryName(elmJsonFile);
127
+ }
128
+ parentComponent.properties = properties;
129
+
130
+ const entries = [];
131
+ if (manifest.type === "package") {
132
+ addRangeDependencies(
133
+ entries,
134
+ manifest.dependencies,
135
+ "direct",
136
+ elmJsonFile,
137
+ cache,
138
+ );
139
+ addRangeDependencies(
140
+ entries,
141
+ manifest["test-dependencies"],
142
+ "direct-test",
143
+ elmJsonFile,
144
+ cache,
145
+ );
146
+ } else {
147
+ for (const [section, kind] of [
148
+ ["dependencies", "direct"],
149
+ ["test-dependencies", "direct-test"],
150
+ ]) {
151
+ const group = manifest[section];
152
+ addExactDependencies(entries, group?.direct, kind, elmJsonFile, cache);
153
+ addExactDependencies(
154
+ entries,
155
+ group?.indirect,
156
+ kind === "direct" ? "indirect" : "indirect-test",
157
+ elmJsonFile,
158
+ cache,
159
+ );
160
+ }
161
+ }
162
+ const pkgList = entries.map((entry) => entry.pkg);
163
+ const rootInputs = entries
164
+ .filter((entry) => entry.kind === "direct" || entry.kind === "direct-test")
165
+ .map((entry) => entry.pkg["bom-ref"]);
166
+ return {
167
+ pkgList,
168
+ dependencies: buildDependencyEdges(entries),
169
+ parentComponent,
170
+ rootInputs,
171
+ };
172
+ }
173
+
174
+ /**
175
+ * Parse a legacy 0.18 Elm project from `elm-package.json` and its optional
176
+ * `elm-stuff/exact-dependencies.json` lock file.
177
+ *
178
+ * @param {string} elmPackageJsonFile Path to `elm-package.json`
179
+ * @param {string} [exactDepsFile] Path to the lock file, if present
180
+ * @returns {{ pkgList: object[], dependencies: object[], parentComponent: object, rootInputs: string[] }}
181
+ */
182
+ export function parseLegacyElmProject(elmPackageJsonFile, exactDepsFile) {
183
+ const manifest = readJsonFile(elmPackageJsonFile);
184
+ if (
185
+ !manifest ||
186
+ typeof manifest !== "object" ||
187
+ (!manifest["elm-version"] && !manifest.dependencies)
188
+ ) {
189
+ return {
190
+ pkgList: [],
191
+ dependencies: [],
192
+ parentComponent: {},
193
+ rootInputs: [],
194
+ };
195
+ }
196
+
197
+ const parentComponent = {
198
+ type: "application",
199
+ name: projectDirectoryName(elmPackageJsonFile),
200
+ };
201
+ if (typeof manifest.version === "string" && manifest.version) {
202
+ parentComponent.version = manifest.version;
203
+ }
204
+ if (typeof manifest.summary === "string" && manifest.summary) {
205
+ parentComponent.description = manifest.summary;
206
+ }
207
+ if (typeof manifest.license === "string" && manifest.license) {
208
+ parentComponent.license = manifest.license;
209
+ }
210
+ const properties = [{ name: "internal:SrcFile", value: elmPackageJsonFile }];
211
+ if (typeof manifest["elm-version"] === "string" && manifest["elm-version"]) {
212
+ properties.push({
213
+ name: "cdx:elm:elmVersion",
214
+ value: manifest["elm-version"],
215
+ });
216
+ }
217
+ parentComponent.properties = properties;
218
+
219
+ const declared = new Set(
220
+ manifest.dependencies && typeof manifest.dependencies === "object"
221
+ ? Object.keys(manifest.dependencies)
222
+ : [],
223
+ );
224
+ const ranges =
225
+ manifest.dependencies && typeof manifest.dependencies === "object"
226
+ ? manifest.dependencies
227
+ : {};
228
+
229
+ const entries = [];
230
+ const exact = exactDepsFile ? readJsonFile(exactDepsFile) : undefined;
231
+ const pinned =
232
+ exact && typeof exact === "object" && !Array.isArray(exact) ? exact : {};
233
+ for (const [name, version] of Object.entries(pinned)) {
234
+ const kind = declared.has(name) ? "direct" : "indirect";
235
+ entries.push({
236
+ kind,
237
+ name,
238
+ pkg: elmPackage(name, typeof version === "string" ? version : undefined, {
239
+ dependency: kind,
240
+ srcFile: exactDepsFile,
241
+ versionRange:
242
+ typeof ranges[name] === "string" ? ranges[name] : undefined,
243
+ }),
244
+ });
245
+ }
246
+ if (!entries.length) {
247
+ // Lockless fallback: the declared dependencies without resolved versions.
248
+ for (const [name, range] of Object.entries(ranges)) {
249
+ entries.push({
250
+ kind: "direct",
251
+ name,
252
+ pkg: elmPackage(name, undefined, {
253
+ dependency: "direct",
254
+ srcFile: elmPackageJsonFile,
255
+ versionRange: typeof range === "string" ? range : undefined,
256
+ }),
257
+ });
258
+ }
259
+ }
260
+ const rootInputs = entries
261
+ .filter((entry) => entry.kind === "direct")
262
+ .map((entry) => entry.pkg["bom-ref"]);
263
+ return {
264
+ pkgList: entries.map((entry) => entry.pkg),
265
+ dependencies: buildDependencyEdges(entries),
266
+ parentComponent,
267
+ rootInputs,
268
+ };
269
+ }
270
+
271
+ /**
272
+ * Tell whether a candidate `elm.json` really is an Elm manifest. The file
273
+ * name is distinctive but lives in every Elm project, so detection only
274
+ * needs a cheap content check before dispatching.
275
+ *
276
+ * @param {string} filePath Path to the candidate `elm.json`
277
+ * @returns {boolean} true when the file declares an Elm project
278
+ */
279
+ export function isElmProjectFile(filePath) {
280
+ const manifest = readJsonFile(filePath);
281
+ return manifest?.type === "application" || manifest?.type === "package";
282
+ }
283
+
284
+ /**
285
+ * Create a reader for the local Elm package cache. Reads are memoised and
286
+ * silently disabled when the cache directory does not exist.
287
+ *
288
+ * @param {string} [elmVersionHint] Exact compiler version directory to try
289
+ * first, when the project pins one
290
+ * @returns {{ readMetadata: (name: string, version: string) => object|null, listVersions: (name: string) => string[] }}
291
+ */
292
+ export function createElmCacheReader(elmVersionHint) {
293
+ const home = elmHomeDir();
294
+ const metadataCache = new Map();
295
+ const versionsCache = new Map();
296
+ let versionDirs;
297
+
298
+ function listVersionDirs() {
299
+ if (versionDirs) {
300
+ return versionDirs;
301
+ }
302
+ try {
303
+ versionDirs = readdirSync(home)
304
+ .filter((entry) => semver.valid(entry))
305
+ .sort()
306
+ .reverse();
307
+ } catch {
308
+ versionDirs = [];
309
+ }
310
+ return versionDirs;
311
+ }
312
+
313
+ function packageDir(name) {
314
+ const split = splitElmName(name);
315
+ return split
316
+ ? { author: split.author, repo: split.repo, valid: true }
317
+ : { valid: false };
318
+ }
319
+
320
+ return {
321
+ readMetadata(name, version) {
322
+ const key = `${name}@${version}`;
323
+ if (metadataCache.has(key)) {
324
+ return metadataCache.get(key);
325
+ }
326
+ let metadata = null;
327
+ const dir = packageDir(name);
328
+ if (dir.valid && semver.valid(version)) {
329
+ const dirs =
330
+ elmVersionHint && semver.valid(elmVersionHint)
331
+ ? [
332
+ elmVersionHint,
333
+ ...listVersionDirs().filter((d) => d !== elmVersionHint),
334
+ ]
335
+ : listVersionDirs();
336
+ for (const versionDir of dirs) {
337
+ const metaFile = join(
338
+ home,
339
+ versionDir,
340
+ "packages",
341
+ dir.author,
342
+ dir.repo,
343
+ version,
344
+ "elm.json",
345
+ );
346
+ if (safeExistsSync(metaFile)) {
347
+ metadata = readJsonFile(metaFile);
348
+ if (metadata) {
349
+ break;
350
+ }
351
+ }
352
+ }
353
+ }
354
+ metadataCache.set(key, metadata);
355
+ return metadata;
356
+ },
357
+ listVersions(name) {
358
+ if (versionsCache.has(name)) {
359
+ return versionsCache.get(name);
360
+ }
361
+ const versions = new Set();
362
+ const dir = packageDir(name);
363
+ if (dir.valid) {
364
+ for (const versionDir of listVersionDirs()) {
365
+ try {
366
+ for (const entry of readdirSync(
367
+ join(home, versionDir, "packages", dir.author, dir.repo),
368
+ )) {
369
+ if (semver.valid(entry)) {
370
+ versions.add(entry);
371
+ }
372
+ }
373
+ } catch {
374
+ // No cached copy under this compiler version.
375
+ }
376
+ }
377
+ }
378
+ const list = [...versions];
379
+ versionsCache.set(name, list);
380
+ return list;
381
+ },
382
+ };
383
+ }
384
+
385
+ /**
386
+ * Resolve the Elm home directory the way the compiler does: `$ELM_HOME` when
387
+ * set, otherwise the OS per-user data directory for "elm" (`~/.elm` on Unix,
388
+ * `%APPDATA%\elm` on Windows).
389
+ *
390
+ * @returns {string} Elm home directory
391
+ */
392
+ function elmHomeDir() {
393
+ const custom = readEnvironmentVariable("ELM_HOME");
394
+ if (custom) {
395
+ return custom;
396
+ }
397
+ if (process.platform === "win32") {
398
+ const appData =
399
+ readEnvironmentVariable("APPDATA") ||
400
+ join(homedir(), "AppData", "Roaming");
401
+ return join(appData, "elm");
402
+ }
403
+ return join(homedir(), ".elm");
404
+ }
405
+
406
+ /**
407
+ * Split an Elm package name into its author and project segments.
408
+ *
409
+ * @param {string} name Package name, e.g. `elm/http`
410
+ * @returns {{author: string, repo: string}|null} null when the name is not a
411
+ * well-formed `author/project` pair
412
+ */
413
+ function splitElmName(name) {
414
+ if (typeof name !== "string") {
415
+ return null;
416
+ }
417
+ const parts = name.split("/");
418
+ if (parts.length !== 2 || !parts.every((p) => ELM_NAME_SEGMENT.test(p))) {
419
+ return null;
420
+ }
421
+ return { author: parts[0], repo: parts[1] };
422
+ }
423
+
424
+ /**
425
+ * Derive a project display name from the manifest's directory, used by
426
+ * application manifests, which carry no name of their own.
427
+ *
428
+ * @param {string} manifestFile Manifest path
429
+ * @returns {string} Directory base name
430
+ */
431
+ function projectDirectoryName(manifestFile) {
432
+ return basename(dirname(resolve(manifestFile)));
433
+ }
434
+
435
+ /**
436
+ * Translate an Elm version constraint into semver range syntax. Only the
437
+ * documented bounded interval (`"1.0.0 <= v < 2.0.0"`) is understood;
438
+ * anything else is returned as null and treated as an opaque constraint.
439
+ *
440
+ * @param {string} range Constraint string from a manifest
441
+ * @returns {string|null} Equivalent semver range, e.g. `">=1.0.0 <2.0.0"`
442
+ */
443
+ function toSemverRange(range) {
444
+ if (typeof range !== "string") {
445
+ return null;
446
+ }
447
+ const match = range.match(ELM_RANGE_PATTERN);
448
+ if (!match) {
449
+ return null;
450
+ }
451
+ const [, lower, lowerOp, upperOp, upper] = match;
452
+ // The grammar allows `<= v` lower bounds only; a `< v` lower bound would
453
+ // exclude the very version the resolver needs, so treat it as opaque.
454
+ if (lowerOp !== "<=" || !semver.valid(lower) || !semver.valid(upper)) {
455
+ return null;
456
+ }
457
+ return `>=${lower} ${upperOp}${upper}`;
458
+ }
459
+
460
+ /**
461
+ * Append the dependencies of one exact-version section of an application
462
+ * `elm.json`, enriching each from the local cache when possible.
463
+ *
464
+ * @param {object[]} entries Accumulator of `{kind, name, pkg, metadata}` records
465
+ */
466
+ function addExactDependencies(entries, section, kind, srcFile, cache) {
467
+ if (!section || typeof section !== "object") {
468
+ return;
469
+ }
470
+ for (const [name, version] of Object.entries(section)) {
471
+ const exact = typeof version === "string" && version ? version : undefined;
472
+ const metadata =
473
+ cache && exact && semver.valid(exact) && splitElmName(name)
474
+ ? cache.readMetadata(name, exact)
475
+ : null;
476
+ entries.push({
477
+ kind,
478
+ name,
479
+ metadata,
480
+ pkg: elmPackage(name, exact, {
481
+ dependency: kind,
482
+ srcFile,
483
+ metadata,
484
+ }),
485
+ });
486
+ }
487
+ }
488
+
489
+ /**
490
+ * Append the dependencies of a range-constrained section of a package
491
+ * `elm.json`. When the local Elm cache holds versions satisfying the declared
492
+ * range, the newest of them — the one Elm's solver would pick — becomes the
493
+ * component version; otherwise the component keeps only the constraint.
494
+ *
495
+ * @param {object[]} entries Accumulator of `{kind, name, pkg, metadata}` records
496
+ */
497
+ function addRangeDependencies(entries, section, kind, srcFile, cache) {
498
+ if (!section || typeof section !== "object") {
499
+ return;
500
+ }
501
+ for (const [name, range] of Object.entries(section)) {
502
+ let version;
503
+ let metadata;
504
+ const semverRange = toSemverRange(range);
505
+ if (cache && semverRange && splitElmName(name)) {
506
+ // Elm's solver picks the newest version within the constraints.
507
+ version =
508
+ semver.maxSatisfying(cache.listVersions(name), semverRange) ||
509
+ undefined;
510
+ if (version) {
511
+ metadata = cache.readMetadata(name, version);
512
+ }
513
+ }
514
+ entries.push({
515
+ kind,
516
+ name,
517
+ metadata,
518
+ pkg: elmPackage(name, version, {
519
+ dependency: kind,
520
+ srcFile,
521
+ metadata,
522
+ versionRange: typeof range === "string" ? range : undefined,
523
+ }),
524
+ });
525
+ }
526
+ }
527
+
528
+ /**
529
+ * Build the dependency graph for the packages of a single manifest.
530
+ *
531
+ * Elm manifests list the resolved set but not the edges between its members;
532
+ * those live in each package's own cached `elm.json`, which the enrichment
533
+ * pass has already read. Edges are therefore emitted only for packages found
534
+ * in the local cache — every component still gets an entry, so none of them
535
+ * is left dangling outside the graph.
536
+ *
537
+ * @param {object[]} entries `{kind, name, pkg, metadata}` records of one manifest
538
+ * @returns {object[]} CycloneDX dependency entries
539
+ */
540
+ function buildDependencyEdges(entries) {
541
+ const refByName = new Map(
542
+ entries.map((entry) => [entry.name, entry.pkg["bom-ref"]]),
543
+ );
544
+ return entries.map((entry) => {
545
+ const declared = entry.metadata?.dependencies;
546
+ const dependsOn =
547
+ declared && typeof declared === "object" && !Array.isArray(declared)
548
+ ? Object.keys(declared)
549
+ .map((name) => refByName.get(name))
550
+ .filter((ref) => ref && ref !== entry.pkg["bom-ref"])
551
+ : [];
552
+ return {
553
+ ref: entry.pkg["bom-ref"],
554
+ dependsOn: [...new Set(dependsOn)].sort(),
555
+ };
556
+ });
557
+ }
558
+
559
+ /**
560
+ * Build a component-like package record for an Elm dependency.
561
+ *
562
+ * @param {string} name Package name as Elm writes it (`author/project`)
563
+ * @param {string|undefined} version Resolved version, if known
564
+ * @param {object} opts Extra context (`dependency`, `srcFile`, `metadata`, `versionRange`)
565
+ * @returns {object} Package record
566
+ */
567
+ function elmPackage(name, version, opts) {
568
+ const split = splitElmName(name);
569
+ const test =
570
+ opts.dependency === "direct-test" || opts.dependency === "indirect-test";
571
+ const purl = tryBuildPurl({
572
+ type: "generic",
573
+ namespace: split?.author,
574
+ name: split?.repo ?? name,
575
+ version: version || undefined,
576
+ });
577
+ const properties = [
578
+ { name: "internal:SrcFile", value: opts.srcFile },
579
+ { name: "cdx:purl:proposedType", value: "elm" },
580
+ { name: "cdx:elm:dependency", value: opts.dependency },
581
+ ];
582
+ if (opts.versionRange) {
583
+ properties.push({
584
+ name: "cdx:elm:versionRange",
585
+ value: opts.versionRange,
586
+ });
587
+ }
588
+ const pkg = {
589
+ name: split?.repo ?? name,
590
+ group: split?.author,
591
+ ...(version ? { version } : {}),
592
+ type: "library",
593
+ scope: test ? "optional" : "required",
594
+ properties,
595
+ };
596
+ if (split) {
597
+ // Both locations follow from the name alone: the registry only publishes
598
+ // packages hosted on GitHub under the same `author/project` path, and
599
+ // documents every version at a stable URL. The link is to the canonical
600
+ // site rather than to whatever `ELM_PACKAGE_URL` points at, since a
601
+ // mirror is where cdxgen read the metadata, not where a reader of the BOM
602
+ // should be sent.
603
+ pkg.repository = {
604
+ url: `https://github.com/${split.author}/${split.repo}`,
605
+ };
606
+ pkg.homepage = {
607
+ url: version
608
+ ? `${ELM_REGISTRY_HOME}/packages/${split.author}/${split.repo}/${version}/`
609
+ : `${ELM_REGISTRY_HOME}/packages/${split.author}/${split.repo}/`,
610
+ };
611
+ }
612
+ if (typeof opts.metadata?.license === "string" && opts.metadata.license) {
613
+ pkg.license = opts.metadata.license;
614
+ }
615
+ if (typeof opts.metadata?.summary === "string" && opts.metadata.summary) {
616
+ pkg.description = opts.metadata.summary;
617
+ }
618
+ if (purl) {
619
+ pkg.purl = purl;
620
+ pkg["bom-ref"] = decodeURIComponent(purl);
621
+ } else {
622
+ pkg["bom-ref"] = `library:${name}:${version || ""}`;
623
+ }
624
+ return pkg;
625
+ }
626
+
627
+ /** The canonical package site, used for the documentation link of a package. */
628
+ const ELM_REGISTRY_HOME = "https://package.elm-lang.org";
629
+
630
+ /**
631
+ * Base URL the registry documents are read from. `ELM_PACKAGE_URL` redirects
632
+ * lookups at a mirror, matching the `PUB_DEV_URL`/`RUST_CRATES_URL` convention.
633
+ *
634
+ * @returns {string} Registry base URL without a trailing slash
635
+ */
636
+ function elmRegistryUrl() {
637
+ const custom = readEnvironmentVariable("ELM_PACKAGE_URL");
638
+ return (custom || ELM_REGISTRY_HOME).replace(/\/+$/, "");
639
+ }
640
+
641
+ /**
642
+ * Package path prefixes the registry's own `robots.txt` disallows for every
643
+ * user-agent. They are the pre-0.19 packages, which the site keeps serving but
644
+ * asks clients not to walk; a 0.18 project's dependencies fall entirely inside
645
+ * this set, so its metadata comes from the local cache or not at all.
646
+ */
647
+ const ROBOTS_DISALLOWED_PREFIXES = [
648
+ "elm-lang/",
649
+ "elm-tools/",
650
+ "elm-community/elm-test/",
651
+ "evancz/elm-html/",
652
+ "evancz/elm-http/",
653
+ "evancz/elm-svg/",
654
+ "evancz/url-parser/",
655
+ "evancz/virtual-dom/",
656
+ ];
657
+
658
+ /**
659
+ * Whether the registry's `robots.txt` permits fetching a package's documents.
660
+ *
661
+ * @param {string} name Package name, `author/project`
662
+ * @returns {boolean} true when no `Disallow` rule covers the package
663
+ */
664
+ export function isRegistryCrawlable(name) {
665
+ const path = `${name}/`;
666
+ return !ROBOTS_DISALLOWED_PREFIXES.some((prefix) => path.startsWith(prefix));
667
+ }
668
+
669
+ /**
670
+ * Enrich Elm components from the package registry.
671
+ *
672
+ * Only the gaps left by the local cache are fetched, and only for packages
673
+ * with a resolved version that `robots.txt` permits. Two documents are read
674
+ * per package, both of which the compiler itself reads when it installs one:
675
+ *
676
+ * - `elm.json` — the published manifest, carrying the summary, the license
677
+ * and the package's own dependencies, which close the gaps in the
678
+ * dependency graph that an unpopulated cache leaves behind.
679
+ * - `endpoint.json` — the source archive URL and the SHA-1 of its bytes,
680
+ * which the compiler verifies after downloading. It becomes the
681
+ * component's distribution reference and hash.
682
+ *
683
+ * Requests carry cdxgen's contact user-agent and pass through the shared
684
+ * per-host rate policy for `package.elm-lang.org` (see `fetchRate.js`).
685
+ *
686
+ * @param {object[]} pkgList Components to enrich, mutated in place
687
+ * @param {object[]} [dependencies] Dependency edges to complete in place
688
+ * @returns {Promise<object[]>} The same package list
689
+ */
690
+ export async function getElmMetadata(pkgList, dependencies) {
691
+ const registry = elmRegistryUrl();
692
+ const targets = pkgList.filter(
693
+ (pkg) =>
694
+ pkg.group &&
695
+ pkg.version &&
696
+ semver.valid(pkg.version) &&
697
+ splitElmName(`${pkg.group}/${pkg.name}`) &&
698
+ isRegistryCrawlable(`${pkg.group}/${pkg.name}`),
699
+ );
700
+ if (!targets.length) {
701
+ return pkgList;
702
+ }
703
+ const manifestUrl = (pkg) =>
704
+ `${registry}/packages/${pkg.group}/${pkg.name}/${pkg.version}/elm.json`;
705
+ const endpointUrl = (pkg) =>
706
+ `${registry}/packages/${pkg.group}/${pkg.name}/${pkg.version}/endpoint.json`;
707
+ const prefetched = await prefetchJson(
708
+ prefetchEnabled()
709
+ ? targets.flatMap((pkg) => [
710
+ { url: manifestUrl(pkg) },
711
+ { url: endpointUrl(pkg) },
712
+ ])
713
+ : [],
714
+ );
715
+ const manifests = new Map();
716
+ for (const pkg of targets) {
717
+ if (DEBUG_MODE) {
718
+ console.log(`Querying ${registry} for ${pkg.group}/${pkg.name}`);
719
+ }
720
+ const manifest = await fetchJson(manifestUrl(pkg), prefetched);
721
+ if (manifest) {
722
+ manifests.set(pkg["bom-ref"], manifest);
723
+ if (!pkg.license && typeof manifest.license === "string") {
724
+ pkg.license = manifest.license;
725
+ }
726
+ if (!pkg.description && typeof manifest.summary === "string") {
727
+ pkg.description = manifest.summary;
728
+ }
729
+ }
730
+ const endpoint = await fetchJson(endpointUrl(pkg), prefetched);
731
+ if (typeof endpoint?.url === "string") {
732
+ pkg.distribution = { url: endpoint.url };
733
+ }
734
+ if (/^[a-f0-9]{40}$/i.test(endpoint?.hash || "")) {
735
+ // The compiler streams the archive and compares this digest against the
736
+ // SHA-1 of the bytes it received, so it is a content hash of the
737
+ // distribution rather than a revision identifier.
738
+ pkg.hashes = [{ alg: "SHA-1", content: endpoint.hash.toLowerCase() }];
739
+ }
740
+ }
741
+ if (dependencies?.length && manifests.size) {
742
+ completeDependencyEdges(dependencies, pkgList, manifests);
743
+ }
744
+ return pkgList;
745
+ }
746
+
747
+ /**
748
+ * Fill in the edges of components the local cache could not describe, using
749
+ * the manifests fetched from the registry. Edges already resolved from the
750
+ * cache are left alone.
751
+ *
752
+ * @param {object[]} dependencies Dependency edges, mutated in place
753
+ * @param {object[]} pkgList Components the edges may point at
754
+ * @param {Map<string, object>} manifests Registry manifests by bom-ref
755
+ */
756
+ function completeDependencyEdges(dependencies, pkgList, manifests) {
757
+ const refByName = new Map(
758
+ pkgList
759
+ .filter((pkg) => pkg.group)
760
+ .map((pkg) => [`${pkg.group}/${pkg.name}`, pkg["bom-ref"]]),
761
+ );
762
+ for (const edge of dependencies) {
763
+ if (edge.dependsOn?.length) {
764
+ continue;
765
+ }
766
+ const declared = manifests.get(edge.ref)?.dependencies;
767
+ if (!declared || typeof declared !== "object" || Array.isArray(declared)) {
768
+ continue;
769
+ }
770
+ edge.dependsOn = [
771
+ ...new Set(
772
+ Object.keys(declared)
773
+ .map((name) => refByName.get(name))
774
+ .filter((ref) => ref && ref !== edge.ref),
775
+ ),
776
+ ].sort();
777
+ }
778
+ }
779
+
780
+ /**
781
+ * Read one registry document, preferring the batched prefetch and falling back
782
+ * to a direct request. A failure leaves the component as the manifests found
783
+ * it rather than aborting the enrichment.
784
+ *
785
+ * @param {string} url Document URL
786
+ * @param {Map} prefetched Batched results
787
+ * @returns {Promise<object|undefined>} Parsed document
788
+ */
789
+ async function fetchJson(url, prefetched) {
790
+ try {
791
+ const res =
792
+ prefetchedResponse(prefetched, url) ||
793
+ (await cdxgenAgent.get(url, { responseType: "json" }));
794
+ return res?.body;
795
+ } catch (err) {
796
+ if (DEBUG_MODE) {
797
+ console.log(`Unable to fetch ${url}: ${err.message}`);
798
+ }
799
+ return undefined;
800
+ }
801
+ }
802
+
803
+ /**
804
+ * Read and parse a JSON file, warning instead of throwing on invalid input.
805
+ *
806
+ * @param {string} filePath File to read
807
+ * @returns {object|undefined} Parsed value
808
+ */
809
+ function readJsonFile(filePath) {
810
+ try {
811
+ return JSON.parse(readFileSync(filePath, "utf-8"));
812
+ } catch (error) {
813
+ console.warn(`Failed to parse ${filePath}: ${error.message}`);
814
+ return undefined;
815
+ }
816
+ }