@hominis/fireforge 0.45.0 → 0.46.1

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 (662) hide show
  1. package/CHANGELOG.md +409 -446
  2. package/dist/bin/fireforge.d.ts +1 -1
  3. package/dist/bin/fireforge.js +21 -15
  4. package/dist/build-info.json +4 -4
  5. package/dist/src/cli-usage-error.d.ts +46 -0
  6. package/dist/src/cli-usage-error.js +81 -0
  7. package/dist/src/cli.d.ts +7 -2
  8. package/dist/src/cli.js +47 -47
  9. package/dist/src/commands/bootstrap-checks.js +2 -1
  10. package/dist/src/commands/bootstrap.js +3 -4
  11. package/dist/src/commands/build.js +27 -17
  12. package/dist/src/commands/config.js +9 -9
  13. package/dist/src/commands/destructive-decision.d.ts +19 -0
  14. package/dist/src/commands/destructive-decision.js +31 -0
  15. package/dist/src/commands/discard.js +5 -5
  16. package/dist/src/commands/doctor-check-core.d.ts +4 -10
  17. package/dist/src/commands/doctor-check-core.js +0 -8
  18. package/dist/src/commands/doctor-external-toolchains.js +2 -2
  19. package/dist/src/commands/doctor-furnace-config-sync.d.ts +2 -2
  20. package/dist/src/commands/doctor-furnace-config-sync.js +20 -20
  21. package/dist/src/commands/doctor-furnace-jar.d.ts +1 -1
  22. package/dist/src/commands/doctor-furnace-jar.js +1 -1
  23. package/dist/src/commands/doctor-furnace.js +22 -22
  24. package/dist/src/commands/doctor-orphaned-harness.d.ts +10 -17
  25. package/dist/src/commands/doctor-orphaned-harness.js +14 -23
  26. package/dist/src/commands/doctor-patch-manifest.d.ts +2 -1
  27. package/dist/src/commands/doctor-patch-manifest.js +7 -6
  28. package/dist/src/commands/doctor-post-rebase-audit.js +3 -3
  29. package/dist/src/commands/doctor-source-pin.d.ts +12 -12
  30. package/dist/src/commands/doctor-source-pin.js +14 -14
  31. package/dist/src/commands/doctor-working-tree.d.ts +3 -3
  32. package/dist/src/commands/doctor-working-tree.js +3 -3
  33. package/dist/src/commands/doctor.d.ts +3 -1
  34. package/dist/src/commands/doctor.js +15 -16
  35. package/dist/src/commands/download.js +50 -37
  36. package/dist/src/commands/export-all.js +29 -12
  37. package/dist/src/commands/export-flow.d.ts +5 -5
  38. package/dist/src/commands/export-flow.js +29 -59
  39. package/dist/src/commands/export-placement-conflicts.d.ts +3 -3
  40. package/dist/src/commands/export-placement-conflicts.js +12 -12
  41. package/dist/src/commands/export-placement-gate.d.ts +2 -2
  42. package/dist/src/commands/export-placement-gate.js +7 -13
  43. package/dist/src/commands/export-placement-policy.d.ts +1 -1
  44. package/dist/src/commands/export-placement-policy.js +4 -6
  45. package/dist/src/commands/export-shared.d.ts +51 -32
  46. package/dist/src/commands/export-shared.js +17 -32
  47. package/dist/src/commands/export.js +14 -5
  48. package/dist/src/commands/furnace/apply.js +16 -16
  49. package/dist/src/commands/furnace/authoring-config.d.ts +13 -0
  50. package/dist/src/commands/furnace/authoring-config.js +19 -0
  51. package/dist/src/commands/furnace/chrome-doc-remove.d.ts +1 -1
  52. package/dist/src/commands/furnace/chrome-doc-remove.js +2 -2
  53. package/dist/src/commands/furnace/chrome-doc-templates.d.ts +14 -14
  54. package/dist/src/commands/furnace/chrome-doc-templates.js +17 -17
  55. package/dist/src/commands/furnace/chrome-doc-tests.d.ts +2 -2
  56. package/dist/src/commands/furnace/chrome-doc-tests.js +4 -4
  57. package/dist/src/commands/furnace/chrome-doc.d.ts +8 -8
  58. package/dist/src/commands/furnace/chrome-doc.js +18 -18
  59. package/dist/src/commands/furnace/create-browser-test.d.ts +1 -1
  60. package/dist/src/commands/furnace/create-browser-test.js +7 -12
  61. package/dist/src/commands/furnace/create-dry-run.d.ts +4 -4
  62. package/dist/src/commands/furnace/create-dry-run.js +6 -9
  63. package/dist/src/commands/furnace/create-features.d.ts +2 -2
  64. package/dist/src/commands/furnace/create-features.js +2 -2
  65. package/dist/src/commands/furnace/create-mochikit.d.ts +4 -4
  66. package/dist/src/commands/furnace/create-mochikit.js +7 -7
  67. package/dist/src/commands/furnace/create-templates.d.ts +31 -15
  68. package/dist/src/commands/furnace/create-templates.js +20 -19
  69. package/dist/src/commands/furnace/create-xpcshell.d.ts +5 -5
  70. package/dist/src/commands/furnace/create-xpcshell.js +8 -8
  71. package/dist/src/commands/furnace/create.d.ts +1 -1
  72. package/dist/src/commands/furnace/create.js +35 -29
  73. package/dist/src/commands/furnace/deploy.js +15 -6
  74. package/dist/src/commands/furnace/diff.js +7 -7
  75. package/dist/src/commands/furnace/index.js +1 -1
  76. package/dist/src/commands/furnace/init.js +17 -16
  77. package/dist/src/commands/furnace/list.js +2 -2
  78. package/dist/src/commands/furnace/override.d.ts +1 -1
  79. package/dist/src/commands/furnace/override.js +43 -34
  80. package/dist/src/commands/furnace/preview.js +17 -17
  81. package/dist/src/commands/furnace/refresh.js +15 -7
  82. package/dist/src/commands/furnace/remove-mutations.d.ts +9 -10
  83. package/dist/src/commands/furnace/remove-mutations.js +19 -19
  84. package/dist/src/commands/furnace/remove-state.d.ts +1 -1
  85. package/dist/src/commands/furnace/remove-state.js +1 -1
  86. package/dist/src/commands/furnace/remove.js +4 -8
  87. package/dist/src/commands/furnace/rename-browser-test.js +10 -15
  88. package/dist/src/commands/furnace/rename-xpcshell.d.ts +2 -2
  89. package/dist/src/commands/furnace/rename-xpcshell.js +3 -3
  90. package/dist/src/commands/furnace/rename.js +36 -36
  91. package/dist/src/commands/furnace/scan.js +3 -3
  92. package/dist/src/commands/furnace/status.js +19 -16
  93. package/dist/src/commands/furnace/sync.js +6 -6
  94. package/dist/src/commands/furnace/test-file-name.d.ts +28 -0
  95. package/dist/src/commands/furnace/test-file-name.js +39 -0
  96. package/dist/src/commands/furnace/validate.js +11 -12
  97. package/dist/src/commands/furnace/validation-output.d.ts +21 -9
  98. package/dist/src/commands/furnace/validation-output.js +3 -8
  99. package/dist/src/commands/import.js +22 -29
  100. package/dist/src/commands/lint-per-patch.d.ts +2 -2
  101. package/dist/src/commands/lint-per-patch.js +27 -12
  102. package/dist/src/commands/lint-per-run-checkjs.d.ts +14 -15
  103. package/dist/src/commands/lint-per-run-checkjs.js +14 -15
  104. package/dist/src/commands/lint-report.d.ts +3 -3
  105. package/dist/src/commands/lint-report.js +4 -4
  106. package/dist/src/commands/lint.d.ts +5 -4
  107. package/dist/src/commands/lint.js +36 -36
  108. package/dist/src/commands/manifest.d.ts +4 -4
  109. package/dist/src/commands/manifest.js +1 -1
  110. package/dist/src/commands/package.js +4 -6
  111. package/dist/src/commands/patch/compact.d.ts +2 -2
  112. package/dist/src/commands/patch/compact.js +19 -20
  113. package/dist/src/commands/patch/delete.d.ts +1 -1
  114. package/dist/src/commands/patch/delete.js +23 -33
  115. package/dist/src/commands/patch/entry-projection.d.ts +13 -0
  116. package/dist/src/commands/patch/entry-projection.js +24 -0
  117. package/dist/src/commands/patch/lint-ignore.d.ts +3 -12
  118. package/dist/src/commands/patch/lint-ignore.js +33 -44
  119. package/dist/src/commands/patch/move-files-create.d.ts +2 -2
  120. package/dist/src/commands/patch/move-files-create.js +29 -15
  121. package/dist/src/commands/patch/move-files-into.d.ts +2 -2
  122. package/dist/src/commands/patch/move-files-into.js +41 -37
  123. package/dist/src/commands/patch/move-files.js +5 -14
  124. package/dist/src/commands/patch/patch-context.d.ts +1 -1
  125. package/dist/src/commands/patch/rename.d.ts +4 -4
  126. package/dist/src/commands/patch/rename.js +35 -49
  127. package/dist/src/commands/patch/reorder.js +35 -48
  128. package/dist/src/commands/patch/split-plan.d.ts +7 -8
  129. package/dist/src/commands/patch/split-plan.js +10 -13
  130. package/dist/src/commands/patch/split.d.ts +2 -2
  131. package/dist/src/commands/patch/split.js +50 -48
  132. package/dist/src/commands/patch/staged-dependency-validate.d.ts +1 -1
  133. package/dist/src/commands/patch/staged-dependency-validate.js +3 -3
  134. package/dist/src/commands/patch/staged-dependency.d.ts +1 -1
  135. package/dist/src/commands/patch/staged-dependency.js +22 -28
  136. package/dist/src/commands/patch/tier.d.ts +2 -2
  137. package/dist/src/commands/patch/tier.js +20 -30
  138. package/dist/src/commands/patch-rename-map.d.ts +19 -0
  139. package/dist/src/commands/patch-rename-map.js +36 -0
  140. package/dist/src/commands/pending-resolution.d.ts +15 -0
  141. package/dist/src/commands/pending-resolution.js +25 -0
  142. package/dist/src/commands/re-export-adjacent.d.ts +25 -3
  143. package/dist/src/commands/re-export-adjacent.js +40 -24
  144. package/dist/src/commands/re-export-bulk-scan.d.ts +3 -3
  145. package/dist/src/commands/re-export-bulk-scan.js +23 -27
  146. package/dist/src/commands/re-export-drift.d.ts +10 -10
  147. package/dist/src/commands/re-export-drift.js +33 -33
  148. package/dist/src/commands/re-export-files.d.ts +2 -3
  149. package/dist/src/commands/re-export-files.js +52 -44
  150. package/dist/src/commands/re-export-lint.d.ts +29 -30
  151. package/dist/src/commands/re-export-lint.js +36 -29
  152. package/dist/src/commands/re-export-options.d.ts +1 -1
  153. package/dist/src/commands/re-export-options.js +1 -1
  154. package/dist/src/commands/re-export-register.d.ts +1 -1
  155. package/dist/src/commands/re-export-register.js +1 -1
  156. package/dist/src/commands/re-export-scan.d.ts +1 -1
  157. package/dist/src/commands/re-export-scan.js +12 -9
  158. package/dist/src/commands/re-export-single.d.ts +23 -4
  159. package/dist/src/commands/re-export-single.js +39 -31
  160. package/dist/src/commands/re-export.js +35 -17
  161. package/dist/src/commands/rebase/abort.js +9 -14
  162. package/dist/src/commands/rebase/continue.js +20 -19
  163. package/dist/src/commands/rebase/index.d.ts +1 -1
  164. package/dist/src/commands/rebase/index.js +5 -5
  165. package/dist/src/commands/rebase/patch-loop.d.ts +2 -2
  166. package/dist/src/commands/rebase/patch-loop.js +20 -24
  167. package/dist/src/commands/register.js +5 -5
  168. package/dist/src/commands/reset.js +1 -1
  169. package/dist/src/commands/resolve.d.ts +2 -2
  170. package/dist/src/commands/resolve.js +21 -13
  171. package/dist/src/commands/run.d.ts +0 -13
  172. package/dist/src/commands/run.js +38 -51
  173. package/dist/src/commands/setup-support.d.ts +16 -4
  174. package/dist/src/commands/setup-support.js +20 -20
  175. package/dist/src/commands/setup.js +4 -10
  176. package/dist/src/commands/source.js +9 -2
  177. package/dist/src/commands/status-check.d.ts +3 -3
  178. package/dist/src/commands/status-check.js +6 -6
  179. package/dist/src/commands/status-json.d.ts +14 -13
  180. package/dist/src/commands/status-json.js +9 -8
  181. package/dist/src/commands/status-output.d.ts +3 -3
  182. package/dist/src/commands/status-output.js +5 -5
  183. package/dist/src/commands/status-ownership.d.ts +3 -3
  184. package/dist/src/commands/status-ownership.js +3 -3
  185. package/dist/src/commands/status.js +25 -26
  186. package/dist/src/commands/test-build-phase.d.ts +3 -3
  187. package/dist/src/commands/test-build-phase.js +48 -24
  188. package/dist/src/commands/test-diagnose.d.ts +1 -1
  189. package/dist/src/commands/test-diagnose.js +20 -20
  190. package/dist/src/commands/test-modes.d.ts +4 -4
  191. package/dist/src/commands/test-modes.js +4 -5
  192. package/dist/src/commands/test-register.js +5 -3
  193. package/dist/src/commands/test-run.d.ts +11 -11
  194. package/dist/src/commands/test-run.js +40 -33
  195. package/dist/src/commands/test-stale-gate.d.ts +12 -12
  196. package/dist/src/commands/test-stale-gate.js +17 -17
  197. package/dist/src/commands/test-verdict.d.ts +19 -5
  198. package/dist/src/commands/test-verdict.js +26 -11
  199. package/dist/src/commands/test.d.ts +3 -3
  200. package/dist/src/commands/test.js +73 -36
  201. package/dist/src/commands/token-coverage.js +3 -3
  202. package/dist/src/commands/token-list.d.ts +26 -0
  203. package/dist/src/commands/token-list.js +153 -0
  204. package/dist/src/commands/token.js +26 -10
  205. package/dist/src/commands/tree.js +26 -21
  206. package/dist/src/commands/typecheck.d.ts +6 -6
  207. package/dist/src/commands/typecheck.js +16 -16
  208. package/dist/src/commands/verify.d.ts +3 -3
  209. package/dist/src/commands/verify.js +10 -10
  210. package/dist/src/commands/watch.js +11 -11
  211. package/dist/src/commands/wire.js +17 -16
  212. package/dist/src/core/ast-utils.d.ts +7 -7
  213. package/dist/src/core/ast-utils.js +8 -8
  214. package/dist/src/core/branding.d.ts +5 -12
  215. package/dist/src/core/branding.js +6 -5
  216. package/dist/src/core/browser-wire.d.ts +1 -1
  217. package/dist/src/core/browser-wire.js +10 -7
  218. package/dist/src/core/build-audit-platform.d.ts +32 -3
  219. package/dist/src/core/build-audit-platform.js +153 -13
  220. package/dist/src/core/build-audit-registration.d.ts +5 -5
  221. package/dist/src/core/build-audit-registration.js +10 -9
  222. package/dist/src/core/build-audit-resolve.d.ts +5 -5
  223. package/dist/src/core/build-audit-resolve.js +6 -6
  224. package/dist/src/core/build-audit-skip.d.ts +57 -0
  225. package/dist/src/core/build-audit-skip.js +112 -0
  226. package/dist/src/core/build-audit-transforms.d.ts +2 -2
  227. package/dist/src/core/build-audit-transforms.js +3 -3
  228. package/dist/src/core/build-audit.d.ts +21 -8
  229. package/dist/src/core/build-audit.js +142 -40
  230. package/dist/src/core/build-baseline-types.d.ts +19 -18
  231. package/dist/src/core/build-baseline-types.js +1 -1
  232. package/dist/src/core/build-baseline.d.ts +48 -31
  233. package/dist/src/core/build-baseline.js +20 -43
  234. package/dist/src/core/build-jar-escalation.d.ts +8 -8
  235. package/dist/src/core/build-jar-escalation.js +18 -17
  236. package/dist/src/core/build-overwrite-guard.d.ts +4 -4
  237. package/dist/src/core/build-overwrite-guard.js +14 -14
  238. package/dist/src/core/build-prepare.d.ts +4 -12
  239. package/dist/src/core/build-prepare.js +22 -26
  240. package/dist/src/core/config-mutate.js +4 -4
  241. package/dist/src/core/config-paths.d.ts +2 -6
  242. package/dist/src/core/config-paths.js +4 -2
  243. package/dist/src/core/config-state.d.ts +0 -6
  244. package/dist/src/core/config-state.js +3 -21
  245. package/dist/src/core/config-validate-build-audit.d.ts +8 -0
  246. package/dist/src/core/config-validate-build-audit.js +80 -0
  247. package/dist/src/core/config-validate-file-size.d.ts +1 -1
  248. package/dist/src/core/config-validate-file-size.js +3 -3
  249. package/dist/src/core/config-validate-patch-policy.d.ts +2 -2
  250. package/dist/src/core/config-validate-patch-policy.js +3 -3
  251. package/dist/src/core/config-validate-test-toolchains.d.ts +2 -2
  252. package/dist/src/core/config-validate-test-toolchains.js +4 -5
  253. package/dist/src/core/config-validate.js +22 -12
  254. package/dist/src/core/config.d.ts +12 -12
  255. package/dist/src/core/config.js +21 -25
  256. package/dist/src/core/coverage-extend.d.ts +16 -16
  257. package/dist/src/core/coverage-extend.js +20 -20
  258. package/dist/src/core/destructive.d.ts +14 -14
  259. package/dist/src/core/destructive.js +9 -9
  260. package/dist/src/core/diff-hunks.d.ts +2 -2
  261. package/dist/src/core/diff-hunks.js +7 -7
  262. package/dist/src/core/discard-baseline.d.ts +10 -10
  263. package/dist/src/core/discard-baseline.js +22 -23
  264. package/dist/src/core/display-state.d.ts +11 -11
  265. package/dist/src/core/display-state.js +12 -12
  266. package/dist/src/core/engine-changes.d.ts +2 -2
  267. package/dist/src/core/engine-changes.js +2 -2
  268. package/dist/src/core/engine-precondition.d.ts +5 -13
  269. package/dist/src/core/engine-precondition.js +5 -10
  270. package/dist/src/core/engine-session-lock.d.ts +18 -18
  271. package/dist/src/core/engine-session-lock.js +50 -54
  272. package/dist/src/core/export-moved-code-shape.d.ts +7 -7
  273. package/dist/src/core/export-moved-code-shape.js +10 -10
  274. package/dist/src/core/file-lock-owner.d.ts +103 -0
  275. package/dist/src/core/file-lock-owner.js +259 -0
  276. package/dist/src/core/file-lock.d.ts +14 -19
  277. package/dist/src/core/file-lock.js +90 -128
  278. package/dist/src/core/firefox-cache.d.ts +13 -1
  279. package/dist/src/core/firefox-cache.js +61 -36
  280. package/dist/src/core/firefox-download.d.ts +15 -0
  281. package/dist/src/core/firefox-download.js +38 -22
  282. package/dist/src/core/firefox-extract.d.ts +8 -7
  283. package/dist/src/core/firefox-extract.js +45 -27
  284. package/dist/src/core/firefox.d.ts +34 -36
  285. package/dist/src/core/firefox.js +13 -44
  286. package/dist/src/core/furnace-apply-dry-run.d.ts +1 -6
  287. package/dist/src/core/furnace-apply-dry-run.js +2 -8
  288. package/dist/src/core/furnace-apply-ftl.d.ts +5 -5
  289. package/dist/src/core/furnace-apply-ftl.js +8 -8
  290. package/dist/src/core/furnace-apply-helpers.d.ts +9 -20
  291. package/dist/src/core/furnace-apply-helpers.js +28 -39
  292. package/dist/src/core/furnace-apply-output.js +1 -1
  293. package/dist/src/core/furnace-apply-overwrite-warn.d.ts +9 -9
  294. package/dist/src/core/furnace-apply-overwrite-warn.js +13 -11
  295. package/dist/src/core/furnace-apply-undeploy-actions.d.ts +1 -1
  296. package/dist/src/core/furnace-apply-undeploy-actions.js +1 -1
  297. package/dist/src/core/furnace-apply.d.ts +4 -4
  298. package/dist/src/core/furnace-apply.js +50 -29
  299. package/dist/src/core/furnace-checksum-utils.d.ts +5 -5
  300. package/dist/src/core/furnace-checksum-utils.js +5 -5
  301. package/dist/src/core/furnace-config-custom.d.ts +1 -1
  302. package/dist/src/core/furnace-config-custom.js +2 -2
  303. package/dist/src/core/furnace-config-order.js +1 -1
  304. package/dist/src/core/furnace-config-tokens.d.ts +3 -3
  305. package/dist/src/core/furnace-config-tokens.js +3 -3
  306. package/dist/src/core/furnace-config-validate.js +2 -2
  307. package/dist/src/core/furnace-config.d.ts +5 -5
  308. package/dist/src/core/furnace-config.js +40 -53
  309. package/dist/src/core/furnace-constants.d.ts +6 -6
  310. package/dist/src/core/furnace-constants.js +9 -8
  311. package/dist/src/core/furnace-css-fragments.d.ts +6 -6
  312. package/dist/src/core/furnace-css-fragments.js +11 -11
  313. package/dist/src/core/furnace-dir-entry.d.ts +20 -0
  314. package/dist/src/core/furnace-dir-entry.js +21 -0
  315. package/dist/src/core/furnace-jsconfig.d.ts +7 -7
  316. package/dist/src/core/furnace-jsconfig.js +22 -20
  317. package/dist/src/core/furnace-operation.d.ts +22 -19
  318. package/dist/src/core/furnace-operation.js +38 -34
  319. package/dist/src/core/furnace-precondition.d.ts +10 -10
  320. package/dist/src/core/furnace-precondition.js +8 -8
  321. package/dist/src/core/furnace-refresh.d.ts +18 -6
  322. package/dist/src/core/furnace-refresh.js +5 -8
  323. package/dist/src/core/furnace-registration-ast.d.ts +2 -3
  324. package/dist/src/core/furnace-registration-ast.js +9 -12
  325. package/dist/src/core/furnace-registration-remove.d.ts +4 -4
  326. package/dist/src/core/furnace-registration-remove.js +6 -6
  327. package/dist/src/core/furnace-registration-validate.d.ts +2 -2
  328. package/dist/src/core/furnace-registration-validate.js +2 -2
  329. package/dist/src/core/furnace-registration.d.ts +7 -6
  330. package/dist/src/core/furnace-registration.js +29 -36
  331. package/dist/src/core/furnace-rollback.d.ts +1 -1
  332. package/dist/src/core/furnace-rollback.js +5 -5
  333. package/dist/src/core/furnace-scanner.js +3 -6
  334. package/dist/src/core/furnace-stale-export.d.ts +17 -16
  335. package/dist/src/core/furnace-stale-export.js +16 -15
  336. package/dist/src/core/furnace-staleness.d.ts +2 -2
  337. package/dist/src/core/furnace-staleness.js +2 -2
  338. package/dist/src/core/furnace-state-persist.d.ts +12 -12
  339. package/dist/src/core/furnace-state-persist.js +10 -10
  340. package/dist/src/core/furnace-step-errors.d.ts +6 -6
  341. package/dist/src/core/furnace-step-errors.js +6 -6
  342. package/dist/src/core/furnace-validate-accessibility.js +14 -14
  343. package/dist/src/core/furnace-validate-compatibility.js +9 -9
  344. package/dist/src/core/furnace-validate-registration.d.ts +2 -2
  345. package/dist/src/core/furnace-validate-registration.js +18 -20
  346. package/dist/src/core/furnace-validate-structure.d.ts +1 -1
  347. package/dist/src/core/furnace-validate-structure.js +4 -4
  348. package/dist/src/core/furnace-validate.js +9 -9
  349. package/dist/src/core/furnace-version-drift.d.ts +5 -5
  350. package/dist/src/core/furnace-version-drift.js +5 -5
  351. package/dist/src/core/git-base.d.ts +6 -4
  352. package/dist/src/core/git-base.js +29 -7
  353. package/dist/src/core/git-diff-new-file.d.ts +40 -0
  354. package/dist/src/core/git-diff-new-file.js +119 -0
  355. package/dist/src/core/git-diff.d.ts +13 -13
  356. package/dist/src/core/git-diff.js +121 -189
  357. package/dist/src/core/git-file-ops.d.ts +6 -13
  358. package/dist/src/core/git-file-ops.js +11 -35
  359. package/dist/src/core/git-readonly-index.d.ts +12 -14
  360. package/dist/src/core/git-readonly-index.js +12 -16
  361. package/dist/src/core/git-status.d.ts +9 -2
  362. package/dist/src/core/git-status.js +23 -11
  363. package/dist/src/core/git.d.ts +3 -9
  364. package/dist/src/core/git.js +28 -57
  365. package/dist/src/core/harness-orphans.d.ts +17 -17
  366. package/dist/src/core/harness-orphans.js +28 -27
  367. package/dist/src/core/history-log.d.ts +19 -0
  368. package/dist/src/core/history-log.js +29 -0
  369. package/dist/src/core/license-headers.d.ts +19 -18
  370. package/dist/src/core/license-headers.js +29 -28
  371. package/dist/src/core/mach-build-artifacts.d.ts +21 -21
  372. package/dist/src/core/mach-build-artifacts.js +23 -23
  373. package/dist/src/core/mach-error-hints.d.ts +5 -10
  374. package/dist/src/core/mach-error-hints.js +14 -14
  375. package/dist/src/core/mach-known-noise-filter.d.ts +42 -38
  376. package/dist/src/core/mach-known-noise-filter.js +57 -56
  377. package/dist/src/core/mach-mozconfig.d.ts +6 -6
  378. package/dist/src/core/mach-mozconfig.js +17 -16
  379. package/dist/src/core/mach-python.d.ts +3 -0
  380. package/dist/src/core/mach-python.js +13 -7
  381. package/dist/src/core/mach-resource-shim.d.ts +24 -12
  382. package/dist/src/core/mach-resource-shim.js +124 -21
  383. package/dist/src/core/mach.d.ts +59 -73
  384. package/dist/src/core/mach.js +47 -115
  385. package/dist/src/core/marionette-port.d.ts +15 -30
  386. package/dist/src/core/marionette-port.js +46 -89
  387. package/dist/src/core/marionette-preflight.d.ts +6 -6
  388. package/dist/src/core/marionette-preflight.js +32 -50
  389. package/dist/src/core/markdown-table.d.ts +9 -9
  390. package/dist/src/core/markdown-table.js +14 -14
  391. package/dist/src/core/mochitest-server-port.d.ts +8 -8
  392. package/dist/src/core/mochitest-server-port.js +17 -17
  393. package/dist/src/core/module-resolution-preflight.d.ts +8 -8
  394. package/dist/src/core/module-resolution-preflight.js +11 -11
  395. package/dist/src/core/moz-manifest-helpers.d.ts +7 -7
  396. package/dist/src/core/moz-manifest-helpers.js +7 -7
  397. package/dist/src/core/moz-manifest-register.d.ts +1 -1
  398. package/dist/src/core/moz-manifest-register.js +2 -2
  399. package/dist/src/core/moz-manifest-rules.js +46 -42
  400. package/dist/src/core/moz-manifest-tokenizers.js +1 -1
  401. package/dist/src/core/ownership-table.d.ts +5 -5
  402. package/dist/src/core/ownership-table.js +7 -7
  403. package/dist/src/core/parser-fallback.d.ts +3 -3
  404. package/dist/src/core/parser-fallback.js +3 -3
  405. package/dist/src/core/patch-apply-fuzz.d.ts +5 -5
  406. package/dist/src/core/patch-apply-fuzz.js +13 -13
  407. package/dist/src/core/patch-apply.d.ts +13 -14
  408. package/dist/src/core/patch-apply.js +36 -42
  409. package/dist/src/core/patch-export-coverage.d.ts +0 -9
  410. package/dist/src/core/patch-export-coverage.js +1 -30
  411. package/dist/src/core/patch-export-update.d.ts +21 -4
  412. package/dist/src/core/patch-export-update.js +7 -6
  413. package/dist/src/core/patch-export.d.ts +20 -30
  414. package/dist/src/core/patch-export.js +55 -77
  415. package/dist/src/core/patch-files.d.ts +0 -4
  416. package/dist/src/core/patch-files.js +0 -11
  417. package/dist/src/core/patch-identifier-suggest.d.ts +1 -1
  418. package/dist/src/core/patch-identifier-suggest.js +4 -4
  419. package/dist/src/core/patch-lint-binary.d.ts +12 -18
  420. package/dist/src/core/patch-lint-cache.d.ts +18 -2
  421. package/dist/src/core/patch-lint-cache.js +16 -19
  422. package/dist/src/core/patch-lint-checkjs.d.ts +55 -38
  423. package/dist/src/core/patch-lint-checkjs.js +78 -64
  424. package/dist/src/core/patch-lint-chrome-jsdoc.d.ts +8 -9
  425. package/dist/src/core/patch-lint-chrome-jsdoc.js +10 -11
  426. package/dist/src/core/patch-lint-creators.d.ts +5 -9
  427. package/dist/src/core/patch-lint-creators.js +3 -3
  428. package/dist/src/core/patch-lint-cross.d.ts +27 -19
  429. package/dist/src/core/patch-lint-cross.js +67 -110
  430. package/dist/src/core/patch-lint-css.d.ts +1 -1
  431. package/dist/src/core/patch-lint-css.js +15 -11
  432. package/dist/src/core/patch-lint-diff-tag.d.ts +5 -5
  433. package/dist/src/core/patch-lint-diff-tag.js +5 -5
  434. package/dist/src/core/patch-lint-diff.d.ts +4 -4
  435. package/dist/src/core/patch-lint-diff.js +4 -4
  436. package/dist/src/core/patch-lint-file-size.d.ts +3 -3
  437. package/dist/src/core/patch-lint-file-size.js +6 -6
  438. package/dist/src/core/patch-lint-forward-registration.d.ts +58 -0
  439. package/dist/src/core/patch-lint-forward-registration.js +210 -0
  440. package/dist/src/core/patch-lint-jsdoc.d.ts +2 -2
  441. package/dist/src/core/patch-lint-jsdoc.js +46 -46
  442. package/dist/src/core/patch-lint-module-registration.d.ts +3 -9
  443. package/dist/src/core/patch-lint-module-registration.js +1 -1
  444. package/dist/src/core/patch-lint-mozbuild.d.ts +1 -1
  445. package/dist/src/core/patch-lint-mozbuild.js +10 -10
  446. package/dist/src/core/patch-lint-observer.d.ts +3 -3
  447. package/dist/src/core/patch-lint-observer.js +7 -7
  448. package/dist/src/core/patch-lint-ownership.d.ts +6 -6
  449. package/dist/src/core/patch-lint-ownership.js +6 -6
  450. package/dist/src/core/patch-lint-prettier.d.ts +6 -6
  451. package/dist/src/core/patch-lint-prettier.js +7 -7
  452. package/dist/src/core/patch-lint-queue-types.d.ts +60 -0
  453. package/dist/src/core/patch-lint-queue-types.js +12 -0
  454. package/dist/src/core/patch-lint-staged-registration.d.ts +61 -0
  455. package/dist/src/core/patch-lint-staged-registration.js +90 -0
  456. package/dist/src/core/patch-lint-unmanaged-companion.d.ts +23 -23
  457. package/dist/src/core/patch-lint-unmanaged-companion.js +26 -26
  458. package/dist/src/core/patch-lint.d.ts +41 -35
  459. package/dist/src/core/patch-lint.js +52 -49
  460. package/dist/src/core/patch-lock.d.ts +1 -1
  461. package/dist/src/core/patch-lock.js +11 -11
  462. package/dist/src/core/patch-manifest-consistency.d.ts +7 -7
  463. package/dist/src/core/patch-manifest-consistency.js +16 -17
  464. package/dist/src/core/patch-manifest-files-affected.d.ts +1 -1
  465. package/dist/src/core/patch-manifest-files-affected.js +3 -3
  466. package/dist/src/core/patch-manifest-io.d.ts +14 -14
  467. package/dist/src/core/patch-manifest-io.js +35 -34
  468. package/dist/src/core/patch-manifest-query.d.ts +1 -1
  469. package/dist/src/core/patch-manifest-query.js +4 -4
  470. package/dist/src/core/patch-manifest-resolve.d.ts +5 -5
  471. package/dist/src/core/patch-manifest-resolve.js +7 -7
  472. package/dist/src/core/patch-manifest-validate.js +3 -3
  473. package/dist/src/core/patch-manifest.d.ts +2 -2
  474. package/dist/src/core/patch-manifest.js +2 -2
  475. package/dist/src/core/patch-parse.d.ts +32 -21
  476. package/dist/src/core/patch-parse.js +40 -15
  477. package/dist/src/core/patch-policy.js +15 -27
  478. package/dist/src/core/patch-registration-refs.d.ts +4 -4
  479. package/dist/src/core/patch-registration-refs.js +7 -7
  480. package/dist/src/core/patch-source-metadata.d.ts +1 -3
  481. package/dist/src/core/patch-source-metadata.js +0 -4
  482. package/dist/src/core/patch-transform.d.ts +8 -16
  483. package/dist/src/core/patch-transform.js +13 -23
  484. package/dist/src/core/process-cpu-time.d.ts +1 -1
  485. package/dist/src/core/process-cpu-time.js +6 -6
  486. package/dist/src/core/rebase-session.d.ts +9 -15
  487. package/dist/src/core/rebase-session.js +13 -25
  488. package/dist/src/core/register-browser-content.js +3 -2
  489. package/dist/src/core/register-browser-test.js +2 -1
  490. package/dist/src/core/register-module.js +2 -1
  491. package/dist/src/core/register-result.d.ts +1 -1
  492. package/dist/src/core/register-result.js +1 -1
  493. package/dist/src/core/register-scaffold.d.ts +2 -2
  494. package/dist/src/core/register-scaffold.js +8 -7
  495. package/dist/src/core/register-shared-css.d.ts +3 -3
  496. package/dist/src/core/register-shared-css.js +6 -5
  497. package/dist/src/core/register-xpcshell-test.d.ts +2 -2
  498. package/dist/src/core/register-xpcshell-test.js +2 -2
  499. package/dist/src/core/run-log-redact.d.ts +38 -0
  500. package/dist/src/core/run-log-redact.js +69 -0
  501. package/dist/src/core/run-log.d.ts +21 -10
  502. package/dist/src/core/run-log.js +76 -27
  503. package/dist/src/core/shared-ftl.d.ts +2 -2
  504. package/dist/src/core/shared-ftl.js +6 -6
  505. package/dist/src/core/signal-critical.d.ts +2 -2
  506. package/dist/src/core/signal-critical.js +4 -6
  507. package/dist/src/core/smoke-patterns.d.ts +5 -6
  508. package/dist/src/core/smoke-patterns.js +15 -20
  509. package/dist/src/core/state-file.d.ts +15 -4
  510. package/dist/src/core/state-file.js +14 -1
  511. package/dist/src/core/status-binary.d.ts +8 -8
  512. package/dist/src/core/status-binary.js +12 -12
  513. package/dist/src/core/status-classify.d.ts +17 -17
  514. package/dist/src/core/status-classify.js +13 -12
  515. package/dist/src/core/test-changed-prefs.d.ts +12 -12
  516. package/dist/src/core/test-changed-prefs.js +40 -25
  517. package/dist/src/core/test-harness-crash.d.ts +39 -39
  518. package/dist/src/core/test-harness-crash.js +69 -69
  519. package/dist/src/core/test-harness-output.d.ts +7 -7
  520. package/dist/src/core/test-harness-output.js +7 -7
  521. package/dist/src/core/test-harness-verdict-notes.d.ts +13 -14
  522. package/dist/src/core/test-harness-verdict-notes.js +4 -4
  523. package/dist/src/core/test-path-scope.d.ts +8 -8
  524. package/dist/src/core/test-path-scope.js +11 -11
  525. package/dist/src/core/test-stale-check.d.ts +22 -23
  526. package/dist/src/core/test-stale-check.js +31 -30
  527. package/dist/src/core/test-stale-symlink.d.ts +1 -1
  528. package/dist/src/core/test-stale-symlink.js +1 -1
  529. package/dist/src/core/test-stall-triage.d.ts +9 -9
  530. package/dist/src/core/test-stall-triage.js +22 -22
  531. package/dist/src/core/test-xpcshell-retry.d.ts +26 -7
  532. package/dist/src/core/test-xpcshell-retry.js +14 -11
  533. package/dist/src/core/token-category.d.ts +35 -2
  534. package/dist/src/core/token-category.js +67 -58
  535. package/dist/src/core/token-coverage.js +4 -5
  536. package/dist/src/core/token-dark-mode.d.ts +9 -9
  537. package/dist/src/core/token-dark-mode.js +13 -25
  538. package/dist/src/core/token-docs.d.ts +1 -1
  539. package/dist/src/core/token-inventory.d.ts +56 -0
  540. package/dist/src/core/token-inventory.js +109 -0
  541. package/dist/src/core/token-manager.d.ts +4 -4
  542. package/dist/src/core/token-manager.js +22 -22
  543. package/dist/src/core/token-scaffold.d.ts +4 -4
  544. package/dist/src/core/token-scaffold.js +4 -4
  545. package/dist/src/core/token-variant.d.ts +14 -14
  546. package/dist/src/core/token-variant.js +52 -53
  547. package/dist/src/core/toolchain-preflight.d.ts +15 -15
  548. package/dist/src/core/toolchain-preflight.js +23 -23
  549. package/dist/src/core/tree-cow.d.ts +3 -5
  550. package/dist/src/core/tree-cow.js +8 -8
  551. package/dist/src/core/tree-guard.d.ts +3 -3
  552. package/dist/src/core/tree-guard.js +23 -23
  553. package/dist/src/core/tree-store.d.ts +15 -15
  554. package/dist/src/core/tree-store.js +39 -38
  555. package/dist/src/core/typecheck-shim.d.ts +16 -15
  556. package/dist/src/core/typecheck-shim.js +26 -25
  557. package/dist/src/core/typecheck.d.ts +12 -12
  558. package/dist/src/core/typecheck.js +33 -33
  559. package/dist/src/core/wire-destroy.d.ts +2 -2
  560. package/dist/src/core/wire-destroy.js +5 -5
  561. package/dist/src/core/wire-dom-fragment.d.ts +3 -3
  562. package/dist/src/core/wire-dom-fragment.js +12 -23
  563. package/dist/src/core/wire-init.d.ts +2 -2
  564. package/dist/src/core/wire-init.js +7 -7
  565. package/dist/src/core/wire-subscript.d.ts +1 -1
  566. package/dist/src/core/wire-subscript.js +3 -3
  567. package/dist/src/core/wire-utils.d.ts +9 -9
  568. package/dist/src/core/wire-utils.js +16 -28
  569. package/dist/src/core/xpcshell-appdir.d.ts +20 -20
  570. package/dist/src/core/xpcshell-appdir.js +28 -30
  571. package/dist/src/core/xpcshell-profile-dir.d.ts +3 -3
  572. package/dist/src/core/xpcshell-profile-dir.js +7 -7
  573. package/dist/src/errors/base.d.ts +69 -16
  574. package/dist/src/errors/base.js +95 -26
  575. package/dist/src/errors/build.d.ts +6 -6
  576. package/dist/src/errors/build.js +26 -34
  577. package/dist/src/errors/codes.d.ts +10 -10
  578. package/dist/src/errors/codes.js +10 -10
  579. package/dist/src/errors/config.d.ts +0 -14
  580. package/dist/src/errors/config.js +13 -34
  581. package/dist/src/errors/download.d.ts +13 -0
  582. package/dist/src/errors/download.js +53 -24
  583. package/dist/src/errors/furnace.d.ts +2 -2
  584. package/dist/src/errors/furnace.js +8 -9
  585. package/dist/src/errors/git.d.ts +9 -9
  586. package/dist/src/errors/git.js +21 -31
  587. package/dist/src/errors/patch.d.ts +3 -3
  588. package/dist/src/errors/patch.js +11 -13
  589. package/dist/src/errors/rebase.d.ts +1 -1
  590. package/dist/src/errors/rebase.js +8 -8
  591. package/dist/src/errors/run.d.ts +1 -1
  592. package/dist/src/errors/run.js +1 -1
  593. package/dist/src/index.d.ts +4 -4
  594. package/dist/src/index.js +4 -4
  595. package/dist/src/types/commands/options.d.ts +155 -200
  596. package/dist/src/types/commands/patches.d.ts +19 -19
  597. package/dist/src/types/commands/project.d.ts +3 -3
  598. package/dist/src/types/config.d.ts +52 -19
  599. package/dist/src/types/furnace.d.ts +17 -17
  600. package/dist/src/types/index.d.ts +0 -1
  601. package/dist/src/types/json.d.ts +3 -3
  602. package/dist/src/types/json.js +2 -2
  603. package/dist/src/types/typecheck.d.ts +3 -3
  604. package/dist/src/utils/assert.d.ts +4 -16
  605. package/dist/src/utils/assert.js +7 -24
  606. package/dist/src/utils/build-info.d.ts +6 -6
  607. package/dist/src/utils/build-info.js +6 -6
  608. package/dist/src/utils/child-env.d.ts +7 -6
  609. package/dist/src/utils/child-env.js +5 -5
  610. package/dist/src/utils/concurrency.d.ts +2 -2
  611. package/dist/src/utils/concurrency.js +2 -2
  612. package/dist/src/utils/elapsed.js +10 -1
  613. package/dist/src/utils/errors.d.ts +8 -8
  614. package/dist/src/utils/errors.js +8 -8
  615. package/dist/src/utils/fs.d.ts +1 -1
  616. package/dist/src/utils/fs.js +15 -20
  617. package/dist/src/utils/hash.d.ts +12 -0
  618. package/dist/src/utils/hash.js +16 -0
  619. package/dist/src/utils/line-dispatch.d.ts +15 -0
  620. package/dist/src/utils/line-dispatch.js +48 -0
  621. package/dist/src/utils/logger.d.ts +4 -4
  622. package/dist/src/utils/logger.js +11 -11
  623. package/dist/src/utils/machine-output.js +2 -2
  624. package/dist/src/utils/options.d.ts +14 -20
  625. package/dist/src/utils/options.js +26 -27
  626. package/dist/src/utils/package-root.js +5 -14
  627. package/dist/src/utils/paths.d.ts +4 -4
  628. package/dist/src/utils/paths.js +4 -4
  629. package/dist/src/utils/platform.d.ts +0 -4
  630. package/dist/src/utils/process-group.d.ts +10 -9
  631. package/dist/src/utils/process-group.js +37 -36
  632. package/dist/src/utils/process.d.ts +70 -55
  633. package/dist/src/utils/process.js +129 -179
  634. package/dist/src/utils/ps-duration.d.ts +4 -4
  635. package/dist/src/utils/ps-duration.js +9 -9
  636. package/dist/src/utils/regex.d.ts +4 -4
  637. package/dist/src/utils/regex.js +7 -7
  638. package/dist/src/utils/sleep.d.ts +26 -0
  639. package/dist/src/utils/sleep.js +31 -0
  640. package/dist/src/utils/stdio-drain.d.ts +7 -7
  641. package/dist/src/utils/stdio-drain.js +8 -8
  642. package/dist/src/utils/validation.d.ts +12 -49
  643. package/dist/src/utils/validation.js +26 -36
  644. package/docs/README.md +2 -2
  645. package/docs/configuration.md +163 -69
  646. package/docs/exit-codes.md +66 -49
  647. package/docs/furnace.md +20 -19
  648. package/docs/lifecycle-invariants.md +414 -247
  649. package/docs/machine-output.md +58 -34
  650. package/docs/patch-workflow.md +48 -44
  651. package/docs/run-logs.md +72 -28
  652. package/docs/testing.md +105 -98
  653. package/docs/verification-trees.md +23 -22
  654. package/package.json +7 -6
  655. package/dist/src/commands/rebase.d.ts +0 -4
  656. package/dist/src/commands/rebase.js +0 -5
  657. package/dist/src/core/patch-artifact-normalize.d.ts +0 -21
  658. package/dist/src/core/patch-artifact-normalize.js +0 -24
  659. package/dist/src/core/patch-lint-reexports.d.ts +0 -7
  660. package/dist/src/core/patch-lint-reexports.js +0 -8
  661. package/dist/src/core/wire-targets.d.ts +0 -7
  662. package/dist/src/core/wire-targets.js +0 -8
@@ -1,18 +1,21 @@
1
1
  // SPDX-License-Identifier: EUPL-1.2
2
2
  import { randomUUID } from 'node:crypto';
3
- import { mkdir, readdir, readFile, rename, rm, stat, writeFile } from 'node:fs/promises';
3
+ import { mkdir, readdir, rename, rm, stat, writeFile } from 'node:fs/promises';
4
4
  import { dirname, join } from 'node:path';
5
5
  import { LockContentionError } from '../errors/base.js';
6
6
  import { getNodeErrorCode, isProcessAlive, toError } from '../utils/errors.js';
7
7
  import { ensureDir } from '../utils/fs.js';
8
8
  import { verbose, warn } from '../utils/logger.js';
9
+ import { sleep } from '../utils/sleep.js';
10
+ import { isLockOwnerAlive, LOCK_PID_FILE, readLockOwner, writeLockOwner, } from './file-lock-owner.js';
11
+ export { LOCK_PID_FILE };
9
12
  const DEFAULT_LOCK_TIMEOUT_MS = 30_000;
10
13
  const DEFAULT_LOCK_POLL_MS = 50;
11
14
  const DEFAULT_STALE_LOCK_MS = 5 * 60_000;
12
15
  /**
13
16
  * Minimum interval between stale-lock probes while a waiter polls. Probing
14
- * exactly once per waiter means a holder that dies *after* that probe leaves
15
- * the waiter polling a permanently dead lock until `timeoutMs` — up to 24
17
+ * exactly once per waiter means a holder that dies after that probe leaves
18
+ * the waiter polling a permanently dead lock until `timeoutMs`, up to 24
16
19
  * hours for the build lock (see `withBuildLock` in mach.ts). Re-probing on a
17
20
  * small interval bounds that to seconds while keeping the per-poll cost
18
21
  * negligible: one stat plus one small read every ~5 s.
@@ -23,52 +26,10 @@ const STALE_REPROBE_INTERVAL_MS = 5_000;
23
26
  * deadline extension but no progress line of its own.
24
27
  */
25
28
  const WAIT_PROBE_INTERVAL_MS = 5_000;
26
- function sleep(ms) {
27
- return new Promise((resolve) => {
28
- setTimeout(resolve, ms);
29
- });
30
- }
31
29
  /** Derives the sibling lock-directory path used to guard a file-based resource. */
32
30
  export function createSiblingLockPath(filePath, suffix = '.fireforge.lock') {
33
31
  return `${filePath}${suffix}`;
34
32
  }
35
- /**
36
- * Filename of a lock directory's owner record, relative to the lock dir.
37
- *
38
- * Exported so external readers (`tree-store.ts`) share the constant instead of
39
- * re-spelling `'pid'`; the file *format* is documented on {@link readLockOwner}.
40
- */
41
- export const LOCK_PID_FILE = 'pid';
42
- /**
43
- * Reads the owner PID (line 1), acquisition token (line 2), and diagnostic
44
- * metadata (lines 3+, see {@link FileLockOptions.ownerMetadata}) from a lock
45
- * directory's PID file. Returns `{ present: false }` when the PID file is
46
- * missing or the PID does not parse as a finite integer (caller falls back
47
- * to the age-only staleness heuristic).
48
- *
49
- * File format note: external readers do `parseInt(content.trim(), 10)`,
50
- * which parses the leading digits of a multi-line file — so the token line
51
- * stays compatible.
52
- */
53
- async function readLockOwner(lockPath) {
54
- try {
55
- const pidContent = await readFile(join(lockPath, LOCK_PID_FILE), 'utf-8');
56
- const [pidLine, tokenLine, ...metadataLines] = pidContent.split('\n');
57
- const pid = parseInt((pidLine ?? '').trim(), 10);
58
- if (Number.isFinite(pid)) {
59
- const metadata = metadataLines.map((line) => line.trim()).filter((line) => line.length > 0);
60
- const token = tokenLine?.trim();
61
- if (token !== undefined && token.length > 0) {
62
- return { present: true, pid, token, metadata };
63
- }
64
- return { present: true, pid, metadata };
65
- }
66
- }
67
- catch {
68
- // PID file missing or unreadable — treat as absent.
69
- }
70
- return { present: false };
71
- }
72
33
  /** Sibling directory in which waiters advertise themselves. */
73
34
  function lockWaitersDir(lockPath) {
74
35
  return `${lockPath}.waiters`;
@@ -127,7 +88,7 @@ export async function readLockStatus(lockPath) {
127
88
  }
128
89
  const owner = await readLockOwner(lockPath);
129
90
  const holder = owner.present
130
- ? { pid: owner.pid, alive: isProcessAlive(owner.pid), metadata: owner.metadata }
91
+ ? { pid: owner.pid, alive: isLockOwnerAlive(owner), metadata: owner.metadata }
131
92
  : undefined;
132
93
  return { held: true, holder, heldForMs, queueDepth: queue.depth };
133
94
  }
@@ -138,13 +99,13 @@ export async function readLockStatus(lockPath) {
138
99
  */
139
100
  async function registerWaiter(lockPath, startedAt) {
140
101
  const dir = lockWaitersDir(lockPath);
141
- const file = join(dir, `${String(startedAt)}-${String(process.pid)}-${randomUUID()}`);
102
+ const file = join(dir, `${startedAt}-${process.pid}-${randomUUID()}`);
142
103
  try {
143
104
  await ensureDir(dir);
144
- await writeFile(file, `${String(process.pid)}\n`, 'utf-8');
105
+ await writeFile(file, `${process.pid}\n`, 'utf-8');
145
106
  }
146
107
  catch (error) {
147
- // Advisory only — never fail a lock acquisition over the queue display.
108
+ // Advisory only. Never fail a lock acquisition over the queue display.
148
109
  verbose(`Could not register lock waiter for ${lockPath}: ${toError(error).message}`);
149
110
  return async () => {
150
111
  /* nothing to remove */
@@ -158,13 +119,13 @@ async function registerWaiter(lockPath, startedAt) {
158
119
  * Reads the contended lock's owner and queue in one go. One probe per cycle
159
120
  * serves both the progress line and the deadline extension, so the two can
160
121
  * never disagree about the position they saw. Failure to read the owner
161
- * degrades to `holder: undefined` — a diagnostic must never break the wait
162
- * loop.
122
+ * degrades to `holder: undefined`, since a diagnostic must never break the
123
+ * wait loop.
163
124
  */
164
125
  async function probeWaitState(lockPath, startedAt) {
165
126
  const owner = await readLockOwner(lockPath);
166
127
  const holder = owner.present
167
- ? { pid: owner.pid, alive: isProcessAlive(owner.pid), metadata: owner.metadata }
128
+ ? { pid: owner.pid, alive: isLockOwnerAlive(owner), metadata: owner.metadata }
168
129
  : undefined;
169
130
  return { holder, queue: await readLockQueue(lockPath, startedAt) };
170
131
  }
@@ -174,13 +135,13 @@ async function probeWaitState(lockPath, startedAt) {
174
135
  *
175
136
  * A naive stat → read-PID → `rm` sequence has a TOCTOU hole: two waiters
176
137
  * both observe the dead owner, one removes the stale dir and `mkdir`s its
177
- * own lock, and the other's already-decided `rm` then deletes the *fresh*
178
- * lock — two processes inside the critical section at once.
138
+ * own lock, and the other's already-decided `rm` then deletes the fresh
139
+ * lock, putting two processes inside the critical section at once.
179
140
  *
180
141
  * Rename-aside closes it: `rename()` is atomic, so exactly one reaper wins
181
142
  * ownership of the directory (the loser gets ENOENT and re-polls), and
182
143
  * nothing ever deletes the live path directly. After winning the rename the
183
- * owner is re-read from the renamed dir — if it is now a *live* process (the
144
+ * owner is re-read from the renamed dir. If it is now a live process (the
184
145
  * lock was released and re-acquired between the staleness read and the
185
146
  * rename), it is renamed back instead of reaped.
186
147
  *
@@ -189,13 +150,13 @@ async function probeWaitState(lockPath, startedAt) {
189
150
  * legitimately held and the caller should keep waiting.
190
151
  */
191
152
  async function reapStaleLock(lockPath, ageMs, onStaleLockMessage) {
192
- const reapPath = `${lockPath}.reaping-${String(process.pid)}-${randomUUID()}`;
153
+ const reapPath = `${lockPath}.reaping-${process.pid}-${randomUUID()}`;
193
154
  try {
194
155
  await rename(lockPath, reapPath);
195
156
  }
196
157
  catch (error) {
197
158
  if (getNodeErrorCode(error) === 'ENOENT') {
198
- // Another reaper won, or the owner released — either way, retry mkdir.
159
+ // Another reaper won, or the owner released. Either way, retry mkdir.
199
160
  verbose(`Stale lock disappeared before reaping completed: ${lockPath}`);
200
161
  return true;
201
162
  }
@@ -205,16 +166,16 @@ async function reapStaleLock(lockPath, ageMs, onStaleLockMessage) {
205
166
  // We exclusively own reapPath now. Re-verify: the owner may have changed
206
167
  // between the staleness probe and the rename (release + fresh acquire).
207
168
  const owner = await readLockOwner(reapPath);
208
- if (owner.present && isProcessAlive(owner.pid)) {
169
+ if (owner.present && isLockOwnerAlive(owner)) {
209
170
  try {
210
171
  await rename(reapPath, lockPath);
211
- verbose(`Aborted stale-lock reap of ${lockPath}: owner PID ${String(owner.pid)} is alive (lock was re-acquired mid-probe)`);
172
+ verbose(`Aborted stale-lock reap of ${lockPath}: owner PID ${owner.pid} is alive (lock was re-acquired mid-probe)`);
212
173
  }
213
174
  catch (restoreError) {
214
175
  // Rename-back can only fail if a third process mkdir'd the lock path
215
176
  // in this instant. The displaced live owner keeps running without its
216
- // lock — surface it loudly instead of hiding the lost exclusion.
217
- warn(`Could not restore live lock for PID ${String(owner.pid)} at ${lockPath} ` +
177
+ // lock, so surface it loudly instead of hiding the lost exclusion.
178
+ warn(`Could not restore live lock for PID ${owner.pid} at ${lockPath} ` +
218
179
  `(${toError(restoreError).message}). Displaced lock kept at ${reapPath} for inspection.`);
219
180
  }
220
181
  return false;
@@ -238,21 +199,22 @@ async function removeIfStaleLock(lockPath, staleMs, onStaleLockMessage) {
238
199
  try {
239
200
  const lockStat = await stat(lockPath);
240
201
  const ageMs = Date.now() - lockStat.mtimeMs;
241
- // Check PID FIRST, independent of age. Age-gating everything means a
202
+ // Check the PID first, independent of age. Age-gating everything means a
242
203
  // lock younger than `staleMs` (default 5 minutes) is never removed even
243
- // when its PID file points at a process that has already exited — the
244
- // situation after SIGINT'ing `furnace preview`, where the signal handler
204
+ // when its PID file points at a process that has already exited. That is
205
+ // the situation after SIGINT'ing `furnace preview`, where the signal handler
245
206
  // calls `process.exit` and `withFileLock`'s release never runs. With the
246
207
  // PID-first check an explicitly-dead owner unblocks immediately.
247
208
  const owner = await readLockOwner(lockPath);
248
209
  if (owner.present) {
249
- if (!isProcessAlive(owner.pid)) {
210
+ if (!isLockOwnerAlive(owner)) {
250
211
  return await reapStaleLock(lockPath, ageMs, onStaleLockMessage);
251
212
  }
252
- // PID is alive — respect it regardless of age. A slow `mach build`
253
- // legitimately holds the lock past the stale threshold and we don't
254
- // want to race-remove it.
255
- verbose(`Lock at ${lockPath} is ${Math.round(ageMs / 1000)}s old but PID ${String(owner.pid)} is still running — not removing`);
213
+ // The owner is alive (PID exists and, where `/proc` can tell, it is
214
+ // the process that acquired the lock, not a recycled PID), so respect
215
+ // it regardless of age. A slow `mach build` legitimately holds the
216
+ // lock past the stale threshold and we don't want to race-remove it.
217
+ verbose(`Lock at ${lockPath} is ${Math.round(ageMs / 1000)}s old but PID ${owner.pid} is still running — not removing`);
256
218
  return false;
257
219
  }
258
220
  // No readable PID file. Fall back to the age-only heuristic so locks
@@ -275,25 +237,22 @@ async function removeIfStaleLock(lockPath, staleMs, onStaleLockMessage) {
275
237
  }
276
238
  /**
277
239
  * Releases a lock previously acquired by this process, verifying ownership
278
- * first. An unconditional `finally { rm }` would remove *whichever* lock
279
- * directory is present — if a stale-lock reaper had (in a pathological race)
240
+ * first. An unconditional `finally { rm }` would remove whichever lock
241
+ * directory is present. If a stale-lock reaper had (in a pathological race)
280
242
  * replaced our lock, that deletes the new owner's lock and compounds the
281
243
  * double-acquisition. When the PID file no longer names us, warn and leave
282
244
  * the directory alone.
283
245
  *
284
- * `ownerFileWritten` is false when our own owner-file write failed at
285
- * acquisition (non-fatal there); ownership then cannot be verified and the
286
- * removal is unconditional, which is strictly better than leaking the lock.
246
+ * The owner record is always present here: `writeLockOwner` is fatal, so an
247
+ * acquisition whose record failed to write never reached the operation.
287
248
  */
288
- async function releaseLock(lockPath, ownerToken, ownerFileWritten) {
289
- if (ownerFileWritten) {
290
- const owner = await readLockOwner(lockPath);
291
- if (owner.present &&
292
- (owner.pid !== process.pid || (owner.token !== undefined && owner.token !== ownerToken))) {
293
- warn(`Not removing lock ${lockPath}: it is now owned by PID ${String(owner.pid)} — ` +
294
- 'this process no longer holds it.');
295
- return;
296
- }
249
+ async function releaseLock(lockPath, ownerToken) {
250
+ const owner = await readLockOwner(lockPath);
251
+ if (owner.present &&
252
+ (owner.pid !== process.pid || (owner.token !== undefined && owner.token !== ownerToken))) {
253
+ warn(`Not removing lock ${lockPath}: it is now owned by PID ${owner.pid} — ` +
254
+ 'this process no longer holds it.');
255
+ return;
297
256
  }
298
257
  await rm(lockPath, { recursive: true, force: true });
299
258
  }
@@ -301,11 +260,11 @@ async function releaseLock(lockPath, ownerToken, ownerFileWritten) {
301
260
  * Removes a lock directory only if this process still owns it, for callers
302
261
  * that hold no acquisition token.
303
262
  *
304
- * The ownership half of {@link releaseLock}, minus the token check — a
263
+ * The ownership half of {@link releaseLock}, minus the token check: a
305
264
  * signal-time sweeper knows which lock paths its own operations opened, but
306
265
  * not the per-acquisition UUID those acquisitions minted. The PID check is
307
266
  * still the one that matters: it stops the sweeper deleting a lock a
308
- * DIFFERENT process acquired in the window between our operation dying and
267
+ * different process acquired in the window between our operation dying and
309
268
  * the sweep running.
310
269
  *
311
270
  * Never throws: callers are shutdown paths where a slow or failing I/O must
@@ -334,44 +293,52 @@ export async function releaseLockIfOwned(lockPath) {
334
293
  * Typed as {@link LockContentionError} (a `FireForgeError`) so the CLI
335
294
  * boundary prints the refusal as one clean line instead of an "Unexpected
336
295
  * error" stack. Callers supplying `onTimeoutMessage` still get the holder
337
- * identification appended — the reason-first copy stays theirs, the "who
296
+ * identification appended: the reason-first copy stays theirs, the "who
338
297
  * holds it" is ours.
298
+ *
299
+ * `queue` is read at expiry by the caller, not carried over from the last
300
+ * progress probe: see {@link formatQueuePositionReached}.
339
301
  */
340
302
  async function buildLockTimeoutError(lockPath, onTimeoutMessage, cause, queue) {
341
303
  const owner = await readLockOwner(lockPath);
342
- const liveHolder = owner.present && isProcessAlive(owner.pid) ? owner : undefined;
304
+ const liveHolder = owner.present && isLockOwnerAlive(owner) ? owner : undefined;
343
305
  const details = liveHolder !== undefined && liveHolder.metadata.length > 0
344
306
  ? ` (${liveHolder.metadata.join(', ')})`
345
307
  : '';
346
308
  const position = formatQueuePositionReached(queue);
347
309
  if (onTimeoutMessage !== undefined) {
348
- const identifiedHolder = liveHolder !== undefined
349
- ? ` The lock is held by PID ${String(liveHolder.pid)}${details}.`
350
- : '';
310
+ const identifiedHolder = liveHolder !== undefined ? ` The lock is held by PID ${liveHolder.pid}${details}.` : '';
351
311
  return new LockContentionError(`${onTimeoutMessage}${identifiedHolder}${position}`, cause);
352
312
  }
353
313
  const holderHint = liveHolder !== undefined
354
- ? ` It is held by running process ${String(liveHolder.pid)}${details} — wait for it to finish, or stop it and retry.`
314
+ ? ` It is held by running process ${liveHolder.pid}${details} — wait for it to finish, or stop it and retry.`
355
315
  : ' If no other FireForge process is running, remove the lock directory and retry.';
356
316
  return new LockContentionError(`Timed out waiting for file lock ${lockPath}.${holderHint}${position}`, cause);
357
317
  }
358
318
  /**
359
319
  * States the queue position the expired wait actually reached.
360
320
  *
361
- * Silence here is what made the worst outcome invisible: a wait that expires
321
+ * Silence here made the worst outcome invisible: a wait that expires
362
322
  * one position from the head has paid the whole budget for nothing, and the
363
323
  * operator cannot tell that from a queue that never moved. Naming the
364
324
  * position turns "raise the budget" into an informed decision instead of a
365
- * guess. Omitted when no queue was observed — an empty registry is not
325
+ * guess. Omitted when no queue was observed, since an empty registry is not
366
326
  * evidence of an empty queue.
327
+ *
328
+ * The queue is read at the moment the budget expires rather than taken from
329
+ * the last progress probe. A probe snapshot is up to one `waitProgressMs`
330
+ * stale, and, the reason this changed, it may not exist at all: a short
331
+ * budget can expire before the first probe lands (a slow runner's
332
+ * `registerWaiter` alone can outlast it), and a caller that configured no
333
+ * probe at all never had one. Both used to lose the position silently.
367
334
  */
368
335
  function formatQueuePositionReached(queue) {
369
- if (queue === undefined || queue.depth === 0)
336
+ if (queue.depth === 0)
370
337
  return '';
371
338
  if (queue.ahead === 0) {
372
- return ` You were next in a queue of ${String(queue.depth)} when the wait expired — a larger --wait-lock would very likely have got a turn.`;
339
+ return ` You were next in a queue of ${queue.depth} when the wait expired — a larger --wait-lock would very likely have got a turn.`;
373
340
  }
374
- return ` You were still ${String(queue.ahead)} from the head of a queue of ${String(queue.depth)} when the wait expired; re-run with a larger --wait-lock.`;
341
+ return ` You were still ${queue.ahead} from the head of a queue of ${queue.depth} when the wait expired; re-run with a larger --wait-lock.`;
375
342
  }
376
343
  /**
377
344
  * Runs one wait probe: reads holder and queue, and reports progress when the
@@ -395,13 +362,13 @@ async function runWaitProbe(lockPath, startedAt, timeoutMs, options) {
395
362
  * SIGTERM'd command leaves its lock on disk. The next contender reclaims it
396
363
  * immediately (the PID-first staleness check sees a dead owner), but in the
397
364
  * window between, `fireforge status --lock` reports the lock held by a
398
- * process that is not running — which is exactly the question an operator
399
- * asks after a kill. The signal sweep closes that window.
365
+ * process that is not running, which is the question an operator asks after
366
+ * a kill. The signal sweep closes that window.
400
367
  *
401
368
  * Paths only: the per-acquisition token lives in `withFileLock`'s frame and
402
369
  * a sweeper cannot see it. {@link releaseLockIfOwned} checks the PID, which
403
- * is the check that matters — it stops the sweep deleting a lock a
404
- * DIFFERENT process acquired in the meantime.
370
+ * is the check that matters: it stops the sweep deleting a lock a
371
+ * different process acquired in the meantime.
405
372
  */
406
373
  const heldLocks = new Set();
407
374
  /**
@@ -435,14 +402,12 @@ export async function withFileLock(lockPath, operation, options = {}) {
435
402
  let lastStaleProbeAt;
436
403
  let currentPollMs = pollMs;
437
404
  let lastProbeAt = startedAt;
438
- /** Best (lowest) queue position seen so far; used to detect an advance. */
405
+ /** Best (lowest) queue position seen so far. Used to detect an advance. */
439
406
  let bestAhead;
440
- /** Last observed queue state, so the timeout refusal can name the position reached. */
441
- let lastQueue;
442
407
  /** Set on first contention so an uncontended acquire costs nothing. */
443
408
  let deregisterWaiter;
444
409
  // One cadence drives both the progress line and the deadline extension.
445
- // Either feature alone is enough to want the probe; neither is worth a
410
+ // Either feature alone is enough to want the probe. Neither is worth a
446
411
  // second timer.
447
412
  const probeIntervalMs = options.waitProgressMs ?? WAIT_PROBE_INTERVAL_MS;
448
413
  const probeWanted = (options.onWaitProgress !== undefined && options.waitProgressMs !== undefined) ||
@@ -469,26 +434,31 @@ export async function withFileLock(lockPath, operation, options = {}) {
469
434
  }
470
435
  }
471
436
  if (Date.now() >= deadline) {
472
- throw await buildLockTimeoutError(lockPath, options.onTimeoutMessage, error, lastQueue);
437
+ // The queue is read here, not carried over from the last progress
438
+ // probe: the position the wait reached is the one number that says
439
+ // whether a larger budget would have helped, and it must not depend
440
+ // on a probe having happened to land inside the budget. We are still
441
+ // registered at this point. `deregisterWaiter` runs in the `finally`
442
+ // below, which is only entered once this refusal has been built.
443
+ throw await buildLockTimeoutError(lockPath, options.onTimeoutMessage, error, await readLockQueue(lockPath, startedAt));
473
444
  }
474
445
  if (probeWanted && Date.now() - lastProbeAt >= probeIntervalMs) {
475
446
  lastProbeAt = Date.now();
476
- // Report the budget that is CURRENTLY in force, not the one
447
+ // Report the budget that is currently in force, not the one
477
448
  // originally requested: once an extension has been granted, the
478
449
  // requested figure is stale, and a progress line that keeps
479
450
  // quoting it reads as a timeout that does not fire.
480
451
  const probe = await runWaitProbe(lockPath, startedAt, deadline - startedAt, options);
481
- lastQueue = probe.queue;
482
- // An advance — someone ahead of us took their turn and finished —
452
+ // An advance (someone ahead of us took their turn and finished)
483
453
  // buys a fresh budget, because the wait is working. Only a queue
484
454
  // that stops moving starves, which is the case the timeout is for.
485
455
  //
486
- // The FIRST observation is a seed, never an advance. `bestAhead`
456
+ // The first observation is a seed, never an advance. `bestAhead`
487
457
  // used to start `undefined`, so the opening probe always satisfied
488
458
  // this test and handed out a free full budget: a `--wait-lock 300`
489
459
  // run outlived its 300 s even against a queue that never moved
490
- // once, which is exactly the "it does not reliably give up after N
491
- // seconds" a downstream fork reported.
460
+ // once, which is the "it does not reliably give up after N
461
+ // seconds" behaviour a downstream fork reported.
492
462
  if (hardDeadline !== undefined) {
493
463
  if (bestAhead === undefined) {
494
464
  bestAhead = probe.queue.ahead;
@@ -511,32 +481,24 @@ export async function withFileLock(lockPath, operation, options = {}) {
511
481
  }
512
482
  }
513
483
  finally {
514
- // Leaving the queue happens the moment we stop waiting — whether we
484
+ // Leaving the queue happens the moment we stop waiting, whether we
515
485
  // acquired the lock or timed out.
516
486
  await deregisterWaiter?.();
517
487
  }
518
- // Stamp ownership (PID + per-acquisition token) into the lock directory so
519
- // stale-lock recovery can check liveness and release can verify the lock
520
- // is still ours before removing it.
488
+ // Stamp ownership (PID + per-acquisition token + acquisition time) into
489
+ // the lock directory so stale-lock recovery can check liveness and release
490
+ // can verify the lock is still ours before removing it. Fatal on failure:
491
+ // an owner-less lock is reaped by the age heuristic after five minutes,
492
+ // which for a multi-hour build meant a second holder mid-critical-section.
493
+ // `writeLockOwner` removes the just-created directory before throwing.
521
494
  const ownerToken = randomUUID();
522
- let ownerFileWritten = false;
523
- try {
524
- const metadata = options.ownerMetadata
525
- ?.map((line) => line.replace(/\r?\n/g, ' ').trim())
526
- .filter((line) => line.length > 0) ?? [];
527
- await writeFile(join(lockPath, LOCK_PID_FILE), `${String(process.pid)}\n${ownerToken}\n${metadata.length > 0 ? `${metadata.join('\n')}\n` : ''}`, 'utf-8');
528
- ownerFileWritten = true;
529
- }
530
- catch {
531
- // Non-fatal: stale recovery falls back to age-only heuristic and release
532
- // falls back to unconditional removal.
533
- }
495
+ await writeLockOwner(lockPath, ownerToken, options.ownerMetadata);
534
496
  heldLocks.add(lockPath);
535
497
  try {
536
498
  return await operation();
537
499
  }
538
500
  finally {
539
501
  heldLocks.delete(lockPath);
540
- await releaseLock(lockPath, ownerToken, ownerFileWritten);
502
+ await releaseLock(lockPath, ownerToken);
541
503
  }
542
504
  }
@@ -3,13 +3,25 @@
3
3
  */
4
4
  import type { ResolvedArchive } from './firefox-archive.js';
5
5
  import type { ProgressCallback } from './firefox-download.js';
6
+ /** Options governing the default (unpinned) integrity check. */
7
+ export interface ArchiveIntegrityOptions {
8
+ /**
9
+ * Accept the download with a warning when the published SHA256SUMS cannot
10
+ * be obtained (`firefox.allowUnverifiedDownload`). Default: fail closed.
11
+ */
12
+ allowUnverifiedDownload?: boolean;
13
+ }
6
14
  /**
7
15
  * Ensures a valid cached archive exists, downloading it if needed.
8
16
  * @param archive - Resolved archive descriptor
9
17
  * @param cacheDir - Cache directory
10
18
  * @param onProgress - Optional progress callback
19
+ * @param expectedSha256 - Operator-pinned digest. When set it replaces the
20
+ * published-SHA256SUMS check
21
+ * @param onCacheProgress - Phase progress messages
22
+ * @param integrity - Default integrity-check options (unpinned downloads)
11
23
  */
12
- export declare function ensureCachedArchive(archive: ResolvedArchive, cacheDir: string, onProgress?: ProgressCallback, expectedSha256?: string, onCacheProgress?: (message: string) => void): Promise<void>;
24
+ export declare function ensureCachedArchive(archive: ResolvedArchive, cacheDir: string, onProgress?: ProgressCallback, expectedSha256?: string, onCacheProgress?: (message: string) => void, integrity?: ArchiveIntegrityOptions): Promise<void>;
13
25
  /**
14
26
  * Removes cached tarball, metadata, and partial download files for an archive.
15
27
  * @param archive - Resolved archive descriptor
@@ -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 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) {
@@ -94,7 +133,7 @@ async function cacheEntryExists(archive, cacheDir) {
94
133
  async function validateCachedArchive(archive, cacheDir, expectedSha256) {
95
134
  const tarballPath = join(cacheDir, archive.filename);
96
135
  const metadataPath = join(cacheDir, archive.metadataFilename);
97
- // Deliberately outside the try/catch below: a permission error probing the
136
+ // Outside the try/catch below on purpose: a permission error probing the
98
137
  // cache must propagate instead of reading as "invalid cache" and triggering
99
138
  // a re-download into a directory we cannot write anyway.
100
139
  if (!(await pathExistsStrict(tarballPath)) || !(await pathExistsStrict(metadataPath))) {
@@ -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, {
@@ -219,6 +244,6 @@ async function removeArchivePartFiles(archive, cacheDir) {
219
244
  }
220
245
  catch (error) {
221
246
  void error;
222
- // Cache dir may not exist yet — that's fine.
247
+ // Cache dir may not exist yet. That's fine.
223
248
  }
224
249
  }