@hominis/fireforge 0.45.0 → 0.46.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 (359) hide show
  1. package/CHANGELOG.md +80 -0
  2. package/dist/bin/fireforge.js +6 -0
  3. package/dist/build-info.json +4 -4
  4. package/dist/src/cli-usage-error.d.ts +46 -0
  5. package/dist/src/cli-usage-error.js +81 -0
  6. package/dist/src/cli.d.ts +5 -1
  7. package/dist/src/cli.js +20 -21
  8. package/dist/src/commands/bootstrap.js +1 -2
  9. package/dist/src/commands/build.js +15 -6
  10. package/dist/src/commands/destructive-decision.d.ts +19 -0
  11. package/dist/src/commands/destructive-decision.js +31 -0
  12. package/dist/src/commands/doctor-check-core.d.ts +0 -6
  13. package/dist/src/commands/doctor-check-core.js +0 -8
  14. package/dist/src/commands/doctor-orphaned-harness.d.ts +0 -6
  15. package/dist/src/commands/doctor-orphaned-harness.js +4 -12
  16. package/dist/src/commands/doctor-post-rebase-audit.js +1 -1
  17. package/dist/src/commands/doctor.d.ts +2 -0
  18. package/dist/src/commands/doctor.js +7 -8
  19. package/dist/src/commands/download.js +35 -24
  20. package/dist/src/commands/export-all.js +21 -4
  21. package/dist/src/commands/export-flow.js +15 -45
  22. package/dist/src/commands/export-placement-conflicts.js +3 -3
  23. package/dist/src/commands/export-placement-gate.js +2 -8
  24. package/dist/src/commands/export-placement-policy.js +3 -5
  25. package/dist/src/commands/export-shared.d.ts +42 -23
  26. package/dist/src/commands/export-shared.js +4 -19
  27. package/dist/src/commands/export.js +10 -1
  28. package/dist/src/commands/furnace/authoring-config.d.ts +13 -0
  29. package/dist/src/commands/furnace/authoring-config.js +19 -0
  30. package/dist/src/commands/furnace/chrome-doc-remove.js +1 -1
  31. package/dist/src/commands/furnace/create-browser-test.js +3 -8
  32. package/dist/src/commands/furnace/create-dry-run.js +2 -5
  33. package/dist/src/commands/furnace/create-templates.d.ts +17 -1
  34. package/dist/src/commands/furnace/create-templates.js +2 -1
  35. package/dist/src/commands/furnace/create.js +25 -19
  36. package/dist/src/commands/furnace/deploy.js +11 -2
  37. package/dist/src/commands/furnace/init.js +2 -1
  38. package/dist/src/commands/furnace/override.js +28 -19
  39. package/dist/src/commands/furnace/refresh.js +9 -1
  40. package/dist/src/commands/furnace/remove-mutations.d.ts +6 -7
  41. package/dist/src/commands/furnace/remove-mutations.js +0 -1
  42. package/dist/src/commands/furnace/remove.js +2 -6
  43. package/dist/src/commands/furnace/rename-browser-test.js +1 -7
  44. package/dist/src/commands/furnace/rename.js +29 -29
  45. package/dist/src/commands/furnace/status.js +12 -9
  46. package/dist/src/commands/furnace/test-file-name.d.ts +28 -0
  47. package/dist/src/commands/furnace/test-file-name.js +39 -0
  48. package/dist/src/commands/furnace/validation-output.d.ts +21 -9
  49. package/dist/src/commands/furnace/validation-output.js +3 -8
  50. package/dist/src/commands/import.js +2 -9
  51. package/dist/src/commands/lint-per-patch.d.ts +2 -2
  52. package/dist/src/commands/lint-per-patch.js +19 -4
  53. package/dist/src/commands/lint-per-run-checkjs.d.ts +2 -2
  54. package/dist/src/commands/lint.d.ts +2 -1
  55. package/dist/src/commands/lint.js +1 -1
  56. package/dist/src/commands/manifest.js +1 -1
  57. package/dist/src/commands/package.js +2 -4
  58. package/dist/src/commands/patch/compact.js +14 -15
  59. package/dist/src/commands/patch/delete.js +15 -25
  60. package/dist/src/commands/patch/entry-projection.d.ts +13 -0
  61. package/dist/src/commands/patch/entry-projection.js +24 -0
  62. package/dist/src/commands/patch/lint-ignore.d.ts +0 -8
  63. package/dist/src/commands/patch/lint-ignore.js +19 -29
  64. package/dist/src/commands/patch/move-files-create.js +24 -10
  65. package/dist/src/commands/patch/move-files-into.js +38 -34
  66. package/dist/src/commands/patch/move-files.js +4 -13
  67. package/dist/src/commands/patch/rename.js +27 -41
  68. package/dist/src/commands/patch/reorder.js +25 -38
  69. package/dist/src/commands/patch/split-plan.d.ts +4 -5
  70. package/dist/src/commands/patch/split-plan.js +4 -8
  71. package/dist/src/commands/patch/split.js +45 -43
  72. package/dist/src/commands/patch/staged-dependency.js +16 -22
  73. package/dist/src/commands/patch/tier.js +15 -25
  74. package/dist/src/commands/patch-rename-map.d.ts +19 -0
  75. package/dist/src/commands/patch-rename-map.js +36 -0
  76. package/dist/src/commands/pending-resolution.d.ts +15 -0
  77. package/dist/src/commands/pending-resolution.js +25 -0
  78. package/dist/src/commands/re-export-adjacent.d.ts +24 -2
  79. package/dist/src/commands/re-export-adjacent.js +32 -16
  80. package/dist/src/commands/re-export-bulk-scan.js +6 -10
  81. package/dist/src/commands/re-export-drift.js +4 -4
  82. package/dist/src/commands/re-export-files.d.ts +2 -3
  83. package/dist/src/commands/re-export-files.js +38 -29
  84. package/dist/src/commands/re-export-lint.d.ts +3 -4
  85. package/dist/src/commands/re-export-lint.js +9 -2
  86. package/dist/src/commands/re-export-scan.js +5 -2
  87. package/dist/src/commands/re-export-single.d.ts +22 -3
  88. package/dist/src/commands/re-export-single.js +19 -11
  89. package/dist/src/commands/re-export.js +28 -11
  90. package/dist/src/commands/rebase/abort.js +3 -8
  91. package/dist/src/commands/rebase/continue.js +12 -12
  92. package/dist/src/commands/rebase/index.js +2 -2
  93. package/dist/src/commands/rebase/patch-loop.d.ts +2 -2
  94. package/dist/src/commands/rebase/patch-loop.js +2 -7
  95. package/dist/src/commands/resolve.js +18 -10
  96. package/dist/src/commands/run.d.ts +0 -13
  97. package/dist/src/commands/run.js +12 -25
  98. package/dist/src/commands/setup-support.d.ts +16 -4
  99. package/dist/src/commands/setup-support.js +16 -16
  100. package/dist/src/commands/setup.js +3 -9
  101. package/dist/src/commands/source.js +8 -1
  102. package/dist/src/commands/status-check.js +2 -2
  103. package/dist/src/commands/status-output.js +2 -2
  104. package/dist/src/commands/status.js +3 -4
  105. package/dist/src/commands/test-build-phase.d.ts +2 -2
  106. package/dist/src/commands/test-build-phase.js +31 -7
  107. package/dist/src/commands/test-modes.d.ts +2 -2
  108. package/dist/src/commands/test-modes.js +0 -1
  109. package/dist/src/commands/test-register.js +5 -3
  110. package/dist/src/commands/test-run.js +25 -18
  111. package/dist/src/commands/test-stale-gate.js +4 -4
  112. package/dist/src/commands/test-verdict.d.ts +16 -2
  113. package/dist/src/commands/test-verdict.js +18 -3
  114. package/dist/src/commands/test.js +42 -5
  115. package/dist/src/commands/token-list.d.ts +26 -0
  116. package/dist/src/commands/token-list.js +153 -0
  117. package/dist/src/commands/token.js +17 -1
  118. package/dist/src/commands/tree.js +7 -2
  119. package/dist/src/commands/typecheck.js +9 -9
  120. package/dist/src/commands/wire.js +4 -3
  121. package/dist/src/core/branding.d.ts +0 -7
  122. package/dist/src/core/branding.js +3 -2
  123. package/dist/src/core/browser-wire.js +6 -3
  124. package/dist/src/core/build-audit-platform.d.ts +29 -0
  125. package/dist/src/core/build-audit-platform.js +140 -0
  126. package/dist/src/core/build-audit-registration.js +3 -2
  127. package/dist/src/core/build-audit-skip.d.ts +57 -0
  128. package/dist/src/core/build-audit-skip.js +112 -0
  129. package/dist/src/core/build-audit.d.ts +15 -2
  130. package/dist/src/core/build-audit.js +110 -10
  131. package/dist/src/core/build-baseline.d.ts +45 -28
  132. package/dist/src/core/build-baseline.js +6 -29
  133. package/dist/src/core/build-jar-escalation.js +3 -2
  134. package/dist/src/core/build-overwrite-guard.js +1 -1
  135. package/dist/src/core/build-prepare.d.ts +0 -8
  136. package/dist/src/core/build-prepare.js +7 -12
  137. package/dist/src/core/config-paths.d.ts +2 -6
  138. package/dist/src/core/config-paths.js +4 -2
  139. package/dist/src/core/config-state.d.ts +0 -6
  140. package/dist/src/core/config-state.js +1 -19
  141. package/dist/src/core/config-validate-build-audit.d.ts +8 -0
  142. package/dist/src/core/config-validate-build-audit.js +80 -0
  143. package/dist/src/core/config-validate-file-size.js +1 -1
  144. package/dist/src/core/config-validate-patch-policy.d.ts +2 -2
  145. package/dist/src/core/config-validate-patch-policy.js +3 -3
  146. package/dist/src/core/config-validate-test-toolchains.d.ts +2 -2
  147. package/dist/src/core/config-validate-test-toolchains.js +4 -5
  148. package/dist/src/core/config-validate.js +14 -3
  149. package/dist/src/core/config.d.ts +2 -2
  150. package/dist/src/core/config.js +11 -15
  151. package/dist/src/core/coverage-extend.js +4 -4
  152. package/dist/src/core/discard-baseline.js +7 -7
  153. package/dist/src/core/engine-precondition.d.ts +1 -9
  154. package/dist/src/core/engine-precondition.js +3 -8
  155. package/dist/src/core/engine-session-lock.js +13 -17
  156. package/dist/src/core/export-moved-code-shape.js +2 -2
  157. package/dist/src/core/file-lock-owner.d.ts +103 -0
  158. package/dist/src/core/file-lock-owner.js +259 -0
  159. package/dist/src/core/file-lock.d.ts +2 -7
  160. package/dist/src/core/file-lock.js +40 -91
  161. package/dist/src/core/firefox-cache.d.ts +13 -1
  162. package/dist/src/core/firefox-cache.js +59 -34
  163. package/dist/src/core/firefox-download.d.ts +15 -0
  164. package/dist/src/core/firefox-download.js +37 -21
  165. package/dist/src/core/firefox-extract.d.ts +6 -5
  166. package/dist/src/core/firefox-extract.js +36 -18
  167. package/dist/src/core/firefox.d.ts +30 -32
  168. package/dist/src/core/firefox.js +5 -36
  169. package/dist/src/core/furnace-apply-dry-run.d.ts +1 -6
  170. package/dist/src/core/furnace-apply-dry-run.js +1 -7
  171. package/dist/src/core/furnace-apply-helpers.d.ts +0 -11
  172. package/dist/src/core/furnace-apply-helpers.js +8 -19
  173. package/dist/src/core/furnace-apply-overwrite-warn.js +4 -2
  174. package/dist/src/core/furnace-apply.d.ts +1 -1
  175. package/dist/src/core/furnace-apply.js +30 -10
  176. package/dist/src/core/furnace-config.d.ts +1 -1
  177. package/dist/src/core/furnace-config.js +23 -36
  178. package/dist/src/core/furnace-constants.js +3 -2
  179. package/dist/src/core/furnace-dir-entry.d.ts +20 -0
  180. package/dist/src/core/furnace-dir-entry.js +21 -0
  181. package/dist/src/core/furnace-operation.d.ts +2 -0
  182. package/dist/src/core/furnace-operation.js +3 -1
  183. package/dist/src/core/furnace-refresh.d.ts +18 -6
  184. package/dist/src/core/furnace-refresh.js +3 -6
  185. package/dist/src/core/furnace-registration-ast.d.ts +0 -1
  186. package/dist/src/core/furnace-registration-ast.js +0 -3
  187. package/dist/src/core/furnace-registration.d.ts +2 -1
  188. package/dist/src/core/furnace-registration.js +22 -29
  189. package/dist/src/core/furnace-scanner.js +3 -6
  190. package/dist/src/core/furnace-state-persist.d.ts +2 -2
  191. package/dist/src/core/furnace-validate-registration.js +5 -7
  192. package/dist/src/core/git-base.d.ts +3 -1
  193. package/dist/src/core/git-base.js +25 -3
  194. package/dist/src/core/git-diff-new-file.d.ts +40 -0
  195. package/dist/src/core/git-diff-new-file.js +119 -0
  196. package/dist/src/core/git-diff.d.ts +9 -9
  197. package/dist/src/core/git-diff.js +88 -157
  198. package/dist/src/core/git-file-ops.d.ts +0 -7
  199. package/dist/src/core/git-file-ops.js +4 -28
  200. package/dist/src/core/git-readonly-index.d.ts +0 -2
  201. package/dist/src/core/git-readonly-index.js +0 -4
  202. package/dist/src/core/git-status.d.ts +7 -0
  203. package/dist/src/core/git-status.js +21 -9
  204. package/dist/src/core/git.d.ts +0 -6
  205. package/dist/src/core/git.js +14 -43
  206. package/dist/src/core/harness-orphans.js +8 -7
  207. package/dist/src/core/history-log.d.ts +19 -0
  208. package/dist/src/core/history-log.js +29 -0
  209. package/dist/src/core/mach-error-hints.d.ts +0 -5
  210. package/dist/src/core/mach-error-hints.js +1 -1
  211. package/dist/src/core/mach-known-noise-filter.d.ts +4 -3
  212. package/dist/src/core/mach-known-noise-filter.js +7 -9
  213. package/dist/src/core/mach-mozconfig.js +2 -1
  214. package/dist/src/core/mach-python.d.ts +2 -0
  215. package/dist/src/core/mach-python.js +12 -7
  216. package/dist/src/core/mach-resource-shim.d.ts +13 -1
  217. package/dist/src/core/mach-resource-shim.js +112 -9
  218. package/dist/src/core/mach.d.ts +39 -53
  219. package/dist/src/core/mach.js +13 -81
  220. package/dist/src/core/marionette-port.d.ts +0 -17
  221. package/dist/src/core/marionette-port.js +12 -58
  222. package/dist/src/core/marionette-preflight.js +9 -27
  223. package/dist/src/core/mochitest-server-port.js +3 -3
  224. package/dist/src/core/moz-manifest-rules.js +37 -33
  225. package/dist/src/core/patch-apply-fuzz.js +4 -4
  226. package/dist/src/core/patch-apply.d.ts +4 -5
  227. package/dist/src/core/patch-apply.js +6 -12
  228. package/dist/src/core/patch-export-coverage.d.ts +0 -9
  229. package/dist/src/core/patch-export-coverage.js +1 -30
  230. package/dist/src/core/patch-export-update.d.ts +18 -1
  231. package/dist/src/core/patch-export-update.js +4 -3
  232. package/dist/src/core/patch-export.d.ts +11 -21
  233. package/dist/src/core/patch-export.js +44 -66
  234. package/dist/src/core/patch-files.d.ts +0 -4
  235. package/dist/src/core/patch-files.js +0 -11
  236. package/dist/src/core/patch-lint-binary.d.ts +6 -12
  237. package/dist/src/core/patch-lint-cache.d.ts +17 -1
  238. package/dist/src/core/patch-lint-cache.js +3 -6
  239. package/dist/src/core/patch-lint-checkjs.d.ts +33 -16
  240. package/dist/src/core/patch-lint-checkjs.js +37 -24
  241. package/dist/src/core/patch-lint-creators.d.ts +2 -6
  242. package/dist/src/core/patch-lint-cross.d.ts +16 -8
  243. package/dist/src/core/patch-lint-cross.js +49 -92
  244. package/dist/src/core/patch-lint-css.js +10 -6
  245. package/dist/src/core/patch-lint-file-size.js +3 -3
  246. package/dist/src/core/patch-lint-forward-registration.d.ts +58 -0
  247. package/dist/src/core/patch-lint-forward-registration.js +210 -0
  248. package/dist/src/core/patch-lint-jsdoc.js +36 -36
  249. package/dist/src/core/patch-lint-module-registration.d.ts +2 -8
  250. package/dist/src/core/patch-lint-prettier.js +1 -1
  251. package/dist/src/core/patch-lint-queue-types.d.ts +60 -0
  252. package/dist/src/core/patch-lint-queue-types.js +12 -0
  253. package/dist/src/core/patch-lint-staged-registration.d.ts +61 -0
  254. package/dist/src/core/patch-lint-staged-registration.js +90 -0
  255. package/dist/src/core/patch-lint.d.ts +14 -9
  256. package/dist/src/core/patch-lint.js +4 -3
  257. package/dist/src/core/patch-lock.js +3 -3
  258. package/dist/src/core/patch-manifest-io.js +8 -7
  259. package/dist/src/core/patch-parse.d.ts +19 -8
  260. package/dist/src/core/patch-parse.js +25 -0
  261. package/dist/src/core/patch-policy.js +15 -27
  262. package/dist/src/core/patch-source-metadata.d.ts +1 -3
  263. package/dist/src/core/patch-source-metadata.js +0 -4
  264. package/dist/src/core/patch-transform.d.ts +0 -8
  265. package/dist/src/core/patch-transform.js +0 -10
  266. package/dist/src/core/process-cpu-time.js +1 -1
  267. package/dist/src/core/rebase-session.d.ts +2 -8
  268. package/dist/src/core/rebase-session.js +5 -17
  269. package/dist/src/core/register-browser-content.js +3 -2
  270. package/dist/src/core/register-browser-test.js +2 -1
  271. package/dist/src/core/register-module.js +2 -1
  272. package/dist/src/core/register-scaffold.js +5 -4
  273. package/dist/src/core/register-shared-css.js +2 -1
  274. package/dist/src/core/run-log-redact.d.ts +38 -0
  275. package/dist/src/core/run-log-redact.js +69 -0
  276. package/dist/src/core/run-log.d.ts +13 -2
  277. package/dist/src/core/run-log.js +67 -18
  278. package/dist/src/core/signal-critical.js +2 -4
  279. package/dist/src/core/smoke-patterns.d.ts +0 -1
  280. package/dist/src/core/smoke-patterns.js +6 -11
  281. package/dist/src/core/state-file.d.ts +12 -1
  282. package/dist/src/core/state-file.js +14 -1
  283. package/dist/src/core/status-classify.js +2 -1
  284. package/dist/src/core/test-changed-prefs.js +26 -11
  285. package/dist/src/core/test-harness-crash.js +2 -2
  286. package/dist/src/core/test-stale-check.d.ts +0 -2
  287. package/dist/src/core/test-stale-check.js +2 -2
  288. package/dist/src/core/test-xpcshell-retry.d.ts +26 -7
  289. package/dist/src/core/test-xpcshell-retry.js +14 -11
  290. package/dist/src/core/token-category.d.ts +33 -0
  291. package/dist/src/core/token-category.js +56 -48
  292. package/dist/src/core/token-dark-mode.js +2 -14
  293. package/dist/src/core/token-inventory.d.ts +56 -0
  294. package/dist/src/core/token-inventory.js +109 -0
  295. package/dist/src/core/token-manager.d.ts +1 -1
  296. package/dist/src/core/token-manager.js +1 -1
  297. package/dist/src/core/token-variant.js +21 -22
  298. package/dist/src/core/toolchain-preflight.js +1 -1
  299. package/dist/src/core/tree-cow.d.ts +0 -2
  300. package/dist/src/core/tree-cow.js +2 -2
  301. package/dist/src/core/tree-store.js +4 -5
  302. package/dist/src/core/typecheck.js +2 -2
  303. package/dist/src/core/wire-dom-fragment.js +6 -17
  304. package/dist/src/core/wire-utils.js +5 -17
  305. package/dist/src/core/xpcshell-appdir.js +2 -4
  306. package/dist/src/errors/base.d.ts +53 -1
  307. package/dist/src/errors/base.js +79 -11
  308. package/dist/src/errors/build.js +20 -28
  309. package/dist/src/errors/config.d.ts +0 -14
  310. package/dist/src/errors/config.js +13 -34
  311. package/dist/src/errors/download.d.ts +13 -0
  312. package/dist/src/errors/download.js +53 -24
  313. package/dist/src/errors/furnace.js +8 -9
  314. package/dist/src/errors/git.d.ts +8 -8
  315. package/dist/src/errors/git.js +20 -30
  316. package/dist/src/errors/patch.js +8 -10
  317. package/dist/src/errors/rebase.js +7 -7
  318. package/dist/src/types/commands/options.d.ts +77 -123
  319. package/dist/src/types/config.d.ts +34 -2
  320. package/dist/src/types/index.d.ts +0 -1
  321. package/dist/src/utils/assert.d.ts +3 -15
  322. package/dist/src/utils/assert.js +1 -18
  323. package/dist/src/utils/build-info.d.ts +1 -1
  324. package/dist/src/utils/child-env.d.ts +2 -1
  325. package/dist/src/utils/elapsed.js +10 -1
  326. package/dist/src/utils/fs.js +8 -13
  327. package/dist/src/utils/hash.d.ts +12 -0
  328. package/dist/src/utils/hash.js +16 -0
  329. package/dist/src/utils/line-dispatch.d.ts +15 -0
  330. package/dist/src/utils/line-dispatch.js +48 -0
  331. package/dist/src/utils/options.d.ts +1 -7
  332. package/dist/src/utils/options.js +6 -7
  333. package/dist/src/utils/package-root.js +3 -12
  334. package/dist/src/utils/platform.d.ts +0 -4
  335. package/dist/src/utils/process-group.d.ts +2 -1
  336. package/dist/src/utils/process-group.js +20 -19
  337. package/dist/src/utils/process.d.ts +40 -26
  338. package/dist/src/utils/process.js +115 -166
  339. package/dist/src/utils/ps-duration.js +3 -3
  340. package/dist/src/utils/sleep.d.ts +26 -0
  341. package/dist/src/utils/sleep.js +31 -0
  342. package/dist/src/utils/validation.d.ts +2 -38
  343. package/dist/src/utils/validation.js +7 -16
  344. package/docs/configuration.md +89 -1
  345. package/docs/exit-codes.md +31 -19
  346. package/docs/lifecycle-invariants.md +18 -12
  347. package/docs/machine-output.md +31 -8
  348. package/docs/patch-workflow.md +2 -1
  349. package/docs/run-logs.md +44 -4
  350. package/docs/testing.md +8 -1
  351. package/package.json +8 -7
  352. package/dist/src/commands/rebase.d.ts +0 -4
  353. package/dist/src/commands/rebase.js +0 -5
  354. package/dist/src/core/patch-artifact-normalize.d.ts +0 -21
  355. package/dist/src/core/patch-artifact-normalize.js +0 -24
  356. package/dist/src/core/patch-lint-reexports.d.ts +0 -7
  357. package/dist/src/core/patch-lint-reexports.js +0 -8
  358. package/dist/src/core/wire-targets.d.ts +0 -7
  359. package/dist/src/core/wire-targets.js +0 -8
@@ -7,13 +7,13 @@ import { createReadStream } from 'node:fs';
7
7
  import { readdir, rename, stat } from 'node:fs/promises';
8
8
  import { join } from 'node:path';
9
9
  import { pipeline } from 'node:stream/promises';
10
- import { ChecksumMismatchError } from '../errors/download.js';
10
+ import { ChecksumMismatchError, ChecksumUnavailableError } from '../errors/download.js';
11
11
  import { toError } from '../utils/errors.js';
12
12
  import { pathExistsStrict, readJson, removeFile, writeJson } from '../utils/fs.js';
13
13
  import { verbose, warn } from '../utils/logger.js';
14
14
  import { createSiblingLockPath, withFileLock } from './file-lock.js';
15
15
  import { validateArchiveMetadata } from './firefox-archive.js';
16
- import { downloadFile } from './firefox-download.js';
16
+ import { downloadFile, fetchWithRetry } from './firefox-download.js';
17
17
  /**
18
18
  * Computes the SHA-256 hex digest of a file.
19
19
  * @param filePath - Path to the file
@@ -26,10 +26,15 @@ async function sha256File(filePath) {
26
26
  }
27
27
  /**
28
28
  * Fetches Mozilla's published SHA256SUMS for the release and extracts the
29
- * digest for this archive. Returns null when the checksum file cannot be
30
- * fetched or does not list the archive — the caller warns loudly and
31
- * continues (offline mirrors and staging hosts legitimately lack it), while
32
- * a FETCHED-but-mismatching digest always fails closed.
29
+ * digest for this archive. Yields a reason instead of a digest when the
30
+ * checksum file cannot be fetched (non-2xx, network error, timeout) or does
31
+ * not list the archive (a captive portal's HTML page lands here too) — the
32
+ * caller decides whether that fails closed. The request goes through the
33
+ * same `fetchWithRetry` as the archive download — same 60 s abort ceiling
34
+ * per attempt, same three-attempt back-off on 429/5xx and network errors —
35
+ * so an unresponsive checksum host cannot hang the command after the
36
+ * tarball has landed, and one transient blip on the small checksum request
37
+ * does not fail the verification closed and discard the archive.
33
38
  *
34
39
  * Line format (both variants appear in the wild):
35
40
  * `<64-hex-digest> <release-relative-path>`
@@ -37,33 +42,67 @@ async function sha256File(filePath) {
37
42
  */
38
43
  async function fetchPublishedSha256(archive) {
39
44
  try {
40
- const response = await fetch(archive.checksumsUrl);
45
+ const response = await fetchWithRetry(archive.checksumsUrl);
41
46
  if (!response.ok) {
42
- verbose(`SHA256SUMS fetch returned HTTP ${String(response.status)} for ${archive.checksumsUrl}`);
43
- return null;
47
+ return { sha256: null, reason: `HTTP ${response.status} fetching SHA256SUMS` };
44
48
  }
45
49
  const body = await response.text();
46
50
  for (const line of body.split('\n')) {
47
51
  const match = /^([0-9a-f]{64})\s+\*?(.+)$/.exec(line.trim());
48
52
  if (match?.[1] && match[2] === archive.pathInChecksums) {
49
- return match[1];
53
+ return { sha256: match[1] };
50
54
  }
51
55
  }
52
- verbose(`SHA256SUMS at ${archive.checksumsUrl} does not list ${archive.pathInChecksums}`);
53
- return null;
56
+ return {
57
+ sha256: null,
58
+ reason: `SHA256SUMS does not list ${archive.pathInChecksums} (wrong file, or a non-Mozilla response)`,
59
+ };
54
60
  }
55
61
  catch (error) {
56
- verbose(`SHA256SUMS fetch failed for ${archive.checksumsUrl}: ${toError(error).message}`);
57
- return null;
62
+ return { sha256: null, reason: `SHA256SUMS fetch failed: ${toError(error).message}` };
58
63
  }
59
64
  }
65
+ /**
66
+ * Default integrity check for a download with no operator pin: the archive
67
+ * digest must equal the one Mozilla publishes. TLS alone is thin trust for
68
+ * the artifact that becomes the git baseline every patch is built on — a
69
+ * compromised CDN response would otherwise be trusted with no signal.
70
+ *
71
+ * Fails CLOSED both on a mismatch and when the published digest cannot be
72
+ * obtained: a 404, a captive-portal HTML page, or a dropped connection
73
+ * would otherwise quietly downgrade the check to "TLS looked fine", which
74
+ * is precisely the condition an attacker positioned on the network can
75
+ * arrange. `allowUnverifiedDownload` (an explicit config opt-in for offline
76
+ * mirrors) degrades the unavailable case to a loud warning; a FETCHED but
77
+ * mismatching digest is never accepted.
78
+ */
79
+ async function verifyAgainstPublishedSha256(archive, actualSha256, options) {
80
+ const published = await fetchPublishedSha256(archive);
81
+ if (published.sha256 !== null) {
82
+ if (actualSha256 !== published.sha256) {
83
+ throw new ChecksumMismatchError(archive.product, published.sha256, actualSha256, archive.url);
84
+ }
85
+ return;
86
+ }
87
+ verbose(`${published.reason} (${archive.checksumsUrl})`);
88
+ if (options.allowUnverifiedDownload !== true) {
89
+ throw new ChecksumUnavailableError(archive.product, archive.checksumsUrl, published.reason, archive.url);
90
+ }
91
+ warn(`Could not verify ${archive.filename} against Mozilla's published SHA256SUMS ` +
92
+ `(${published.reason}). firefox.allowUnverifiedDownload is set, so the download is ` +
93
+ 'trusted on TLS alone — pin firefox.sha256 in fireforge.json to verify it.');
94
+ }
60
95
  /**
61
96
  * Ensures a valid cached archive exists, downloading it if needed.
62
97
  * @param archive - Resolved archive descriptor
63
98
  * @param cacheDir - Cache directory
64
99
  * @param onProgress - Optional progress callback
100
+ * @param expectedSha256 - Operator-pinned digest; when set it replaces the
101
+ * published-SHA256SUMS check
102
+ * @param onCacheProgress - Phase progress messages
103
+ * @param integrity - Default integrity-check options (unpinned downloads)
65
104
  */
66
- export async function ensureCachedArchive(archive, cacheDir, onProgress, expectedSha256, onCacheProgress) {
105
+ export async function ensureCachedArchive(archive, cacheDir, onProgress, expectedSha256, onCacheProgress, integrity = {}) {
67
106
  const lockPath = createSiblingLockPath(join(cacheDir, archive.filename), '.fireforge-cache.lock');
68
107
  await withFileLock(lockPath, async () => {
69
108
  onCacheProgress?.(`Validating source archive cache metadata for ${archive.filename}...`);
@@ -78,7 +117,7 @@ export async function ensureCachedArchive(archive, cacheDir, onProgress, expecte
78
117
  else {
79
118
  await removeArchivePartFiles(archive, cacheDir);
80
119
  }
81
- await downloadToCache(archive, cacheDir, onProgress, expectedSha256, onCacheProgress);
120
+ await downloadToCache(archive, cacheDir, onProgress, expectedSha256, onCacheProgress, integrity);
82
121
  });
83
122
  }
84
123
  async function cacheEntryExists(archive, cacheDir) {
@@ -135,7 +174,7 @@ async function validateCachedArchive(archive, cacheDir, expectedSha256) {
135
174
  * @param cacheDir - Cache directory
136
175
  * @param onProgress - Optional progress callback
137
176
  */
138
- async function downloadToCache(archive, cacheDir, onProgress, expectedSha256, onCacheProgress) {
177
+ async function downloadToCache(archive, cacheDir, onProgress, expectedSha256, onCacheProgress, integrity = {}) {
139
178
  const tarballPath = join(cacheDir, archive.filename);
140
179
  // Use a unique .part path so concurrent downloads for the same archive
141
180
  // do not clobber each other's partial files.
@@ -157,24 +196,10 @@ async function downloadToCache(archive, cacheDir, onProgress, expectedSha256, on
157
196
  }
158
197
  }
159
198
  else {
160
- // Default integrity check: verify against Mozilla's published
161
- // SHA256SUMS. TLS alone is thin trust for the artifact that becomes
162
- // the git baseline every patch is built on — a compromised CDN
163
- // response would otherwise be trusted with no signal. Mismatch fails
164
- // closed (the catch below deletes the artifact); an unfetchable
165
- // SHA256SUMS degrades to a loud warning so offline/mirror workflows
166
- // keep working (pin firefox.sha256 in fireforge.json to fail closed
167
- // even then).
199
+ // Default integrity check (see verifyAgainstPublishedSha256): any
200
+ // failure throws, and the catch below deletes the promoted artifact.
168
201
  onCacheProgress?.(`Verifying archive against published SHA256SUMS...`);
169
- const publishedSha256 = await fetchPublishedSha256(archive);
170
- if (publishedSha256 === null) {
171
- warn(`Could not verify ${archive.filename} against Mozilla's published SHA256SUMS ` +
172
- `(${archive.checksumsUrl} unavailable). The download is trusted on TLS alone — ` +
173
- 'set firefox.sha256 in fireforge.json to require checksum verification.');
174
- }
175
- else if (sha256 !== publishedSha256) {
176
- throw new ChecksumMismatchError(archive.product, publishedSha256, sha256, archive.url);
177
- }
202
+ await verifyAgainstPublishedSha256(archive, sha256, integrity);
178
203
  }
179
204
  onCacheProgress?.(`Writing source archive cache metadata for ${archive.metadataFilename}...`);
180
205
  await writeJson(metadataPath, {
@@ -5,6 +5,21 @@
5
5
  * Progress callback for download operations.
6
6
  */
7
7
  export type ProgressCallback = (downloaded: number, total: number) => void;
8
+ /**
9
+ * Fetches a URL with timeout and bounded retry for transient failures
10
+ * (429/5xx, network errors, the per-request timeout).
11
+ *
12
+ * Any other response — 2xx or a non-retryable failure such as 404 — is
13
+ * returned as-is on the first attempt; the caller decides what a given
14
+ * status means for ITS request. Shared by the tarball download and the
15
+ * SHA256SUMS lookup so both requests the download makes survive the same
16
+ * transient blips: with checksum verification failing closed, an
17
+ * un-retried checksum fetch would throw away a just-landed 600 MB archive
18
+ * over one dropped connection.
19
+ *
20
+ * @throws DownloadError once every attempt has failed with a retryable error
21
+ */
22
+ export declare function fetchWithRetry(url: string): Promise<Response>;
8
23
  /**
9
24
  * Downloads a file from a URL with progress tracking, timeout, and retry.
10
25
  * @param url - URL to download
@@ -7,7 +7,13 @@ import { basename } from 'node:path';
7
7
  import { Readable, Transform } from 'node:stream';
8
8
  import { pipeline } from 'node:stream/promises';
9
9
  import { DownloadError, VersionNotFoundError } from '../errors/download.js';
10
- /** Default request timeout in milliseconds (60 seconds). */
10
+ import { sleep } from '../utils/sleep.js';
11
+ /**
12
+ * Per-attempt request timeout in milliseconds (60 seconds). Every request
13
+ * the download makes — the tarball and the SHA256SUMS lookup in
14
+ * `firefox-cache.ts` — goes through {@link fetchWithRetry} and so shares
15
+ * this abort ceiling.
16
+ */
11
17
  const REQUEST_TIMEOUT_MS = 60_000;
12
18
  /** Maximum number of download attempts. */
13
19
  const MAX_ATTEMPTS = 3;
@@ -18,11 +24,20 @@ const RETRY_BASE_DELAY_MS = 1_000;
18
24
  /** Stall detection timeout — abort if no data received for this duration (30 seconds). */
19
25
  const DOWNLOAD_STALL_TIMEOUT_MS = 30_000;
20
26
  /**
21
- * Fetches a URL with timeout and bounded retry for transient failures.
27
+ * Fetches a URL with timeout and bounded retry for transient failures
28
+ * (429/5xx, network errors, the per-request timeout).
22
29
  *
23
- * Non-retryable errors (e.g. 404) are thrown immediately.
30
+ * Any other response — 2xx or a non-retryable failure such as 404 — is
31
+ * returned as-is on the first attempt; the caller decides what a given
32
+ * status means for ITS request. Shared by the tarball download and the
33
+ * SHA256SUMS lookup so both requests the download makes survive the same
34
+ * transient blips: with checksum verification failing closed, an
35
+ * un-retried checksum fetch would throw away a just-landed 600 MB archive
36
+ * over one dropped connection.
37
+ *
38
+ * @throws DownloadError once every attempt has failed with a retryable error
24
39
  */
25
- async function fetchWithRetry(url) {
40
+ export async function fetchWithRetry(url) {
26
41
  let lastError;
27
42
  for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
28
43
  const controller = new AbortController();
@@ -31,23 +46,12 @@ async function fetchWithRetry(url) {
31
46
  }, REQUEST_TIMEOUT_MS);
32
47
  try {
33
48
  const response = await fetch(url, { signal: controller.signal });
34
- if (response.status === 404) {
35
- throw new VersionNotFoundError(basename(url).replace('.source.tar.xz', ''));
36
- }
37
- if (RETRYABLE_STATUS_CODES.has(response.status)) {
38
- lastError = new DownloadError(`HTTP ${response.status}: ${response.statusText}`, url);
39
- }
40
- else if (!response.ok) {
41
- throw new DownloadError(`HTTP ${response.status}: ${response.statusText}`, url);
42
- }
43
- else {
49
+ if (!RETRYABLE_STATUS_CODES.has(response.status)) {
44
50
  return response;
45
51
  }
52
+ lastError = new DownloadError(`HTTP ${response.status}: ${response.statusText}`, url);
46
53
  }
47
54
  catch (error) {
48
- if (error instanceof VersionNotFoundError || error instanceof DownloadError) {
49
- throw error;
50
- }
51
55
  // Network / timeout errors are retryable
52
56
  lastError = error;
53
57
  }
@@ -55,9 +59,7 @@ async function fetchWithRetry(url) {
55
59
  clearTimeout(timer);
56
60
  }
57
61
  if (attempt < MAX_ATTEMPTS - 1) {
58
- await new Promise((resolve) => {
59
- setTimeout(resolve, RETRY_BASE_DELAY_MS * (attempt + 1) ** 2);
60
- });
62
+ await sleep(RETRY_BASE_DELAY_MS * (attempt + 1) ** 2);
61
63
  }
62
64
  }
63
65
  if (lastError instanceof DownloadError) {
@@ -65,6 +67,20 @@ async function fetchWithRetry(url) {
65
67
  }
66
68
  throw new DownloadError(lastError instanceof Error ? lastError.message : 'Download failed after retries', url, lastError instanceof Error ? lastError : undefined);
67
69
  }
70
+ /**
71
+ * Fetches the archive itself: a 404 is the release not existing
72
+ * (`VersionNotFoundError`), any other non-2xx is a `DownloadError`.
73
+ */
74
+ async function fetchArchive(url) {
75
+ const response = await fetchWithRetry(url);
76
+ if (response.status === 404) {
77
+ throw new VersionNotFoundError(basename(url).replace('.source.tar.xz', ''));
78
+ }
79
+ if (!response.ok) {
80
+ throw new DownloadError(`HTTP ${response.status}: ${response.statusText}`, url);
81
+ }
82
+ return response;
83
+ }
68
84
  /**
69
85
  * Creates a Transform stream that aborts if no data is received within the stall timeout.
70
86
  * The timer resets on each chunk. If it fires, the stream is destroyed with a DownloadError.
@@ -110,7 +126,7 @@ function createStallDetector(url, timeoutMs = DOWNLOAD_STALL_TIMEOUT_MS) {
110
126
  * @returns The content-length if available, otherwise undefined
111
127
  */
112
128
  export async function downloadFile(url, destPath, onProgress) {
113
- const response = await fetchWithRetry(url);
129
+ const response = await fetchArchive(url);
114
130
  if (!response.body) {
115
131
  throw new DownloadError('No response body received', url);
116
132
  }
@@ -2,11 +2,12 @@
2
2
  * Firefox source extraction and installed version detection.
3
3
  */
4
4
  /**
5
- * Validates entry names from a `tar -tf` listing.
5
+ * Validates entry names from a `tar -tf` listing: escaping paths and
6
+ * `.git` members are both rejected.
6
7
  * @param names - Listing lines (one member name per line)
7
- * @returns The first unsafe member name, or null when all are safe
8
+ * @returns The first unsafe member name, or undefined when all are safe
8
9
  */
9
- export declare function findUnsafeArchiveEntryName(names: readonly string[]): string | null;
10
+ export declare function findUnsafeArchiveEntryName(names: readonly string[]): string | undefined;
10
11
  /**
11
12
  * Validates symlink and hardlink targets from a `tar -tvf` listing.
12
13
  *
@@ -17,9 +18,9 @@ export declare function findUnsafeArchiveEntryName(names: readonly string[]): st
17
18
  * target); hardlinks print ` link to target` on GNU tar and bsdtar alike.
18
19
  *
19
20
  * @param verboseLines - `tar -tvf` listing lines
20
- * @returns A description of the first unsafe link found, or null
21
+ * @returns A description of the first unsafe link found, or undefined
21
22
  */
22
- export declare function findUnsafeArchiveLink(verboseLines: readonly string[]): string | null;
23
+ export declare function findUnsafeArchiveLink(verboseLines: readonly string[]): string | undefined;
23
24
  /**
24
25
  * Extracts a tar.xz archive after a listing preflight that rejects
25
26
  * path-traversal member names and escaping link targets.
@@ -20,19 +20,42 @@ function isUnsafeArchivePath(path) {
20
20
  return path.split(/[\\/]/).includes('..');
21
21
  }
22
22
  /**
23
- * Validates entry names from a `tar -tf` listing.
23
+ * Returns true when an archive member path has a `.git` segment at any
24
+ * depth (`.git`, `.git/config`, `foo/.git/hooks/pre-commit`). Such a member
25
+ * lands INSIDE the extraction root, so the traversal check passes it — but
26
+ * the extracted tree is immediately turned into a git repository and
27
+ * committed, and git honours a pre-seeded `.git/config` (`core.fsmonitor`,
28
+ * `core.hooksPath`) or `.git/hooks/*` on that first commit. Only the `.git`
29
+ * directory/file is rejected; `.gitmodules`, `.gitignore` and the like are
30
+ * legitimate upstream files.
31
+ */
32
+ function hasGitSegment(path) {
33
+ return path.split(/[\\/]/).some((segment) => segment.toLowerCase() === '.git');
34
+ }
35
+ /**
36
+ * Returns true when an archive member NAME must be rejected: an escaping
37
+ * path, or a `.git` segment (see {@link hasGitSegment}). Link targets use
38
+ * the narrower {@link isUnsafeArchivePath} — a target can only be reached
39
+ * through a member name that was checked here.
40
+ */
41
+ function isUnsafeArchiveMemberName(name) {
42
+ return isUnsafeArchivePath(name) || hasGitSegment(name);
43
+ }
44
+ /**
45
+ * Validates entry names from a `tar -tf` listing: escaping paths and
46
+ * `.git` members are both rejected.
24
47
  * @param names - Listing lines (one member name per line)
25
- * @returns The first unsafe member name, or null when all are safe
48
+ * @returns The first unsafe member name, or undefined when all are safe
26
49
  */
27
50
  export function findUnsafeArchiveEntryName(names) {
28
51
  for (const raw of names) {
29
52
  const name = raw.trim();
30
53
  if (name.length === 0)
31
54
  continue;
32
- if (isUnsafeArchivePath(name))
55
+ if (isUnsafeArchiveMemberName(name))
33
56
  return name;
34
57
  }
35
- return null;
58
+ return undefined;
36
59
  }
37
60
  /**
38
61
  * Validates symlink and hardlink targets from a `tar -tvf` listing.
@@ -44,7 +67,7 @@ export function findUnsafeArchiveEntryName(names) {
44
67
  * target); hardlinks print ` link to target` on GNU tar and bsdtar alike.
45
68
  *
46
69
  * @param verboseLines - `tar -tvf` listing lines
47
- * @returns A description of the first unsafe link found, or null
70
+ * @returns A description of the first unsafe link found, or undefined
48
71
  */
49
72
  export function findUnsafeArchiveLink(verboseLines) {
50
73
  for (const line of verboseLines) {
@@ -64,7 +87,7 @@ export function findUnsafeArchiveLink(verboseLines) {
64
87
  return `hardlink target: ${target}`;
65
88
  }
66
89
  }
67
- return null;
90
+ return undefined;
68
91
  }
69
92
  /**
70
93
  * Lists the archive and rejects members that could escape the extraction
@@ -111,7 +134,7 @@ function createLineScanner(onLine) {
111
134
  }
112
135
  /**
113
136
  * Runs one tar listing pass, streaming lines through `checkLine` and
114
- * returning the first unsafe finding (or null) plus the exit code.
137
+ * returning the first unsafe finding (or undefined) plus the exit code.
115
138
  *
116
139
  * Streaming matters for safety, not just memory: a buffered `exec` collector
117
140
  * silently truncates at 50 MB, and a full Firefox `-tvf` listing already
@@ -124,10 +147,10 @@ function createLineScanner(onLine) {
124
147
  async function runListingScan(archivePath, tarFlag, checkLine) {
125
148
  // LC_ALL=C keeps the -tvf column format stable for the link parse.
126
149
  const listEnv = { LC_ALL: 'C', LANG: 'C' };
127
- let unsafe = null;
150
+ let unsafe;
128
151
  let stderrTail = '';
129
152
  const scanner = createLineScanner((line) => {
130
- if (unsafe === null) {
153
+ if (unsafe === undefined) {
131
154
  unsafe = checkLine(line);
132
155
  }
133
156
  });
@@ -146,24 +169,19 @@ async function runListingScan(archivePath, tarFlag, checkLine) {
146
169
  }
147
170
  async function preflightArchiveEntries(archivePath) {
148
171
  const [nameScan, linkScan] = await Promise.all([
149
- runListingScan(archivePath, '-tf', (line) => {
150
- const name = line.trim();
151
- if (name.length === 0)
152
- return null;
153
- return isUnsafeArchivePath(name) ? name : null;
154
- }),
172
+ runListingScan(archivePath, '-tf', (line) => findUnsafeArchiveEntryName([line])),
155
173
  runListingScan(archivePath, '-tvf', (line) => findUnsafeArchiveLink([line])),
156
174
  ]);
157
175
  if (nameScan.exitCode !== 0) {
158
176
  throw new ExtractionError(archivePath, new Error(`tar -tf preflight exited with code ${nameScan.exitCode}:\n${nameScan.stderrTail}`));
159
177
  }
160
- if (nameScan.unsafe !== null) {
161
- throw new ExtractionError(archivePath, new Error(`Archive rejected: member name could escape the extraction root: ${nameScan.unsafe}`));
178
+ if (nameScan.unsafe !== undefined) {
179
+ throw new ExtractionError(archivePath, new Error(`Archive rejected: member name could escape the extraction root or seed .git: ${nameScan.unsafe}`));
162
180
  }
163
181
  if (linkScan.exitCode !== 0) {
164
182
  throw new ExtractionError(archivePath, new Error(`tar -tvf preflight exited with code ${linkScan.exitCode}:\n${linkScan.stderrTail}`));
165
183
  }
166
- if (linkScan.unsafe !== null) {
184
+ if (linkScan.unsafe !== undefined) {
167
185
  throw new ExtractionError(archivePath, new Error(`Archive rejected: link could escape the extraction root (${linkScan.unsafe})`));
168
186
  }
169
187
  }
@@ -5,26 +5,9 @@
5
5
  * {@link downloadFirefoxSource} orchestrator.
6
6
  */
7
7
  import type { FirefoxProduct } from '../types/config.js';
8
+ import type { ArchiveIntegrityOptions } from './firefox-cache.js';
8
9
  import type { ProgressCallback } from './firefox-download.js';
9
- export { resolveArchive } from './firefox-archive.js';
10
- export type { ProgressCallback } from './firefox-download.js';
11
10
  export { formatBytes, getFirefoxVersion } from './firefox-extract.js';
12
- /**
13
- * Gets the download URL for a Firefox source tarball.
14
- * @param version - Firefox version (e.g., "140.9.0esr")
15
- * @param product - Firefox product type
16
- * @param candidate - Optional release-candidate build directory (e.g. "build2")
17
- * @returns Full URL to the source tarball
18
- */
19
- export declare function getDownloadUrl(version: string, product?: FirefoxProduct, candidate?: string): string;
20
- /**
21
- * Gets the filename for a Firefox source tarball.
22
- * @param version - Firefox version
23
- * @param product - Firefox product type
24
- * @param candidate - Optional release-candidate build directory (e.g. "build2")
25
- * @returns Tarball filename
26
- */
27
- export declare function getTarballFilename(version: string, product?: FirefoxProduct, candidate?: string): string;
28
11
  /**
29
12
  * Lifecycle phase reported by {@link downloadFirefoxSource}. The download
30
13
  * CLI command uses this to swap spinners between the bytes-on-the-wire phase
@@ -49,20 +32,35 @@ export type FirefoxSourceProgressCallback = (message: string) => void;
49
32
  * @returns names of the directories that were removed
50
33
  */
51
34
  export declare function sweepOrphanedEngineWorkDirs(destDir: string): Promise<string[]>;
35
+ /** Inputs for {@link downloadFirefoxSource}. */
36
+ export interface DownloadFirefoxSourceOptions {
37
+ /** Firefox version to fetch, e.g. `140.9.0esr`. */
38
+ version: string;
39
+ /** Product channel the version belongs to. */
40
+ product: FirefoxProduct;
41
+ /** Directory the extracted source tree is moved into. */
42
+ destDir: string;
43
+ /** Directory holding downloaded tarballs between runs. */
44
+ cacheDir: string;
45
+ /** Called with byte progress for the whole transfer. */
46
+ onProgress?: ProgressCallback | undefined;
47
+ /** Called when the run moves from downloading to extracting. */
48
+ onPhase?: FirefoxSourcePhaseCallback | undefined;
49
+ /** Expected archive checksum; verified after download when supplied. */
50
+ expectedSha256?: string | undefined;
51
+ /** Called with byte progress within the current phase. */
52
+ onPhaseProgress?: FirefoxSourceProgressCallback | undefined;
53
+ /**
54
+ * Release-candidate build directory (e.g. `build2`), resolving the archive
55
+ * under `candidates/` instead of `releases/`.
56
+ */
57
+ candidate?: string | undefined;
58
+ /** Integrity-check defaults for unpinned downloads. */
59
+ integrity?: ArchiveIntegrityOptions | undefined;
60
+ }
52
61
  /**
53
62
  * Downloads and extracts Firefox source.
54
- * @param version - Firefox version to download
55
- * @param product - Firefox product type
56
- * @param destDir - Destination directory for extracted source
57
- * @param cacheDir - Directory to store downloaded tarball
58
- * @param onProgress - Optional progress callback for the download byte stream
59
- * @param onPhase - Optional callback fired when the function transitions
60
- * between phases (`'download'` → `'extract'`). Fires exactly once per
61
- * phase even if the cached archive path skips the wire entirely.
62
- * @param expectedSha256 - Expected archive checksum; verified after download when supplied
63
- * @param onPhaseProgress - Called with byte progress within the current phase
64
- * @param candidate - Optional release-candidate build directory (e.g.
65
- * "build2") resolving the archive under `candidates/` instead of
66
- * `releases/`.
63
+ *
64
+ * @param options - See {@link DownloadFirefoxSourceOptions}.
67
65
  */
68
- export declare function downloadFirefoxSource(version: string, product: FirefoxProduct, destDir: string, cacheDir: string, onProgress?: ProgressCallback, onPhase?: FirefoxSourcePhaseCallback, expectedSha256?: string, onPhaseProgress?: FirefoxSourceProgressCallback, candidate?: string): Promise<void>;
66
+ export declare function downloadFirefoxSource(options: DownloadFirefoxSourceOptions): Promise<void>;
@@ -14,28 +14,7 @@ import { resolveArchive } from './firefox-archive.js';
14
14
  import { ensureCachedArchive, invalidateArchiveCache } from './firefox-cache.js';
15
15
  import { extractTarXz } from './firefox-extract.js';
16
16
  // ── Re-exports (preserve public API) ──
17
- export { resolveArchive } from './firefox-archive.js';
18
17
  export { formatBytes, getFirefoxVersion } from './firefox-extract.js';
19
- /**
20
- * Gets the download URL for a Firefox source tarball.
21
- * @param version - Firefox version (e.g., "140.9.0esr")
22
- * @param product - Firefox product type
23
- * @param candidate - Optional release-candidate build directory (e.g. "build2")
24
- * @returns Full URL to the source tarball
25
- */
26
- export function getDownloadUrl(version, product = 'firefox', candidate) {
27
- return resolveArchive(version, product, candidate).url;
28
- }
29
- /**
30
- * Gets the filename for a Firefox source tarball.
31
- * @param version - Firefox version
32
- * @param product - Firefox product type
33
- * @param candidate - Optional release-candidate build directory (e.g. "build2")
34
- * @returns Tarball filename
35
- */
36
- export function getTarballFilename(version, product = 'firefox', candidate) {
37
- return resolveArchive(version, product, candidate).filename;
38
- }
39
18
  /**
40
19
  * Classifies an extraction failure as archive corruption (invalidate the
41
20
  * cache and re-download) versus an environmental problem (keep the cached
@@ -95,27 +74,17 @@ export async function sweepOrphanedEngineWorkDirs(destDir) {
95
74
  }
96
75
  /**
97
76
  * Downloads and extracts Firefox source.
98
- * @param version - Firefox version to download
99
- * @param product - Firefox product type
100
- * @param destDir - Destination directory for extracted source
101
- * @param cacheDir - Directory to store downloaded tarball
102
- * @param onProgress - Optional progress callback for the download byte stream
103
- * @param onPhase - Optional callback fired when the function transitions
104
- * between phases (`'download'` → `'extract'`). Fires exactly once per
105
- * phase even if the cached archive path skips the wire entirely.
106
- * @param expectedSha256 - Expected archive checksum; verified after download when supplied
107
- * @param onPhaseProgress - Called with byte progress within the current phase
108
- * @param candidate - Optional release-candidate build directory (e.g.
109
- * "build2") resolving the archive under `candidates/` instead of
110
- * `releases/`.
77
+ *
78
+ * @param options - See {@link DownloadFirefoxSourceOptions}.
111
79
  */
112
- export async function downloadFirefoxSource(version, product, destDir, cacheDir, onProgress, onPhase, expectedSha256, onPhaseProgress, candidate) {
80
+ export async function downloadFirefoxSource(options) {
81
+ const { version, product, destDir, cacheDir, onProgress, onPhase, expectedSha256, onPhaseProgress, candidate, integrity, } = options;
113
82
  const archive = resolveArchive(version, product, candidate);
114
83
  const tarballPath = join(cacheDir, archive.filename);
115
84
  // Ensure cache directory exists
116
85
  await ensureDir(cacheDir);
117
86
  onPhase?.('download');
118
- await ensureCachedArchive(archive, cacheDir, onProgress, expectedSha256, onPhaseProgress);
87
+ await ensureCachedArchive(archive, cacheDir, onProgress, expectedSha256, onPhaseProgress, integrity);
119
88
  // Extract to a unique temporary directory so concurrent downloads for
120
89
  // the same destination do not clobber each other.
121
90
  onPhase?.('extract');
@@ -4,14 +4,9 @@
4
4
  * stay within the per-file line budget. Consumed only by that module.
5
5
  */
6
6
  import type { ComponentApplyContext, CustomComponentConfig, DryRunAction, StepError } from '../types/furnace.js';
7
- interface DirectoryEntry {
8
- isFile(): boolean;
9
- isSymbolicLink?(): boolean;
10
- name: string;
11
- }
7
+ import { type DirectoryEntry } from './furnace-dir-entry.js';
12
8
  /** Computes the planned dry-run actions (and pre-flight step errors) for a custom component. */
13
9
  export declare function buildCustomDryRunActions(ctx: Pick<ComponentApplyContext, 'engineDir' | 'name' | 'componentDir' | 'ftlDir'>, config: CustomComponentConfig, targetDir: string, entries: DirectoryEntry[]): Promise<{
14
10
  actions: DryRunAction[];
15
11
  stepErrors: StepError[];
16
12
  }>;
17
- export {};
@@ -9,14 +9,8 @@ import { toError } from '../utils/errors.js';
9
9
  import { pathExists } from '../utils/fs.js';
10
10
  import { describeLocaleFtlJarMnRegistration, describeSharedFtlPrune } from './furnace-apply-ftl.js';
11
11
  import { describeFragmentExpansion } from './furnace-css-fragments.js';
12
+ import { isRegularFile } from './furnace-dir-entry.js';
12
13
  import { validateCustomElementRegistration, validateJarMnInsertionForFiles, } from './furnace-registration.js';
13
- function isRegularFile(entry) {
14
- if (!entry.isFile())
15
- return false;
16
- if (typeof entry.isSymbolicLink === 'function' && entry.isSymbolicLink())
17
- return false;
18
- return true;
19
- }
20
14
  /** Computes the planned dry-run actions (and pre-flight step errors) for a custom component. */
21
15
  export async function buildCustomDryRunActions(ctx, config, targetDir, entries) {
22
16
  const { engineDir, name, componentDir, ftlDir } = ctx;
@@ -1,16 +1,5 @@
1
1
  import type { ComponentApplyContext, CustomComponentConfig, DryRunAction, OverrideComponentConfig, StepError } from '../types/furnace.js';
2
2
  import { type RollbackJournal } from './furnace-rollback.js';
3
- interface DirectoryEntry {
4
- isFile(): boolean;
5
- isSymbolicLink?(): boolean;
6
- name: string;
7
- }
8
- /**
9
- * True for a plain-file directory entry — symlinks and directories are
10
- * never copy candidates. Exported for the patch-owned overwrite probe,
11
- * which walks the same override copy-candidate set as apply.
12
- */
13
- export declare function isRegularFile(entry: DirectoryEntry): boolean;
14
3
  /**
15
4
  * Filter deciding which files in an override workspace directory are candidates
16
5
  * for copying into the engine. Exported so `furnace remove` can invert apply