@starci/hfs 4.11.22 → 4.12.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 (1088) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +31 -31
  3. package/ci/cli.d.mts +15 -0
  4. package/ci/cli.mjs +71 -52
  5. package/ci/github.d.mts +43 -0
  6. package/ci/github.mjs +33 -28
  7. package/ci/local.d.mts +12 -0
  8. package/ci/local.mjs +145 -108
  9. package/ci/secret.d.mts +15 -0
  10. package/ci/secret.mjs +31 -21
  11. package/ci/vars.d.mts +17 -0
  12. package/ci/vars.mjs +47 -33
  13. package/ci/verify.d.mts +18 -0
  14. package/ci/verify.mjs +52 -40
  15. package/codemod/button-intent.d.mts +63 -0
  16. package/codemod/button-intent.mjs +338 -285
  17. package/coverage/cli.d.mts +28 -0
  18. package/coverage/cli.mjs +65 -51
  19. package/db/backup-receipt.d.mts +28 -0
  20. package/db/backup-receipt.mjs +78 -58
  21. package/db/drift.d.mts +86 -0
  22. package/db/drift.mjs +213 -180
  23. package/db/dump.d.mts +52 -0
  24. package/db/dump.mjs +63 -56
  25. package/db/entity-list.d.mts +4 -0
  26. package/db/entity-list.mjs +0 -2
  27. package/db/image.d.mts +8 -0
  28. package/db/image.mjs +1 -2
  29. package/db/migrate.d.mts +104 -0
  30. package/db/migrate.mjs +207 -167
  31. package/db/new.d.mts +11 -0
  32. package/db/new.mjs +25 -25
  33. package/db/post-cutover.d.mts +5 -0
  34. package/db/post-cutover.mjs +89 -82
  35. package/db/postgres.d.mts +71 -0
  36. package/db/postgres.mjs +62 -61
  37. package/emit/compiler.d.mts +3 -0
  38. package/emit/compiler.mjs +25 -27
  39. package/emit/contracts.d.mts +35 -0
  40. package/emit/contracts.mjs +46 -50
  41. package/emit/events.d.mts +9 -0
  42. package/emit/events.mjs +19 -20
  43. package/emit/form-bounds.d.mts +83 -0
  44. package/emit/form-bounds.mjs +133 -117
  45. package/emit/graphql-input-bounds.d.mts +63 -0
  46. package/emit/graphql-input-bounds.mjs +90 -79
  47. package/emit/input-bounds.d.mts +30 -0
  48. package/emit/input-bounds.mjs +29 -27
  49. package/emit/operation-schemas.d.mts +24 -0
  50. package/emit/operation-schemas.mjs +136 -116
  51. package/emit/operations-worker.d.mts +1 -0
  52. package/emit/operations-worker.mjs +9 -7
  53. package/emit/operations.d.mts +47 -0
  54. package/emit/operations.mjs +124 -133
  55. package/emit/schema-worker.d.mts +1 -0
  56. package/emit/schema-worker.mjs +57 -50
  57. package/emit/static-graph.d.mts +48 -0
  58. package/emit/static-graph.mjs +766 -640
  59. package/emit/type-schema.d.mts +29 -0
  60. package/emit/type-schema.mjs +153 -140
  61. package/lint/bound-sys.cjs +27 -33
  62. package/lint/chunk.d.mts +17 -0
  63. package/lint/chunk.mjs +41 -31
  64. package/lint/run.d.mts +120 -0
  65. package/lint/run.mjs +414 -325
  66. package/package.json +21 -4
  67. package/release/candidate-ci.d.mts +46 -0
  68. package/release/candidate-ci.mjs +76 -70
  69. package/release/gate.d.mts +120 -0
  70. package/release/gate.mjs +358 -330
  71. package/release/index.d.mts +68 -0
  72. package/release/index.mjs +95 -73
  73. package/release/lint-report.d.mts +14 -0
  74. package/release/lint-report.mjs +33 -32
  75. package/release/push.d.mts +32 -0
  76. package/release/push.mjs +92 -79
  77. package/release/records.d.mts +18 -0
  78. package/release/records.mjs +22 -21
  79. package/release/sonar-quality-gate.d.mts +18 -0
  80. package/release/sonar-quality-gate.mjs +60 -52
  81. package/release/urgent.d.mts +20 -0
  82. package/release/urgent.mjs +76 -74
  83. package/report/order.d.mts +1 -0
  84. package/report/sonar.d.mts +73 -0
  85. package/report/sonar.mjs +82 -89
  86. package/runtime/engine/admission.d.mts +209 -0
  87. package/runtime/engine/admission.mjs +225 -204
  88. package/runtime/engine/by-code-unit.d.mts +5 -0
  89. package/runtime/engine/by-code-unit.mjs +6 -6
  90. package/runtime/engine/config.d.mts +333 -0
  91. package/runtime/engine/config.mjs +486 -328
  92. package/runtime/engine/db/blob.d.mts +83 -0
  93. package/runtime/engine/db/blob.mjs +344 -268
  94. package/runtime/engine/db/event-compact.d.mts +21 -0
  95. package/runtime/engine/db/event-compact.mjs +34 -38
  96. package/runtime/engine/db/machine-connection.d.mts +68 -0
  97. package/runtime/engine/db/machine-connection.mjs +166 -119
  98. package/runtime/engine/db/machine-open.d.mts +26 -0
  99. package/runtime/engine/db/machine-open.mjs +59 -45
  100. package/runtime/engine/db/machine-schema.d.mts +34 -0
  101. package/runtime/engine/db/machine-schema.mjs +102 -83
  102. package/runtime/engine/db/machine.d.mts +1746 -0
  103. package/runtime/engine/db/machine.mjs +1040 -734
  104. package/runtime/engine/db/provider-reservations.d.mts +169 -0
  105. package/runtime/engine/db/provider-reservations.mjs +141 -119
  106. package/runtime/engine/db/provision-lock.d.mts +23 -0
  107. package/runtime/engine/db/provision-lock.mjs +14 -14
  108. package/runtime/engine/db/ref-value.d.mts +19 -0
  109. package/runtime/engine/db/ref-value.mjs +36 -35
  110. package/runtime/engine/db/sqlite-types.d.mts +28 -0
  111. package/runtime/engine/db/sqlite-types.mjs +4 -0
  112. package/runtime/engine/digest.d.mts +4 -0
  113. package/runtime/engine/digest.mjs +16 -7
  114. package/runtime/engine/invalid-config.d.mts +13 -0
  115. package/runtime/engine/invalid-config.mjs +17 -11
  116. package/runtime/engine/model-config.d.mts +91 -0
  117. package/runtime/engine/model-config.mjs +98 -76
  118. package/runtime/engine/orca-config.d.mts +26 -0
  119. package/runtime/engine/orca-config.mjs +22 -17
  120. package/runtime/engine/plain-object.d.mts +2 -0
  121. package/runtime/engine/refuse.d.mts +9 -0
  122. package/runtime/engine/refuse.mjs +3 -6
  123. package/runtime/engine/release-config.d.mts +19 -0
  124. package/runtime/engine/release-config.mjs +22 -18
  125. package/runtime/engine/removed-vocabulary.d.mts +12 -0
  126. package/runtime/engine/removed-vocabulary.mjs +21 -20
  127. package/runtime/engine/resources-config.d.mts +34 -0
  128. package/runtime/engine/resources-config.mjs +42 -35
  129. package/runtime/engine/runtime-root.d.mts +28 -0
  130. package/runtime/engine/runtime-root.mjs +11 -13
  131. package/runtime/engine/secrets.d.mts +41 -0
  132. package/runtime/engine/secrets.mjs +170 -102
  133. package/runtime/engine/sonar-config.d.mts +10 -0
  134. package/runtime/engine/sonar-config.mjs +16 -13
  135. package/runtime/engine/temp-root.d.mts +17 -0
  136. package/runtime/engine/temp-root.mjs +3 -9
  137. package/runtime/engine/yaml.d.mts +9 -0
  138. package/runtime/knowledge/hfs/canon-pins.yaml +10 -10
  139. package/runtime/knowledge/hfs/ci-standard.yaml +2 -2
  140. package/runtime/knowledge/hfs/rules.yaml +72 -6
  141. package/runtime/knowledge/hfs/slots.yaml +3 -3
  142. package/runtime/knowledge/sonar-gate.yaml +1 -1
  143. package/runtime/modules/kernel/failure-codes.yaml +43 -32
  144. package/runtime/modules/schemas/application-stacks.schema.yaml +3 -3
  145. package/runtime/packages/cli/src/catalog.generated.d.mts +13 -0
  146. package/runtime/packages/cli/src/catalog.generated.mjs +1853 -14824
  147. package/runtime/packages/cli/src/validate-args.d.mts +74 -0
  148. package/runtime/packages/cli/src/validate-args.mjs +183 -171
  149. package/runtime/packages/jest-preset/sonar-exclusions.cjs +5 -5
  150. package/runtime/scripts/api/fs/claim-file.d.mts +12 -0
  151. package/runtime/scripts/api/fs/claim-file.mjs +31 -17
  152. package/runtime/scripts/api/fs/ensure-temp-root.d.mts +4 -0
  153. package/runtime/scripts/api/fs/ensure-temp-root.mjs +15 -14
  154. package/runtime/scripts/api/fs/forbidden-root.d.mts +15 -0
  155. package/runtime/scripts/api/fs/forbidden-root.mjs +31 -23
  156. package/runtime/scripts/api/fs/is-link-like.d.mts +10 -0
  157. package/runtime/scripts/api/fs/is-link-like.mjs +25 -11
  158. package/runtime/scripts/api/fs/lib.d.mts +12 -0
  159. package/runtime/scripts/api/fs/lib.mjs +28 -8
  160. package/runtime/scripts/api/fs/make-temp-dir.d.mts +5 -0
  161. package/runtime/scripts/api/fs/make-temp-dir.mjs +3 -4
  162. package/runtime/scripts/api/fs/rename-over.d.mts +13 -0
  163. package/runtime/scripts/api/fs/rename-over.mjs +16 -10
  164. package/runtime/scripts/api/fs/safe-remove.d.mts +25 -0
  165. package/runtime/scripts/api/fs/safe-remove.mjs +129 -71
  166. package/runtime/scripts/api/git/add.d.mts +3 -0
  167. package/runtime/scripts/api/git/add.mjs +0 -1
  168. package/runtime/scripts/api/git/cat-file.d.mts +3 -0
  169. package/runtime/scripts/api/git/cat-file.mjs +0 -1
  170. package/runtime/scripts/api/git/check-ignore.d.mts +3 -0
  171. package/runtime/scripts/api/git/check-ignore.mjs +0 -1
  172. package/runtime/scripts/api/git/commit-tree.d.mts +3 -0
  173. package/runtime/scripts/api/git/commit-tree.mjs +0 -1
  174. package/runtime/scripts/api/git/config-get.d.mts +10 -0
  175. package/runtime/scripts/api/git/config-get.mjs +1 -2
  176. package/runtime/scripts/api/git/diff.d.mts +3 -0
  177. package/runtime/scripts/api/git/diff.mjs +0 -1
  178. package/runtime/scripts/api/git/init.d.mts +7 -0
  179. package/runtime/scripts/api/git/init.mjs +1 -2
  180. package/runtime/scripts/api/git/is-ancestor.d.mts +5 -0
  181. package/runtime/scripts/api/git/is-ancestor.mjs +0 -1
  182. package/runtime/scripts/api/git/lib.d.mts +67 -0
  183. package/runtime/scripts/api/git/lib.mjs +32 -32
  184. package/runtime/scripts/api/git/ls-files.d.mts +3 -0
  185. package/runtime/scripts/api/git/ls-files.mjs +0 -1
  186. package/runtime/scripts/api/git/ls-tree.d.mts +3 -0
  187. package/runtime/scripts/api/git/ls-tree.mjs +0 -1
  188. package/runtime/scripts/api/git/merge-base.d.mts +5 -0
  189. package/runtime/scripts/api/git/merge-base.mjs +2 -3
  190. package/runtime/scripts/api/git/remote.d.mts +3 -0
  191. package/runtime/scripts/api/git/remote.mjs +0 -1
  192. package/runtime/scripts/api/git/rev-parse-query.d.mts +3 -0
  193. package/runtime/scripts/api/git/rev-parse-query.mjs +0 -1
  194. package/runtime/scripts/api/git/show.d.mts +3 -0
  195. package/runtime/scripts/api/git/show.mjs +0 -1
  196. package/runtime/scripts/api/git/tag.d.mts +3 -0
  197. package/runtime/scripts/api/git/tag.mjs +0 -1
  198. package/runtime/scripts/api/git/update-ref.d.mts +7 -0
  199. package/runtime/scripts/api/git/update-ref.mjs +1 -2
  200. package/runtime/scripts/api/git/write-tree.d.mts +3 -0
  201. package/runtime/scripts/api/git/write-tree.mjs +0 -1
  202. package/runtime/scripts/api/node/lib.d.mts +7 -0
  203. package/runtime/scripts/api/node/lib.mjs +17 -7
  204. package/runtime/scripts/api/node/run-node.d.mts +3 -0
  205. package/runtime/scripts/api/node/run-node.mjs +0 -3
  206. package/runtime/scripts/api/process/resolve-real-tool.d.mts +12 -0
  207. package/runtime/scripts/api/process/resolve-real-tool.mjs +46 -38
  208. package/runtime/scripts/api/process/run-program.d.mts +3 -0
  209. package/runtime/scripts/api/process/run-program.mjs +5 -2
  210. package/runtime/scripts/api/sops/decrypt.d.mts +9 -0
  211. package/runtime/scripts/api/sops/decrypt.mjs +1 -2
  212. package/runtime/scripts/api/sops/lib.d.mts +149 -0
  213. package/runtime/scripts/api/sops/lib.mjs +417 -318
  214. package/runtime/scripts/api/sops/resolve-sops.d.mts +3 -0
  215. package/runtime/scripts/api/sops/resolve-sops.mjs +1 -2
  216. package/runtime/scripts/api/sops/seal.d.mts +19 -0
  217. package/runtime/scripts/api/sops/seal.mjs +14 -10
  218. package/runtime/scripts/api/sops/selected-age-recipient.d.mts +3 -0
  219. package/runtime/scripts/api/sops/selected-age-recipient.mjs +1 -2
  220. package/runtime/scripts/api/sops/update-keys.d.mts +16 -0
  221. package/runtime/scripts/api/sops/update-keys.mjs +11 -8
  222. package/runtime/scripts/cli/gen-catalog.d.mts +42 -0
  223. package/runtime/scripts/cli/gen-catalog.mjs +72 -0
  224. package/runtime/scripts/gates/runtime-host.d.mts +23 -0
  225. package/runtime/scripts/gates/runtime-host.mjs +48 -41
  226. package/runtime/scripts/gates/sonar-gate.d.mts +219 -0
  227. package/runtime/scripts/gates/sonar-gate.mjs +166 -153
  228. package/runtime/scripts/gates/starcistacks-ci.d.mts +35 -0
  229. package/runtime/scripts/gates/starcistacks-ci.mjs +47 -39
  230. package/runtime/scripts/gates/starcistacks-form.d.mts +80 -0
  231. package/runtime/scripts/gates/starcistacks-form.mjs +16 -14
  232. package/runtime/scripts/gates/starcistacks-runtime.d.mts +1 -0
  233. package/runtime/scripts/gates/starcistacks-value-file.d.mts +2 -0
  234. package/runtime/scripts/gates/starcistacks-value-file.mjs +14 -11
  235. package/runtime/scripts/gates/starcistacks.d.mts +141 -0
  236. package/runtime/scripts/gates/starcistacks.mjs +470 -355
  237. package/runtime/scripts/guards/git-pathspec.d.mts +48 -0
  238. package/runtime/scripts/guards/git-pathspec.mjs +215 -130
  239. package/runtime/scripts/guards/git-policy.d.mts +30 -0
  240. package/runtime/scripts/guards/git-policy.mjs +278 -235
  241. package/runtime/scripts/guards/hook-io.d.mts +22 -0
  242. package/runtime/scripts/guards/hook-io.mjs +77 -63
  243. package/runtime/scripts/guards/push-scan.d.mts +31 -0
  244. package/runtime/scripts/guards/push-scan.mjs +93 -80
  245. package/runtime/scripts/guards/release-push-gate.d.mts +22 -0
  246. package/runtime/scripts/guards/release-push-gate.mjs +40 -38
  247. package/runtime/scripts/hfs/allows.d.mts +24 -0
  248. package/runtime/scripts/hfs/allows.mjs +37 -38
  249. package/runtime/scripts/hfs/app-wiring.d.mts +72 -0
  250. package/runtime/scripts/hfs/app-wiring.mjs +197 -167
  251. package/runtime/scripts/hfs/architecture/ast-walks.d.mts +89 -0
  252. package/runtime/scripts/hfs/architecture/ast-walks.mjs +180 -163
  253. package/runtime/scripts/hfs/architecture/automatic-gates.d.mts +21 -0
  254. package/runtime/scripts/hfs/architecture/automatic-gates.mjs +80 -73
  255. package/runtime/scripts/hfs/architecture/backend-app-files.d.mts +10 -0
  256. package/runtime/scripts/hfs/architecture/backend-app-files.mjs +88 -77
  257. package/runtime/scripts/hfs/architecture/backend.d.mts +5 -0
  258. package/runtime/scripts/hfs/architecture/backend.mjs +378 -357
  259. package/runtime/scripts/hfs/architecture/background-unowned.d.mts +25 -0
  260. package/runtime/scripts/hfs/architecture/background-unowned.mjs +201 -187
  261. package/runtime/scripts/hfs/architecture/canon-clones.d.mts +8 -0
  262. package/runtime/scripts/hfs/architecture/canon-clones.mjs +260 -209
  263. package/runtime/scripts/hfs/architecture/canon-jsx-clones.d.mts +5 -0
  264. package/runtime/scripts/hfs/architecture/canon-jsx-clones.mjs +149 -116
  265. package/runtime/scripts/hfs/architecture/client-reaches-server.d.mts +28 -0
  266. package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +72 -68
  267. package/runtime/scripts/hfs/architecture/clones.d.mts +54 -0
  268. package/runtime/scripts/hfs/architecture/clones.mjs +212 -217
  269. package/runtime/scripts/hfs/architecture/config-unread.d.mts +14 -0
  270. package/runtime/scripts/hfs/architecture/config-unread.mjs +7 -9
  271. package/runtime/scripts/hfs/architecture/config.d.mts +74 -0
  272. package/runtime/scripts/hfs/architecture/config.mjs +302 -268
  273. package/runtime/scripts/hfs/architecture/connection-map.d.mts +33 -0
  274. package/runtime/scripts/hfs/architecture/connection-map.mjs +258 -225
  275. package/runtime/scripts/hfs/architecture/constructor-deps.d.mts +57 -0
  276. package/runtime/scripts/hfs/architecture/constructor-deps.mjs +80 -71
  277. package/runtime/scripts/hfs/architecture/context-coupling.d.mts +21 -0
  278. package/runtime/scripts/hfs/architecture/context-coupling.mjs +82 -77
  279. package/runtime/scripts/hfs/architecture/context-map.d.mts +102 -0
  280. package/runtime/scripts/hfs/architecture/context-map.mjs +267 -234
  281. package/runtime/scripts/hfs/architecture/context-owner.d.mts +15 -0
  282. package/runtime/scripts/hfs/architecture/context-owner.mjs +73 -61
  283. package/runtime/scripts/hfs/architecture/context-platform-tables.d.mts +18 -0
  284. package/runtime/scripts/hfs/architecture/context-platform-tables.mjs +47 -43
  285. package/runtime/scripts/hfs/architecture/context-transaction.d.mts +20 -0
  286. package/runtime/scripts/hfs/architecture/context-transaction.mjs +54 -52
  287. package/runtime/scripts/hfs/architecture/contract-fixture-guard.d.mts +37 -0
  288. package/runtime/scripts/hfs/architecture/contract-fixture-guard.mjs +113 -104
  289. package/runtime/scripts/hfs/architecture/contracts-readonly.d.mts +32 -0
  290. package/runtime/scripts/hfs/architecture/contracts-readonly.mjs +288 -270
  291. package/runtime/scripts/hfs/architecture/contracts.d.mts +30 -0
  292. package/runtime/scripts/hfs/architecture/contracts.mjs +408 -393
  293. package/runtime/scripts/hfs/architecture/cross-app-duplicate.d.mts +27 -0
  294. package/runtime/scripts/hfs/architecture/cross-app-duplicate.mjs +50 -51
  295. package/runtime/scripts/hfs/architecture/dead-exports-bindings.d.mts +21 -0
  296. package/runtime/scripts/hfs/architecture/dead-exports-bindings.mjs +143 -119
  297. package/runtime/scripts/hfs/architecture/dead-exports.d.mts +32 -0
  298. package/runtime/scripts/hfs/architecture/dead-exports.mjs +286 -228
  299. package/runtime/scripts/hfs/architecture/default-deny.d.mts +21 -0
  300. package/runtime/scripts/hfs/architecture/default-deny.mjs +129 -110
  301. package/runtime/scripts/hfs/architecture/doc-language.d.mts +17 -0
  302. package/runtime/scripts/hfs/architecture/doc-language.mjs +24 -19
  303. package/runtime/scripts/hfs/architecture/entity-registry.d.mts +49 -0
  304. package/runtime/scripts/hfs/architecture/entity-registry.mjs +20 -22
  305. package/runtime/scripts/hfs/architecture/entrypoint.d.mts +13 -0
  306. package/runtime/scripts/hfs/architecture/entrypoint.mjs +47 -45
  307. package/runtime/scripts/hfs/architecture/error-codes.d.mts +16 -0
  308. package/runtime/scripts/hfs/architecture/error-codes.mjs +37 -35
  309. package/runtime/scripts/hfs/architecture/error-masked.d.mts +16 -0
  310. package/runtime/scripts/hfs/architecture/error-masked.mjs +55 -59
  311. package/runtime/scripts/hfs/architecture/fe-slot-allows.d.mts +38 -0
  312. package/runtime/scripts/hfs/architecture/fe-slot-allows.mjs +24 -23
  313. package/runtime/scripts/hfs/architecture/feature-shape.d.mts +26 -0
  314. package/runtime/scripts/hfs/architecture/feature-shape.mjs +40 -38
  315. package/runtime/scripts/hfs/architecture/framework-pinned.d.mts +3 -0
  316. package/runtime/scripts/hfs/architecture/framework-pinned.mjs +21 -21
  317. package/runtime/scripts/hfs/architecture/frontend-block-render.d.mts +26 -0
  318. package/runtime/scripts/hfs/architecture/frontend-block-render.mjs +106 -98
  319. package/runtime/scripts/hfs/architecture/frontend-grammar.d.mts +7 -0
  320. package/runtime/scripts/hfs/architecture/frontend-grammar.mjs +153 -150
  321. package/runtime/scripts/hfs/architecture/frontend-routing.d.mts +23 -0
  322. package/runtime/scripts/hfs/architecture/frontend-routing.mjs +161 -149
  323. package/runtime/scripts/hfs/architecture/frontend-world-render.d.mts +72 -0
  324. package/runtime/scripts/hfs/architecture/frontend-world-render.mjs +343 -305
  325. package/runtime/scripts/hfs/architecture/frontend-world.d.mts +94 -0
  326. package/runtime/scripts/hfs/architecture/frontend-world.mjs +340 -294
  327. package/runtime/scripts/hfs/architecture/frontend.d.mts +31 -0
  328. package/runtime/scripts/hfs/architecture/frontend.mjs +174 -184
  329. package/runtime/scripts/hfs/architecture/function-spec-providers.d.mts +27 -0
  330. package/runtime/scripts/hfs/architecture/function-spec-providers.mjs +124 -106
  331. package/runtime/scripts/hfs/architecture/hfs-graph.d.mts +78 -0
  332. package/runtime/scripts/hfs/architecture/hfs-graph.mjs +52 -48
  333. package/runtime/scripts/hfs/architecture/hfs.d.mts +135 -0
  334. package/runtime/scripts/hfs/architecture/hfs.mjs +328 -282
  335. package/runtime/scripts/hfs/architecture/hooks-are-hooks.d.mts +24 -0
  336. package/runtime/scripts/hfs/architecture/hooks-are-hooks.mjs +81 -71
  337. package/runtime/scripts/hfs/architecture/i18n-keys.d.mts +31 -0
  338. package/runtime/scripts/hfs/architecture/i18n-keys.mjs +155 -141
  339. package/runtime/scripts/hfs/architecture/identity.d.mts +7 -0
  340. package/runtime/scripts/hfs/architecture/identity.mjs +141 -125
  341. package/runtime/scripts/hfs/architecture/index.d.mts +101 -0
  342. package/runtime/scripts/hfs/architecture/index.mjs +272 -272
  343. package/runtime/scripts/hfs/architecture/injection-token-exported.d.mts +21 -0
  344. package/runtime/scripts/hfs/architecture/injection-token-exported.mjs +45 -43
  345. package/runtime/scripts/hfs/architecture/jwt-ast.d.mts +16 -0
  346. package/runtime/scripts/hfs/architecture/jwt-ast.mjs +195 -176
  347. package/runtime/scripts/hfs/architecture/machine-ast.d.mts +88 -0
  348. package/runtime/scripts/hfs/architecture/machine-ast.mjs +163 -155
  349. package/runtime/scripts/hfs/architecture/managed-scripts.d.mts +2 -0
  350. package/runtime/scripts/hfs/architecture/managed-scripts.mjs +19 -20
  351. package/runtime/scripts/hfs/architecture/module-per-transport.d.mts +35 -0
  352. package/runtime/scripts/hfs/architecture/module-per-transport.mjs +177 -172
  353. package/runtime/scripts/hfs/architecture/next-data-contract.d.mts +35 -0
  354. package/runtime/scripts/hfs/architecture/next-data-contract.mjs +100 -78
  355. package/runtime/scripts/hfs/architecture/next-data-inference.d.mts +39 -0
  356. package/runtime/scripts/hfs/architecture/next-data-inference.mjs +170 -154
  357. package/runtime/scripts/hfs/architecture/next-data-key.d.mts +50 -0
  358. package/runtime/scripts/hfs/architecture/next-data-key.mjs +311 -275
  359. package/runtime/scripts/hfs/architecture/next-data-symbols.d.mts +5 -0
  360. package/runtime/scripts/hfs/architecture/next-data-symbols.mjs +17 -17
  361. package/runtime/scripts/hfs/architecture/next-data.d.mts +36 -0
  362. package/runtime/scripts/hfs/architecture/next-data.mjs +351 -334
  363. package/runtime/scripts/hfs/architecture/operation-contract-roots.d.mts +10 -0
  364. package/runtime/scripts/hfs/architecture/operation-contract-roots.mjs +39 -37
  365. package/runtime/scripts/hfs/architecture/owners.d.mts +5 -0
  366. package/runtime/scripts/hfs/architecture/owners.mjs +97 -98
  367. package/runtime/scripts/hfs/architecture/package-shape.d.mts +26 -0
  368. package/runtime/scripts/hfs/architecture/package-shape.mjs +56 -55
  369. package/runtime/scripts/hfs/architecture/presentation.d.mts +24 -0
  370. package/runtime/scripts/hfs/architecture/presentation.mjs +138 -130
  371. package/runtime/scripts/hfs/architecture/reachability.d.mts +25 -0
  372. package/runtime/scripts/hfs/architecture/reachability.mjs +258 -241
  373. package/runtime/scripts/hfs/architecture/register-once.d.mts +18 -0
  374. package/runtime/scripts/hfs/architecture/register-once.mjs +152 -139
  375. package/runtime/scripts/hfs/architecture/registration.d.mts +15 -0
  376. package/runtime/scripts/hfs/architecture/registration.mjs +301 -267
  377. package/runtime/scripts/hfs/architecture/required-files.d.mts +64 -0
  378. package/runtime/scripts/hfs/architecture/required-files.mjs +172 -167
  379. package/runtime/scripts/hfs/architecture/route-files-thin.d.mts +23 -0
  380. package/runtime/scripts/hfs/architecture/route-files-thin.mjs +85 -77
  381. package/runtime/scripts/hfs/architecture/schema-owner.d.mts +34 -0
  382. package/runtime/scripts/hfs/architecture/schema-owner.mjs +206 -181
  383. package/runtime/scripts/hfs/architecture/server-action.d.mts +22 -0
  384. package/runtime/scripts/hfs/architecture/server-action.mjs +48 -48
  385. package/runtime/scripts/hfs/architecture/side-boundary.d.mts +3 -0
  386. package/runtime/scripts/hfs/architecture/side-boundary.mjs +2 -5
  387. package/runtime/scripts/hfs/architecture/source-names-shape.d.mts +85 -0
  388. package/runtime/scripts/hfs/architecture/source-names-shape.mjs +261 -259
  389. package/runtime/scripts/hfs/architecture/source-names.d.mts +7 -0
  390. package/runtime/scripts/hfs/architecture/source-names.mjs +304 -278
  391. package/runtime/scripts/hfs/architecture/sql-keys.d.mts +5 -0
  392. package/runtime/scripts/hfs/architecture/sql-keys.mjs +277 -194
  393. package/runtime/scripts/hfs/architecture/sql-owner.d.mts +30 -0
  394. package/runtime/scripts/hfs/architecture/sql-owner.mjs +236 -194
  395. package/runtime/scripts/hfs/architecture/sql-returning.d.mts +22 -0
  396. package/runtime/scripts/hfs/architecture/sql-returning.mjs +46 -45
  397. package/runtime/scripts/hfs/architecture/sql-token-forms.d.mts +21 -0
  398. package/runtime/scripts/hfs/architecture/sql-token-forms.mjs +86 -74
  399. package/runtime/scripts/hfs/architecture/sql-tokens.d.mts +32 -0
  400. package/runtime/scripts/hfs/architecture/sql-tokens.mjs +456 -341
  401. package/runtime/scripts/hfs/architecture/staged-consumers.d.mts +7 -0
  402. package/runtime/scripts/hfs/architecture/staged-consumers.mjs +35 -30
  403. package/runtime/scripts/hfs/architecture/surface.d.mts +26 -0
  404. package/runtime/scripts/hfs/architecture/surface.mjs +64 -74
  405. package/runtime/scripts/hfs/architecture/symbols.d.mts +11 -0
  406. package/runtime/scripts/hfs/architecture/symbols.mjs +209 -216
  407. package/runtime/scripts/hfs/architecture/test-world-files.d.mts +45 -0
  408. package/runtime/scripts/hfs/architecture/test-world-files.mjs +165 -155
  409. package/runtime/scripts/hfs/architecture/tiers.d.mts +29 -0
  410. package/runtime/scripts/hfs/architecture/tiers.mjs +161 -154
  411. package/runtime/scripts/hfs/architecture/transport-owner.d.mts +37 -0
  412. package/runtime/scripts/hfs/architecture/transport-owner.mjs +155 -149
  413. package/runtime/scripts/hfs/architecture/type-context.d.mts +60 -0
  414. package/runtime/scripts/hfs/architecture/type-context.mjs +255 -250
  415. package/runtime/scripts/hfs/architecture/typescript.d.mts +94 -0
  416. package/runtime/scripts/hfs/architecture/typescript.mjs +358 -318
  417. package/runtime/scripts/hfs/architecture/unit-spec-providers.d.mts +34 -0
  418. package/runtime/scripts/hfs/architecture/unit-spec-providers.mjs +110 -92
  419. package/runtime/scripts/hfs/architecture.d.mts +13 -0
  420. package/runtime/scripts/hfs/architecture.mjs +39 -29
  421. package/runtime/scripts/hfs/canon-pins.d.mts +26 -0
  422. package/runtime/scripts/hfs/canon-pins.mjs +5 -8
  423. package/runtime/scripts/hfs/catalog-once.d.mts +2 -0
  424. package/runtime/scripts/hfs/catalog-once.mjs +8 -9
  425. package/runtime/scripts/hfs/check-results.d.mts +29 -0
  426. package/runtime/scripts/hfs/check-results.mjs +14 -18
  427. package/runtime/scripts/hfs/check.d.mts +144 -0
  428. package/runtime/scripts/hfs/check.mjs +309 -329
  429. package/runtime/scripts/hfs/ci-declaration.d.mts +77 -0
  430. package/runtime/scripts/hfs/ci-declaration.mjs +99 -87
  431. package/runtime/scripts/hfs/coverage-from.d.mts +22 -0
  432. package/runtime/scripts/hfs/coverage-from.mjs +52 -49
  433. package/runtime/scripts/hfs/coverage-scope.d.mts +87 -0
  434. package/runtime/scripts/hfs/coverage-scope.mjs +139 -142
  435. package/runtime/scripts/hfs/db-baseline-verb.d.mts +84 -0
  436. package/runtime/scripts/hfs/db-baseline-verb.mjs +327 -266
  437. package/runtime/scripts/hfs/db-errors.d.mts +2 -0
  438. package/runtime/scripts/hfs/db-errors.mjs +2 -1
  439. package/runtime/scripts/hfs/db-plan-verb.d.mts +33 -0
  440. package/runtime/scripts/hfs/db-plan-verb.mjs +174 -128
  441. package/runtime/scripts/hfs/db-restamp-verb.d.mts +5 -0
  442. package/runtime/scripts/hfs/db-restamp-verb.mjs +94 -70
  443. package/runtime/scripts/hfs/db-squash.d.mts +117 -0
  444. package/runtime/scripts/hfs/db-squash.mjs +414 -351
  445. package/runtime/scripts/hfs/declaration-shape.d.mts +3 -0
  446. package/runtime/scripts/hfs/declaration-shape.mjs +140 -103
  447. package/runtime/scripts/hfs/declaration-slots.d.mts +18 -0
  448. package/runtime/scripts/hfs/declaration-slots.mjs +64 -61
  449. package/runtime/scripts/hfs/doc-commands.d.mts +71 -0
  450. package/runtime/scripts/hfs/doc-commands.mjs +257 -250
  451. package/runtime/scripts/hfs/edition-slots.d.mts +37 -0
  452. package/runtime/scripts/hfs/edition-slots.mjs +43 -40
  453. package/runtime/scripts/hfs/example-secrets.d.mts +18 -0
  454. package/runtime/scripts/hfs/example-secrets.mjs +173 -154
  455. package/runtime/scripts/hfs/gate-threshold.d.mts +20 -0
  456. package/runtime/scripts/hfs/gate-threshold.mjs +14 -22
  457. package/runtime/scripts/hfs/grammar-presentation.d.mts +74 -0
  458. package/runtime/scripts/hfs/grammar-presentation.mjs +109 -106
  459. package/runtime/scripts/hfs/linear-text.d.mts +6 -0
  460. package/runtime/scripts/hfs/linear-text.mjs +20 -20
  461. package/runtime/scripts/hfs/manifest-shape.d.mts +216 -0
  462. package/runtime/scripts/hfs/manifest-shape.mjs +399 -324
  463. package/runtime/scripts/hfs/param-names.d.mts +3 -0
  464. package/runtime/scripts/hfs/param-names.mjs +6 -11
  465. package/runtime/scripts/hfs/path-findings.d.mts +22 -0
  466. package/runtime/scripts/hfs/path-findings.mjs +120 -100
  467. package/runtime/scripts/hfs/pin-findings.d.mts +19 -0
  468. package/runtime/scripts/hfs/pin-findings.mjs +26 -21
  469. package/runtime/scripts/hfs/refusal-codes.d.mts +1 -0
  470. package/runtime/scripts/hfs/refusal-codes.mjs +2 -2
  471. package/runtime/scripts/hfs/repo-identity.d.mts +15 -0
  472. package/runtime/scripts/hfs/repo-identity.mjs +55 -42
  473. package/runtime/scripts/hfs/rule-catalog.d.mts +77 -0
  474. package/runtime/scripts/hfs/rule-catalog.mjs +353 -257
  475. package/runtime/scripts/hfs/rule-params-shape.d.mts +10 -0
  476. package/runtime/scripts/hfs/rule-params-shape.mjs +60 -47
  477. package/runtime/scripts/hfs/rules/a15-findings.d.mts +18 -0
  478. package/runtime/scripts/hfs/rules/a15-findings.mjs +59 -55
  479. package/runtime/scripts/hfs/rules/app-env-required.d.mts +9 -0
  480. package/runtime/scripts/hfs/rules/app-env-required.mjs +13 -13
  481. package/runtime/scripts/hfs/rules/app-root.d.mts +15 -0
  482. package/runtime/scripts/hfs/rules/app-root.mjs +20 -21
  483. package/runtime/scripts/hfs/rules/app-workflow-findings.d.mts +12 -0
  484. package/runtime/scripts/hfs/rules/app-workflow-findings.mjs +4 -4
  485. package/runtime/scripts/hfs/rules/assets.d.mts +13 -0
  486. package/runtime/scripts/hfs/rules/assets.mjs +58 -46
  487. package/runtime/scripts/hfs/rules/boot-proof.d.mts +11 -0
  488. package/runtime/scripts/hfs/rules/boot-proof.mjs +7 -8
  489. package/runtime/scripts/hfs/rules/bypass-markers.d.mts +9 -0
  490. package/runtime/scripts/hfs/rules/bypass-markers.mjs +7 -12
  491. package/runtime/scripts/hfs/rules/canon-copy-drift.d.mts +10 -0
  492. package/runtime/scripts/hfs/rules/canon-copy-drift.mjs +70 -68
  493. package/runtime/scripts/hfs/rules/ci-custody.d.mts +7 -0
  494. package/runtime/scripts/hfs/rules/ci-custody.mjs +72 -73
  495. package/runtime/scripts/hfs/rules/ci-standard.d.mts +12 -0
  496. package/runtime/scripts/hfs/rules/ci-standard.mjs +149 -142
  497. package/runtime/scripts/hfs/rules/cli.d.mts +7 -0
  498. package/runtime/scripts/hfs/rules/cli.mjs +24 -23
  499. package/runtime/scripts/hfs/rules/contract-compat.d.mts +11 -0
  500. package/runtime/scripts/hfs/rules/contract-compat.mjs +64 -53
  501. package/runtime/scripts/hfs/rules/contract.d.mts +26 -0
  502. package/runtime/scripts/hfs/rules/contract.mjs +126 -111
  503. package/runtime/scripts/hfs/rules/coverage-below.d.mts +17 -0
  504. package/runtime/scripts/hfs/rules/coverage-below.mjs +200 -167
  505. package/runtime/scripts/hfs/rules/database-access.d.mts +12 -0
  506. package/runtime/scripts/hfs/rules/database-access.mjs +231 -202
  507. package/runtime/scripts/hfs/rules/database-baseline.d.mts +22 -0
  508. package/runtime/scripts/hfs/rules/database-baseline.mjs +104 -104
  509. package/runtime/scripts/hfs/rules/database-config.d.mts +11 -0
  510. package/runtime/scripts/hfs/rules/database-config.mjs +101 -135
  511. package/runtime/scripts/hfs/rules/database-constants.d.mts +36 -0
  512. package/runtime/scripts/hfs/rules/database-constants.mjs +7 -7
  513. package/runtime/scripts/hfs/rules/database-migrations.d.mts +41 -0
  514. package/runtime/scripts/hfs/rules/database-migrations.mjs +428 -388
  515. package/runtime/scripts/hfs/rules/database-plpgsql.d.mts +5 -0
  516. package/runtime/scripts/hfs/rules/database-plpgsql.mjs +278 -259
  517. package/runtime/scripts/hfs/rules/database-queue-sources.d.mts +4 -0
  518. package/runtime/scripts/hfs/rules/database-queue-sources.mjs +48 -43
  519. package/runtime/scripts/hfs/rules/database-queues.d.mts +15 -0
  520. package/runtime/scripts/hfs/rules/database-queues.mjs +211 -182
  521. package/runtime/scripts/hfs/rules/database-sql.d.mts +16 -0
  522. package/runtime/scripts/hfs/rules/database-sql.mjs +394 -394
  523. package/runtime/scripts/hfs/rules/database-transaction.d.mts +3 -0
  524. package/runtime/scripts/hfs/rules/database-transaction.mjs +25 -27
  525. package/runtime/scripts/hfs/rules/database.d.mts +22 -0
  526. package/runtime/scripts/hfs/rules/database.mjs +42 -58
  527. package/runtime/scripts/hfs/rules/deploy-scan.d.mts +10 -0
  528. package/runtime/scripts/hfs/rules/deploy-scan.mjs +177 -168
  529. package/runtime/scripts/hfs/rules/deploy-workflow.d.mts +3 -0
  530. package/runtime/scripts/hfs/rules/deploy-workflow.mjs +194 -171
  531. package/runtime/scripts/hfs/rules/deps.d.mts +6 -0
  532. package/runtime/scripts/hfs/rules/deps.mjs +61 -60
  533. package/runtime/scripts/hfs/rules/doc-fresh.d.mts +11 -0
  534. package/runtime/scripts/hfs/rules/doc-fresh.mjs +242 -223
  535. package/runtime/scripts/hfs/rules/docker-health.d.mts +9 -0
  536. package/runtime/scripts/hfs/rules/docker-health.mjs +222 -196
  537. package/runtime/scripts/hfs/rules/docker.d.mts +13 -0
  538. package/runtime/scripts/hfs/rules/docker.mjs +234 -208
  539. package/runtime/scripts/hfs/rules/edition.d.mts +18 -0
  540. package/runtime/scripts/hfs/rules/edition.mjs +103 -101
  541. package/runtime/scripts/hfs/rules/env-parity.d.mts +25 -0
  542. package/runtime/scripts/hfs/rules/env-parity.mjs +278 -252
  543. package/runtime/scripts/hfs/rules/env-value-shape.d.mts +21 -0
  544. package/runtime/scripts/hfs/rules/env-value-shape.mjs +82 -67
  545. package/runtime/scripts/hfs/rules/event-bus.d.mts +17 -0
  546. package/runtime/scripts/hfs/rules/event-bus.mjs +145 -133
  547. package/runtime/scripts/hfs/rules/event-tables.d.mts +7 -0
  548. package/runtime/scripts/hfs/rules/event-tables.mjs +77 -75
  549. package/runtime/scripts/hfs/rules/fe-build-args.d.mts +10 -0
  550. package/runtime/scripts/hfs/rules/fe-build-args.mjs +116 -99
  551. package/runtime/scripts/hfs/rules/fe-contract-documents.d.mts +7 -0
  552. package/runtime/scripts/hfs/rules/fe-contract-documents.mjs +121 -111
  553. package/runtime/scripts/hfs/rules/fe-no-tests.d.mts +9 -0
  554. package/runtime/scripts/hfs/rules/fe-no-tests.mjs +37 -36
  555. package/runtime/scripts/hfs/rules/fe-stack.d.mts +23 -0
  556. package/runtime/scripts/hfs/rules/fe-stack.mjs +108 -105
  557. package/runtime/scripts/hfs/rules/feature-files.d.mts +10 -0
  558. package/runtime/scripts/hfs/rules/feature-files.mjs +14 -19
  559. package/runtime/scripts/hfs/rules/frontend-tree.d.mts +18 -0
  560. package/runtime/scripts/hfs/rules/frontend-tree.mjs +105 -91
  561. package/runtime/scripts/hfs/rules/integration-specs.d.mts +7 -0
  562. package/runtime/scripts/hfs/rules/integration-specs.mjs +190 -177
  563. package/runtime/scripts/hfs/rules/kinds.d.mts +6 -0
  564. package/runtime/scripts/hfs/rules/kinds.mjs +88 -81
  565. package/runtime/scripts/hfs/rules/lexicon.d.mts +9 -0
  566. package/runtime/scripts/hfs/rules/lexicon.mjs +43 -40
  567. package/runtime/scripts/hfs/rules/lint-suppression.d.mts +5 -0
  568. package/runtime/scripts/hfs/rules/lint-suppression.mjs +22 -20
  569. package/runtime/scripts/hfs/rules/lite-proof-lane.d.mts +9 -0
  570. package/runtime/scripts/hfs/rules/lite-proof-lane.mjs +59 -58
  571. package/runtime/scripts/hfs/rules/lite-secrets.d.mts +12 -0
  572. package/runtime/scripts/hfs/rules/lite-secrets.mjs +60 -63
  573. package/runtime/scripts/hfs/rules/mcp-tools.d.mts +7 -0
  574. package/runtime/scripts/hfs/rules/mcp-tools.mjs +56 -41
  575. package/runtime/scripts/hfs/rules/monorepo.d.mts +17 -0
  576. package/runtime/scripts/hfs/rules/monorepo.mjs +153 -138
  577. package/runtime/scripts/hfs/rules/peer-integrations.d.mts +7 -0
  578. package/runtime/scripts/hfs/rules/peer-integrations.mjs +39 -36
  579. package/runtime/scripts/hfs/rules/pipeline.d.mts +7 -0
  580. package/runtime/scripts/hfs/rules/pipeline.mjs +53 -53
  581. package/runtime/scripts/hfs/rules/proof-commands.d.mts +14 -0
  582. package/runtime/scripts/hfs/rules/proof-commands.mjs +52 -41
  583. package/runtime/scripts/hfs/rules/read.d.mts +7 -0
  584. package/runtime/scripts/hfs/rules/read.mjs +19 -15
  585. package/runtime/scripts/hfs/rules/release-provenance.d.mts +12 -0
  586. package/runtime/scripts/hfs/rules/release-provenance.mjs +196 -159
  587. package/runtime/scripts/hfs/rules/repo-local-checks.d.mts +5 -0
  588. package/runtime/scripts/hfs/rules/repo-local-checks.mjs +19 -17
  589. package/runtime/scripts/hfs/rules/rule-types.d.mts +40 -0
  590. package/runtime/scripts/hfs/rules/rule-types.mjs +4 -0
  591. package/runtime/scripts/hfs/rules/saga-delivery.d.mts +2 -0
  592. package/runtime/scripts/hfs/rules/saga-delivery.mjs +58 -50
  593. package/runtime/scripts/hfs/rules/saga.d.mts +7 -0
  594. package/runtime/scripts/hfs/rules/saga.mjs +151 -145
  595. package/runtime/scripts/hfs/rules/script-hygiene.d.mts +11 -0
  596. package/runtime/scripts/hfs/rules/script-hygiene.mjs +112 -95
  597. package/runtime/scripts/hfs/rules/secret-custody-only.d.mts +7 -0
  598. package/runtime/scripts/hfs/rules/secret-custody-only.mjs +29 -35
  599. package/runtime/scripts/hfs/rules/secrets.d.mts +29 -0
  600. package/runtime/scripts/hfs/rules/secrets.mjs +45 -44
  601. package/runtime/scripts/hfs/rules/services.d.mts +7 -0
  602. package/runtime/scripts/hfs/rules/services.mjs +148 -148
  603. package/runtime/scripts/hfs/rules/spec-placement.d.mts +8 -0
  604. package/runtime/scripts/hfs/rules/spec-placement.mjs +12 -13
  605. package/runtime/scripts/hfs/rules/stack-tooling.d.mts +11 -0
  606. package/runtime/scripts/hfs/rules/stack-tooling.mjs +45 -52
  607. package/runtime/scripts/hfs/rules/stacks-auth-cookie-domain.d.mts +30 -0
  608. package/runtime/scripts/hfs/rules/stacks-auth-cookie-domain.mjs +149 -0
  609. package/runtime/scripts/hfs/rules/stacks-deploy.d.mts +7 -0
  610. package/runtime/scripts/hfs/rules/stacks-deploy.mjs +32 -42
  611. package/runtime/scripts/hfs/rules/stacks-ingress.d.mts +16 -0
  612. package/runtime/scripts/hfs/rules/stacks-ingress.mjs +62 -64
  613. package/runtime/scripts/hfs/rules/stacks-lite-backing.d.mts +17 -0
  614. package/runtime/scripts/hfs/rules/stacks-lite-backing.mjs +111 -116
  615. package/runtime/scripts/hfs/rules/stacks-release.d.mts +26 -0
  616. package/runtime/scripts/hfs/rules/stacks-release.mjs +335 -309
  617. package/runtime/scripts/hfs/rules/stacks-runtime.d.mts +31 -0
  618. package/runtime/scripts/hfs/rules/stacks-runtime.mjs +130 -124
  619. package/runtime/scripts/hfs/rules/stacks-swarm.d.mts +5 -0
  620. package/runtime/scripts/hfs/rules/stacks-swarm.mjs +366 -344
  621. package/runtime/scripts/hfs/rules/stacks.d.mts +7 -0
  622. package/runtime/scripts/hfs/rules/stacks.mjs +37 -38
  623. package/runtime/scripts/hfs/rules/staged-specs.d.mts +10 -0
  624. package/runtime/scripts/hfs/rules/staged-specs.mjs +14 -18
  625. package/runtime/scripts/hfs/rules/supply-chain.d.mts +10 -0
  626. package/runtime/scripts/hfs/rules/supply-chain.mjs +143 -150
  627. package/runtime/scripts/hfs/rules/test-topology.d.mts +5 -0
  628. package/runtime/scripts/hfs/rules/test-topology.mjs +23 -21
  629. package/runtime/scripts/hfs/rules/text-integrity.d.mts +9 -0
  630. package/runtime/scripts/hfs/rules/text-integrity.mjs +47 -43
  631. package/runtime/scripts/hfs/rules/unused-symbol-flow.d.mts +22 -0
  632. package/runtime/scripts/hfs/rules/unused-symbol-flow.mjs +226 -204
  633. package/runtime/scripts/hfs/rules/unused-symbol-ledger.d.mts +7 -0
  634. package/runtime/scripts/hfs/rules/unused-symbol-ledger.mjs +70 -49
  635. package/runtime/scripts/hfs/rules/unused-symbol-properties.d.mts +12 -0
  636. package/runtime/scripts/hfs/rules/unused-symbol-properties.mjs +141 -126
  637. package/runtime/scripts/hfs/rules/unused-symbol-translators.d.mts +8 -0
  638. package/runtime/scripts/hfs/rules/unused-symbol-translators.mjs +160 -140
  639. package/runtime/scripts/hfs/rules/unused-symbol-values.d.mts +9 -0
  640. package/runtime/scripts/hfs/rules/unused-symbol-values.mjs +138 -125
  641. package/runtime/scripts/hfs/rules/unused-symbol.d.mts +11 -0
  642. package/runtime/scripts/hfs/rules/unused-symbol.mjs +283 -253
  643. package/runtime/scripts/hfs/rules/workflow-contexts.d.mts +8 -0
  644. package/runtime/scripts/hfs/rules/workflow-contexts.mjs +79 -67
  645. package/runtime/scripts/hfs/runtime-rules/git-triggers.d.mts +6 -0
  646. package/runtime/scripts/hfs/runtime-rules/git-triggers.mjs +36 -35
  647. package/runtime/scripts/hfs/runtime-rules/hook-shape.d.mts +10 -0
  648. package/runtime/scripts/hfs/runtime-rules/hook-shape.mjs +45 -42
  649. package/runtime/scripts/hfs/runtime-rules/source-ast.d.mts +39 -0
  650. package/runtime/scripts/hfs/runtime-rules/source-ast.mjs +130 -116
  651. package/runtime/scripts/hfs/runtime-rules/workflow-source.d.mts +5 -0
  652. package/runtime/scripts/hfs/runtime-rules/workflow-source.mjs +8 -7
  653. package/runtime/scripts/hfs/sealed-members.d.mts +22 -0
  654. package/runtime/scripts/hfs/sealed-members.mjs +79 -75
  655. package/runtime/scripts/hfs/secret-bootstrap-crypto.d.mts +12 -0
  656. package/runtime/scripts/hfs/secret-bootstrap-crypto.mjs +151 -167
  657. package/runtime/scripts/hfs/secret-document.d.mts +16 -0
  658. package/runtime/scripts/hfs/secret-document.mjs +59 -75
  659. package/runtime/scripts/hfs/secret-edit.d.mts +14 -0
  660. package/runtime/scripts/hfs/secret-edit.mjs +115 -135
  661. package/runtime/scripts/hfs/secret-errors.d.mts +4 -0
  662. package/runtime/scripts/hfs/secret-errors.mjs +19 -16
  663. package/runtime/scripts/hfs/secret-files.d.mts +20 -0
  664. package/runtime/scripts/hfs/secret-files.mjs +103 -112
  665. package/runtime/scripts/hfs/secret-init.d.mts +108 -0
  666. package/runtime/scripts/hfs/secret-init.mjs +304 -346
  667. package/runtime/scripts/hfs/secret-output.d.mts +18 -0
  668. package/runtime/scripts/hfs/secret-output.mjs +52 -80
  669. package/runtime/scripts/hfs/secret-prompt.d.mts +39 -0
  670. package/runtime/scripts/hfs/secret-prompt.mjs +108 -93
  671. package/runtime/scripts/hfs/secret-sops.d.mts +29 -0
  672. package/runtime/scripts/hfs/secret-sops.mjs +161 -199
  673. package/runtime/scripts/hfs/secret-standard.d.mts +89 -0
  674. package/runtime/scripts/hfs/secret-standard.mjs +300 -264
  675. package/runtime/scripts/hfs/secret-store.d.mts +36 -0
  676. package/runtime/scripts/hfs/secret-store.mjs +120 -136
  677. package/runtime/scripts/hfs/secret.d.mts +40 -0
  678. package/runtime/scripts/hfs/secret.mjs +254 -222
  679. package/runtime/scripts/hfs/slot-app-view.d.mts +11 -0
  680. package/runtime/scripts/hfs/slot-app-view.mjs +69 -74
  681. package/runtime/scripts/hfs/slot-classify.d.mts +75 -0
  682. package/runtime/scripts/hfs/slot-classify.mjs +42 -38
  683. package/runtime/scripts/hfs/slot-errors.d.mts +6 -0
  684. package/runtime/scripts/hfs/slot-errors.mjs +8 -7
  685. package/runtime/scripts/hfs/slot-imports.d.mts +8 -0
  686. package/runtime/scripts/hfs/slot-imports.mjs +70 -63
  687. package/runtime/scripts/hfs/slot-manifest-shape.d.mts +9 -0
  688. package/runtime/scripts/hfs/slot-manifest-shape.mjs +74 -52
  689. package/runtime/scripts/hfs/slot-match.d.mts +50 -0
  690. package/runtime/scripts/hfs/slot-match.mjs +122 -111
  691. package/runtime/scripts/hfs/slot-path.d.mts +1 -0
  692. package/runtime/scripts/hfs/slot-path.mjs +0 -2
  693. package/runtime/scripts/hfs/slot-required.d.mts +11 -0
  694. package/runtime/scripts/hfs/slot-required.mjs +65 -59
  695. package/runtime/scripts/hfs/slot-semantic-problems.d.mts +9 -0
  696. package/runtime/scripts/hfs/slot-semantic-problems.mjs +138 -112
  697. package/runtime/scripts/hfs/slot-side-problems.d.mts +3 -0
  698. package/runtime/scripts/hfs/slot-side-problems.mjs +23 -19
  699. package/runtime/scripts/hfs/slots.d.mts +188 -0
  700. package/runtime/scripts/hfs/slots.mjs +175 -152
  701. package/runtime/scripts/hfs/sql/pg-parse.d.mts +9 -0
  702. package/runtime/scripts/hfs/sql/pg-parse.mjs +25 -25
  703. package/runtime/scripts/hfs/stack-config-kit.d.mts +22 -0
  704. package/runtime/scripts/hfs/stack-config-kit.mjs +21 -17
  705. package/runtime/scripts/hfs/stack-deploy-config.d.mts +155 -0
  706. package/runtime/scripts/hfs/stack-deploy-config.mjs +345 -273
  707. package/runtime/scripts/hfs/stack-ingress-config.d.mts +74 -0
  708. package/runtime/scripts/hfs/stack-ingress-config.mjs +211 -177
  709. package/runtime/scripts/hfs/stacks-gate.d.mts +52 -0
  710. package/runtime/scripts/hfs/stacks-gate.mjs +557 -397
  711. package/runtime/scripts/hfs/starcistacks-runtime.d.mts +7 -0
  712. package/runtime/scripts/hfs/starcistacks-runtime.mjs +51 -40
  713. package/runtime/scripts/hfs/test-world-slot.d.mts +2 -0
  714. package/runtime/scripts/hfs/test-world-slot.mjs +0 -1
  715. package/runtime/scripts/hfs/trailing-slashes.d.mts +2 -0
  716. package/runtime/scripts/hfs/trailing-slashes.mjs +4 -4
  717. package/runtime/scripts/hfs/tree-findings.d.mts +9 -0
  718. package/runtime/scripts/hfs/tree-findings.mjs +18 -17
  719. package/runtime/scripts/hfs/tree.d.mts +35 -0
  720. package/runtime/scripts/hfs/tree.mjs +56 -46
  721. package/runtime/scripts/hfs/typescript-programs.d.mts +36 -0
  722. package/runtime/scripts/hfs/typescript-programs.mjs +66 -69
  723. package/runtime/scripts/hfs/view.d.mts +45 -0
  724. package/runtime/scripts/hfs/view.mjs +38 -40
  725. package/runtime/scripts/lib/bypass-markers.d.mts +15 -0
  726. package/runtime/scripts/lib/bypass-markers.mjs +210 -197
  727. package/runtime/scripts/lib/cli-arg.d.mts +67 -0
  728. package/runtime/scripts/lib/cli-arg.mjs +59 -63
  729. package/runtime/scripts/lib/command-tokens.d.mts +11 -0
  730. package/runtime/scripts/lib/command-tokens.mjs +77 -78
  731. package/runtime/scripts/lib/compose-values.d.mts +6 -0
  732. package/runtime/scripts/lib/compose-values.mjs +7 -7
  733. package/runtime/scripts/lib/css-rules.d.mts +36 -0
  734. package/runtime/scripts/lib/css-rules.mjs +181 -171
  735. package/runtime/scripts/lib/deployment-findings.d.mts +9 -0
  736. package/runtime/scripts/lib/deployment-findings.mjs +4 -4
  737. package/runtime/scripts/lib/dockerfile.d.mts +39 -0
  738. package/runtime/scripts/lib/dockerfile.mjs +131 -114
  739. package/runtime/scripts/lib/dom-tree.d.mts +7 -0
  740. package/runtime/scripts/lib/dom-tree.mjs +3 -3
  741. package/runtime/scripts/lib/env.d.mts +12 -0
  742. package/runtime/scripts/lib/env.mjs +3 -6
  743. package/runtime/scripts/lib/escape.d.mts +6 -0
  744. package/runtime/scripts/lib/escape.mjs +2 -6
  745. package/runtime/scripts/lib/event-contract.d.mts +21 -0
  746. package/runtime/scripts/lib/event-contract.mjs +73 -78
  747. package/runtime/scripts/lib/event-items.d.mts +20 -0
  748. package/runtime/scripts/lib/event-items.mjs +27 -15
  749. package/runtime/scripts/lib/example-refs.d.mts +69 -0
  750. package/runtime/scripts/lib/example-refs.mjs +145 -129
  751. package/runtime/scripts/lib/fs-kind.d.mts +12 -0
  752. package/runtime/scripts/lib/fs-kind.mjs +20 -10
  753. package/runtime/scripts/lib/git-hooks-dir.d.mts +15 -0
  754. package/runtime/scripts/lib/git-hooks-dir.mjs +16 -16
  755. package/runtime/scripts/lib/git.d.mts +24 -0
  756. package/runtime/scripts/lib/git.mjs +15 -16
  757. package/runtime/scripts/lib/glob.d.mts +4 -0
  758. package/runtime/scripts/lib/glob.mjs +23 -14
  759. package/runtime/scripts/lib/grammar-lint.d.mts +47 -0
  760. package/runtime/scripts/lib/grammar-lint.mjs +33 -37
  761. package/runtime/scripts/lib/grammar-vendor-parity-jsx.d.mts +40 -0
  762. package/runtime/scripts/lib/grammar-vendor-parity-jsx.mjs +285 -266
  763. package/runtime/scripts/lib/grammar-vendor-parity.d.mts +61 -0
  764. package/runtime/scripts/lib/grammar-vendor-parity.mjs +209 -216
  765. package/runtime/scripts/lib/graphql-contract.d.mts +53 -0
  766. package/runtime/scripts/lib/graphql-contract.mjs +201 -198
  767. package/runtime/scripts/lib/graphql-sdl.d.mts +66 -0
  768. package/runtime/scripts/lib/graphql-sdl.mjs +230 -205
  769. package/runtime/scripts/lib/hook-limits.d.mts +11 -0
  770. package/runtime/scripts/lib/hook-limits.mjs +0 -1
  771. package/runtime/scripts/lib/i18n.d.mts +48 -0
  772. package/runtime/scripts/lib/i18n.mjs +32 -35
  773. package/runtime/scripts/lib/in-order.d.mts +12 -0
  774. package/runtime/scripts/lib/in-order.mjs +57 -38
  775. package/runtime/scripts/lib/is-main.d.mts +6 -0
  776. package/runtime/scripts/lib/is-main.mjs +11 -4
  777. package/runtime/scripts/lib/js-lex.d.mts +32 -0
  778. package/runtime/scripts/lib/js-lex.mjs +196 -162
  779. package/runtime/scripts/lib/json-clone.d.mts +11 -0
  780. package/runtime/scripts/lib/json-clone.mjs +45 -36
  781. package/runtime/scripts/lib/json-schema.d.mts +2 -0
  782. package/runtime/scripts/lib/json-schema.mjs +127 -90
  783. package/runtime/scripts/lib/json.d.mts +16 -0
  784. package/runtime/scripts/lib/json.mjs +26 -14
  785. package/runtime/scripts/lib/language.d.mts +44 -0
  786. package/runtime/scripts/lib/language.mjs +133 -129
  787. package/runtime/scripts/lib/list.d.mts +17 -0
  788. package/runtime/scripts/lib/list.mjs +9 -14
  789. package/runtime/scripts/lib/loosening-budgets.d.mts +12 -0
  790. package/runtime/scripts/lib/loosening-budgets.mjs +35 -29
  791. package/runtime/scripts/lib/loosening-config.d.mts +64 -0
  792. package/runtime/scripts/lib/loosening-config.mjs +267 -236
  793. package/runtime/scripts/lib/loosening-state.d.mts +77 -0
  794. package/runtime/scripts/lib/loosening-state.mjs +134 -128
  795. package/runtime/scripts/lib/mutation-fence.d.mts +11 -0
  796. package/runtime/scripts/lib/mutation-fence.mjs +10 -5
  797. package/runtime/scripts/lib/normalize.d.mts +12 -0
  798. package/runtime/scripts/lib/normalize.mjs +11 -11
  799. package/runtime/scripts/lib/numeric-match.d.mts +2 -0
  800. package/runtime/scripts/lib/numeric-match.mjs +2 -2
  801. package/runtime/scripts/lib/package-at.d.mts +24 -0
  802. package/runtime/scripts/lib/package-at.mjs +34 -31
  803. package/runtime/scripts/lib/path-key.d.mts +76 -0
  804. package/runtime/scripts/lib/path-key.mjs +53 -38
  805. package/runtime/scripts/lib/pid-alive.d.mts +2 -0
  806. package/runtime/scripts/lib/pid-alive.mjs +10 -3
  807. package/runtime/scripts/lib/properties.d.mts +2 -0
  808. package/runtime/scripts/lib/properties.mjs +17 -11
  809. package/runtime/scripts/lib/provision-operation-schema.d.mts +4 -0
  810. package/runtime/scripts/lib/provision-operation-schema.mjs +4 -5
  811. package/runtime/scripts/lib/read-text.d.mts +12 -0
  812. package/runtime/scripts/lib/read-text.mjs +30 -17
  813. package/runtime/scripts/lib/redact.d.mts +52 -0
  814. package/runtime/scripts/lib/redact.mjs +135 -121
  815. package/runtime/scripts/lib/regex.d.mts +2 -0
  816. package/runtime/scripts/lib/regex.mjs +1 -2
  817. package/runtime/scripts/lib/roots.d.mts +32 -0
  818. package/runtime/scripts/lib/roots.mjs +5 -15
  819. package/runtime/scripts/lib/runtime-example.d.mts +1 -0
  820. package/runtime/scripts/lib/runtime-example.mjs +10 -8
  821. package/runtime/scripts/lib/same-text.d.mts +2 -0
  822. package/runtime/scripts/lib/same-text.mjs +0 -1
  823. package/runtime/scripts/lib/sealed-member.d.mts +16 -0
  824. package/runtime/scripts/lib/sealed-member.mjs +8 -13
  825. package/runtime/scripts/lib/secret-patterns.d.mts +30 -0
  826. package/runtime/scripts/lib/secret-patterns.mjs +175 -163
  827. package/runtime/scripts/lib/shell-source.d.mts +2 -0
  828. package/runtime/scripts/lib/shell-source.mjs +0 -1
  829. package/runtime/scripts/lib/sleep-sync.d.mts +2 -0
  830. package/runtime/scripts/lib/sleep-sync.mjs +5 -6
  831. package/runtime/scripts/lib/sops-envelope.d.mts +34 -0
  832. package/runtime/scripts/lib/sops-envelope.mjs +148 -120
  833. package/runtime/scripts/lib/source-phrases.d.mts +4 -0
  834. package/runtime/scripts/lib/source-phrases.mjs +13 -13
  835. package/runtime/scripts/lib/sql-ast.d.mts +13 -0
  836. package/runtime/scripts/lib/sql-ast.mjs +19 -18
  837. package/runtime/scripts/lib/sqlite.d.mts +21 -0
  838. package/runtime/scripts/lib/sqlite.mjs +3 -9
  839. package/runtime/scripts/lib/stack-declaration.d.mts +51 -0
  840. package/runtime/scripts/lib/stack-declaration.mjs +27 -20
  841. package/runtime/scripts/lib/stack-services.d.mts +73 -0
  842. package/runtime/scripts/lib/stack-services.mjs +128 -129
  843. package/runtime/scripts/lib/starciwork-boundary.d.mts +10 -0
  844. package/runtime/scripts/lib/starciwork-boundary.mjs +98 -92
  845. package/runtime/scripts/lib/swarm-stack.d.mts +24 -0
  846. package/runtime/scripts/lib/swarm-stack.mjs +112 -100
  847. package/runtime/scripts/lib/trim-end.d.mts +2 -0
  848. package/runtime/scripts/lib/trim-end.mjs +4 -4
  849. package/runtime/scripts/lib/ts-analysis.d.mts +24 -0
  850. package/runtime/scripts/lib/ts-analysis.mjs +28 -26
  851. package/runtime/scripts/lib/ts-ast.d.mts +36 -0
  852. package/runtime/scripts/lib/ts-ast.mjs +36 -42
  853. package/runtime/scripts/lib/walk.d.mts +19 -0
  854. package/runtime/scripts/lib/walk.mjs +33 -27
  855. package/runtime/scripts/lib/work-edition.d.mts +10 -0
  856. package/runtime/scripts/lib/work-edition.mjs +29 -21
  857. package/runtime/scripts/lib/workflow-events.d.mts +2 -0
  858. package/runtime/scripts/lib/workflow-events.mjs +5 -4
  859. package/runtime/scripts/lib/worktree-exclude.d.mts +16 -0
  860. package/runtime/scripts/lib/worktree-exclude.mjs +6 -8
  861. package/runtime/scripts/lib/yaml-cached.d.mts +2 -0
  862. package/runtime/scripts/lib/yaml-cached.mjs +4 -4
  863. package/runtime/scripts/machine/artifact-hold.d.mts +26 -0
  864. package/runtime/scripts/machine/artifact-hold.mjs +22 -23
  865. package/runtime/scripts/work/asset-slot.d.mts +96 -0
  866. package/runtime/scripts/work/asset-slot.mjs +201 -175
  867. package/runtime/scripts/work/draw/draw-dna-anatomy.d.mts +30 -0
  868. package/runtime/scripts/work/draw/draw-dna-anatomy.mjs +40 -36
  869. package/runtime/scripts/work/draw/draw-dna-findings.d.mts +12 -0
  870. package/runtime/scripts/work/draw/draw-dna-findings.mjs +240 -206
  871. package/runtime/scripts/work/draw/draw-dna-selector.d.mts +7 -0
  872. package/runtime/scripts/work/draw/draw-dna-selector.mjs +28 -28
  873. package/runtime/scripts/work/draw/draw-dna.d.mts +191 -0
  874. package/runtime/scripts/work/draw/draw-dna.mjs +283 -249
  875. package/runtime/scripts/work/grammar-proposal.d.mts +83 -0
  876. package/runtime/scripts/work/grammar-proposal.mjs +215 -178
  877. package/runtime/scripts/work/work-io.d.mts +62 -0
  878. package/runtime/scripts/work/work-io.mjs +82 -63
  879. package/scaffold/add-app.d.mts +15 -0
  880. package/scaffold/add-app.mjs +111 -163
  881. package/scaffold/add-cli-lite.d.mts +8 -0
  882. package/scaffold/add-cli-lite.mjs +173 -207
  883. package/scaffold/add-compose.d.mts +44 -0
  884. package/scaffold/add-compose.mjs +62 -62
  885. package/scaffold/add-database-lite.d.mts +2 -0
  886. package/scaffold/add-database-lite.mjs +10 -10
  887. package/scaffold/add-fe-layout.d.mts +16 -0
  888. package/scaffold/add-fe-layout.mjs +23 -23
  889. package/scaffold/add-fe-overlay.d.mts +17 -0
  890. package/scaffold/add-fe-overlay.mjs +76 -78
  891. package/scaffold/add-fe-page.d.mts +41 -0
  892. package/scaffold/add-fe-page.mjs +84 -89
  893. package/scaffold/add-fe-unit.d.mts +59 -0
  894. package/scaffold/add-fe-unit.mjs +205 -197
  895. package/scaffold/add-inbox-lite.d.mts +17 -0
  896. package/scaffold/add-inbox-lite.mjs +209 -264
  897. package/scaffold/add-plan.d.mts +68 -0
  898. package/scaffold/add-plan.mjs +164 -149
  899. package/scaffold/add-queue-lite.d.mts +34 -0
  900. package/scaffold/add-queue-lite.mjs +267 -267
  901. package/scaffold/add-table.d.mts +16 -0
  902. package/scaffold/add-table.mjs +405 -442
  903. package/scaffold/add.d.mts +34 -0
  904. package/scaffold/add.mjs +260 -246
  905. package/scaffold/app.d.mts +198 -0
  906. package/scaffold/app.mjs +413 -391
  907. package/scaffold/edition-gate.d.mts +49 -0
  908. package/scaffold/edition-gate.mjs +19 -29
  909. package/scaffold/image.d.mts +4 -0
  910. package/scaffold/image.mjs +12 -12
  911. package/scaffold/lite-exports.d.mts +15 -0
  912. package/scaffold/lite-exports.mjs +262 -255
  913. package/scaffold/pattern-file-slots.d.mts +7 -0
  914. package/scaffold/pattern-file-slots.mjs +29 -24
  915. package/scaffold/service.d.mts +60 -0
  916. package/scaffold/service.mjs +297 -279
  917. package/scaffold/test-world.d.mts +31 -0
  918. package/scaffold/test-world.mjs +71 -78
  919. package/scaffold/yaml-block-edit.d.mts +23 -0
  920. package/scaffold/yaml-block-edit.mjs +95 -87
  921. package/scripts/sync-bundles.d.mts +31 -0
  922. package/src/fixtures/bypass-index.d.mts +4 -0
  923. package/src/fixtures/hfs-stack-tooling/passing.d.mts +8 -0
  924. package/src/fixtures/hfs-stack-tooling/violating.d.mts +19 -0
  925. package/src/fixtures/hfs-stacks-full/model-unavailable.d.mts +10 -0
  926. package/src/fixtures/hfs-stacks-full/passing.d.mts +19 -0
  927. package/src/fixtures/hfs-stacks-full/violating.d.mts +10 -0
  928. package/src/fixtures/loosening/adversarial.d.mts +4 -0
  929. package/src/fixtures/loosening/forms.d.mts +16 -0
  930. package/src/fixtures/loosening/swallows.d.mts +5 -0
  931. package/src/helpers/hfs-arch-fixture.d.mts +47 -0
  932. package/src/helpers/hfs-cli-fixture.d.mts +65 -0
  933. package/src/helpers/hfs-hfs-docker-rules-fixture.d.mts +5 -0
  934. package/src/helpers/hfs-secret-fixture.d.mts +19 -0
  935. package/src/helpers/pre-push-fixture.d.mts +21 -0
  936. package/src/helpers/stack-compose.d.mts +6 -0
  937. package/src/helpers/swarm-stack-fixture.d.mts +18 -0
  938. package/src/helpers/tmpdir.d.mts +2 -0
  939. package/src/main.d.mts +58 -0
  940. package/src/main.mjs +493 -348
  941. package/stack/age-install.d.mts +13 -0
  942. package/stack/age-install.mjs +28 -25
  943. package/stack/backup.d.mts +11 -0
  944. package/stack/backup.mjs +76 -69
  945. package/stack/build-args.d.mts +7 -0
  946. package/stack/build-args.mjs +22 -23
  947. package/stack/build.d.mts +8 -0
  948. package/stack/build.mjs +138 -119
  949. package/stack/cli.d.mts +55 -0
  950. package/stack/cli.mjs +187 -130
  951. package/stack/config.d.mts +197 -0
  952. package/stack/config.mjs +47 -33
  953. package/stack/context.d.mts +122 -0
  954. package/stack/context.mjs +63 -65
  955. package/stack/converge.d.mts +13 -0
  956. package/stack/converge.mjs +73 -69
  957. package/stack/deploy.d.mts +88 -0
  958. package/stack/deploy.mjs +272 -224
  959. package/stack/digest.d.mts +8 -0
  960. package/stack/digest.mjs +1 -6
  961. package/stack/docker-transport.d.mts +5 -0
  962. package/stack/docker-transport.mjs +101 -93
  963. package/stack/edge-alias.d.mts +42 -0
  964. package/stack/edge-alias.mjs +62 -60
  965. package/stack/errors.d.mts +18 -0
  966. package/stack/errors.mjs +8 -10
  967. package/stack/image-scan.d.mts +20 -0
  968. package/stack/image-scan.mjs +114 -101
  969. package/stack/ingress-create.d.mts +4 -0
  970. package/stack/ingress-create.mjs +82 -86
  971. package/stack/ingress-publish.d.mts +6 -0
  972. package/stack/ingress-publish.mjs +194 -182
  973. package/stack/ingress.d.mts +55 -0
  974. package/stack/ingress.mjs +534 -524
  975. package/stack/jobs.d.mts +12 -0
  976. package/stack/jobs.mjs +35 -34
  977. package/stack/live-restore.d.mts +35 -0
  978. package/stack/live-restore.mjs +120 -125
  979. package/stack/post-deploy.d.mts +8 -0
  980. package/stack/post-deploy.mjs +25 -25
  981. package/stack/prepare.d.mts +24 -0
  982. package/stack/prepare.mjs +213 -200
  983. package/stack/private-file.d.mts +5 -0
  984. package/stack/private-file.mjs +9 -9
  985. package/stack/provision.d.mts +25 -0
  986. package/stack/provision.mjs +64 -63
  987. package/stack/prune.d.mts +11 -0
  988. package/stack/prune.mjs +38 -27
  989. package/stack/release-store.d.mts +28 -0
  990. package/stack/release-store.mjs +68 -67
  991. package/stack/remote.d.mts +83 -0
  992. package/stack/remote.mjs +176 -162
  993. package/stack/render.d.mts +14 -0
  994. package/stack/render.mjs +132 -124
  995. package/stack/retire.d.mts +9 -0
  996. package/stack/retire.mjs +31 -34
  997. package/stack/sealed.d.mts +46 -0
  998. package/stack/sealed.mjs +78 -82
  999. package/stack/server-key.d.mts +67 -0
  1000. package/stack/server-key.mjs +217 -203
  1001. package/stack/ssh-session.d.mts +29 -0
  1002. package/stack/ssh-session.mjs +133 -123
  1003. package/stack/target-binding.d.mts +21 -0
  1004. package/stack/target-binding.mjs +68 -44
  1005. package/stack/task.d.mts +12 -0
  1006. package/stack/task.mjs +26 -31
  1007. package/stack/urgent-floor.d.mts +106 -0
  1008. package/stack/urgent-floor.mjs +211 -160
  1009. package/stack/urgent-release.d.mts +38 -0
  1010. package/stack/urgent-release.mjs +26 -22
  1011. package/stack/verify.d.mts +14 -0
  1012. package/stack/verify.mjs +75 -65
  1013. package/sync/ci-commands.d.mts +26 -0
  1014. package/sync/ci-commands.mjs +181 -182
  1015. package/sync/cli.d.mts +8 -0
  1016. package/sync/cli.mjs +9 -9
  1017. package/sync/format.d.mts +38 -0
  1018. package/sync/format.mjs +30 -24
  1019. package/sync/hygiene.d.mts +23 -0
  1020. package/sync/hygiene.mjs +113 -99
  1021. package/sync/index.d.mts +237 -0
  1022. package/sync/index.mjs +457 -436
  1023. package/sync/managed.d.mts +23 -0
  1024. package/sync/managed.mjs +104 -94
  1025. package/sync/quality-services.d.mts +35 -0
  1026. package/sync/quality-services.mjs +48 -41
  1027. package/sync/readme-badges.d.mts +13 -0
  1028. package/sync/readme-badges.mjs +45 -44
  1029. package/sync/sonar-key.d.mts +31 -0
  1030. package/sync/sonar-key.mjs +29 -28
  1031. package/sync/stack-images.d.mts +36 -0
  1032. package/sync/stack-images.mjs +85 -95
  1033. package/sync/ts-strict.d.mts +14 -0
  1034. package/sync/ts-strict.mjs +43 -34
  1035. package/templates/app/ci-workflows/github/workflows/ci.yml +1 -1
  1036. package/templates/app/ci-workflows/github/workflows/images.yml +1 -1
  1037. package/templates/app/ci-workflows/github/workflows/stack-deploy.yml +2 -2
  1038. package/templates/app/ci-workflows/github/workflows/stack-key-restore.yml +1 -1
  1039. package/templates/app/ci-workflows-lite/github/workflows/ci.yml +1 -1
  1040. package/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml +3 -3
  1041. package/templates/app/ci-workflows-lite/github/workflows/images.yml +1 -1
  1042. package/templates/app/ci-workflows-lite/github/workflows/stack-deploy.yml +2 -2
  1043. package/templates/app/ci-workflows-lite/github/workflows/stack-key-restore.yml +1 -1
  1044. package/templates/app/codegen-lite/scripts/codegen.mjs +1 -1
  1045. package/templates/app/db-bootstrap/db/bootstrap.sql +1 -1
  1046. package/templates/app/db-image/starcistacks/images/db/Dockerfile +1 -1
  1047. package/templates/app/hooks/husky/pre-commit +2 -2
  1048. package/templates/app/package-scripts/db.json +3 -3
  1049. package/templates/app/package-scripts/package.json +1 -1
  1050. package/templates/app/quality-config/sonar-project.properties +1 -1
  1051. package/templates/app/skeleton/.starcistacks/application-stacks.yaml +2 -2
  1052. package/templates/app/skeleton/.starcistacks/production/README.md +4 -4
  1053. package/templates/app/skeleton/.starcistacks/production/infra/swarm/stack.yaml +8 -3
  1054. package/templates/app/skeleton/README.md +1 -1
  1055. package/templates/app/skeleton-lite/.starcistacks/application-stacks.yaml +2 -2
  1056. package/templates/app/skeleton-lite/.starcistacks/dev/README.md +1 -1
  1057. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/stack.yaml +6 -2
  1058. package/templates/app/skeleton-lite/.starcistacks/dev/runtime/env/KEYS.md +3 -2
  1059. package/templates/app/skeleton-lite/.starcistacks/production/README.md +5 -5
  1060. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/stack.yaml +7 -2
  1061. package/templates/app/skeleton-lite/.starcistacks/production/runtime/env/KEYS.md +3 -2
  1062. package/templates/app/skeleton-lite/README.md +2 -2
  1063. package/templates/be/skeleton/apps/cli/src/compose/platform.ts +1 -1
  1064. package/templates/be/skeleton/apps/cli/src/main.ts +6 -4
  1065. package/templates/be/skeleton/src/modules/platform/config/env-source.config.ts +12 -0
  1066. package/templates/be/skeleton/src/modules/platform/database/abstract.entity.ts +19 -0
  1067. package/templates/be/skeleton/src/modules/platform/database/database.module.ts +7 -1
  1068. package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.spec.ts +19 -0
  1069. package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.ts +4 -3
  1070. package/templates/be/skeleton/src/modules/platform/logging/logging.module.ts +1 -1
  1071. package/templates/be/skeleton/src/modules/platform/logging/logging.options.ts +5 -2
  1072. package/templates/be/skeleton-lite/apps/cli/src/compose/platform.ts +1 -1
  1073. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.config.spec.ts +40 -0
  1074. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.config.ts +1 -0
  1075. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.cookie.spec.ts +48 -1
  1076. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.cookie.ts +5 -0
  1077. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.options.ts +5 -0
  1078. package/templates/be/skeleton-lite/src/modules/integrations/passport/strategies/google.strategy.ts +1 -1
  1079. package/templates/be/skeleton-lite/src/modules/integrations/passport/strategies/oauth-state.store.ts +7 -3
  1080. package/templates/be/skeleton-lite/src/modules/platform/database/database.module.ts +7 -1
  1081. package/templates/be/table/entity.ts.tpl +4 -15
  1082. package/upgrade/check.d.mts +9 -0
  1083. package/upgrade/check.mjs +8 -8
  1084. package/upgrade/file-state.d.mts +6 -0
  1085. package/upgrade/file-state.mjs +32 -40
  1086. package/upgrade/index.d.mts +13 -0
  1087. package/upgrade/index.mjs +122 -151
  1088. package/vendor-types.d.mts +8 -0
package/stack/ingress.mjs CHANGED
@@ -20,7 +20,7 @@ import { refusal } from './errors.mjs';
20
20
  import { remoteDocker, shellQuote, withRunner } from './remote.mjs';
21
21
  import { createIngress } from './ingress-create.mjs';
22
22
  import { publishIngress } from './ingress-publish.mjs';
23
-
23
+ const declaresBackup = (ingress) => ingress.backup !== null;
24
24
  const MATCHER = /(Host|HostRegexp)\(([^)]*)\)/gu;
25
25
  const QUOTED = /`([^`]*)`/gu;
26
26
  const FILE_MARK = /^#@@FILE (.+)$/mu;
@@ -29,678 +29,688 @@ const SETTLE_MS = 2000;
29
29
  const START_ATTEMPTS = 6;
30
30
  const AGE_RECIPIENT = /age1[0-9a-z]{50,}/u;
31
31
  const MOUNTPOINT = /^\/[A-Za-z0-9._/-]+$/u;
32
-
33
32
  const asMap = (value) => (plain(value) ? value : {});
34
33
  const sorted = (items) => [...items].sort(byCodeUnit);
35
34
  const originOf = (url) => {
36
- try {
37
- const parsed = new URL(url);
38
- return `${parsed.protocol}//${parsed.host}`;
39
- } catch {
40
- return '[unparsed]';
41
- }
35
+ try {
36
+ const parsed = new URL(url);
37
+ return `${parsed.protocol}//${parsed.host}`;
38
+ }
39
+ catch {
40
+ return '[unparsed]';
41
+ }
42
42
  };
43
-
44
43
  // ---- the live and the declared routes -------------------------------------------------------------------------------------------
45
-
46
44
  /** Host keys of a proxy router rule: `a.example` for Host(...), `regexp:<pattern>` for HostRegexp(...). */
47
45
  function hostsOfRule(rule) {
48
- const out = [];
49
- for (const [, kind, args] of String(rule).matchAll(MATCHER)) {
50
- for (const [, value] of args.matchAll(QUOTED)) out.push(kind === 'Host' ? value.toLowerCase() : `regexp:${value}`);
51
- }
52
- return out;
46
+ const out = [];
47
+ for (const [, kind = '', args = ''] of String(rule).matchAll(MATCHER)) {
48
+ for (const [, value = ''] of args.matchAll(QUOTED))
49
+ out.push(kind === 'Host' ? value.toLowerCase() : `regexp:${value}`);
50
+ }
51
+ return out;
53
52
  }
54
-
55
53
  /** { routes: [{ host, targets }], middlewares: [name] } of one parsed file-provider dynamic document (any shape is tolerated). */
56
54
  function routesOfDocument(doc) {
57
- const http = asMap(asMap(doc).http);
58
- const services = asMap(http.services);
59
- const targetsOf = (name) => {
60
- const servers = asMap(asMap(services[String(name).split('@')[0]]).loadBalancer).servers;
61
- return (Array.isArray(servers) ? servers : []).map((server) => originOf(String(asMap(server).url)));
62
- };
63
- const routes = Object.values(asMap(http.routers)).flatMap((router) => {
64
- const targets = typeof asMap(router).service === 'string' ? targetsOf(router.service) : [];
65
- return hostsOfRule(asMap(router).rule ?? '').map((host) => ({ host, targets }));
66
- });
67
- return { routes, middlewares: Object.keys(asMap(http.middlewares)) };
68
- }
69
-
55
+ const http = asMap(asMap(doc).http);
56
+ const services = asMap(http.services);
57
+ const targetsOf = (name) => {
58
+ const servers = asMap(asMap(services[String(name).split('@')[0]]).loadBalancer).servers;
59
+ return (Array.isArray(servers) ? servers : []).map((server) => originOf(String(asMap(server).url)));
60
+ };
61
+ const routes = Object.values(asMap(http.routers)).flatMap((router) => {
62
+ const own = asMap(router);
63
+ const targets = typeof own.service === 'string' ? targetsOf(own.service) : [];
64
+ return hostsOfRule(own.rule ?? '').map((host) => ({ host, targets }));
65
+ });
66
+ return { routes, middlewares: Object.keys(asMap(http.middlewares)) };
67
+ }
70
68
  function liveCommand(dir) {
71
- const loop = String.raw`for f in *.yml *.yaml; do if [ -f "$f" ]; then printf '#@@FILE %s\n' "$f"; cat "$f"; echo; fi; done`;
72
- return `cd ${shellQuote(dir)} && ${loop}`;
69
+ const loop = String.raw `for f in *.yml *.yaml; do if [ -f "$f" ]; then printf '#@@FILE %s\n' "$f"; cat "$f"; echo; fi; done`;
70
+ return `cd ${shellQuote(dir)} && ${loop}`;
73
71
  }
74
-
75
72
  /** Reads every route file of the old edge: { routes, files, unparsable }. */
76
73
  function readLive(run, legacy) {
77
- const result = run(liveCommand(legacy.dynamicDir));
78
- if (result.status !== 0) return { error: `cannot read ${legacy.dynamicDir} on the manager: ${firstLine(result.stderr)}` };
79
- const parts = String(result.stdout).split(FILE_MARK);
80
- const live = { routes: [], files: [], unparsable: [] };
81
- for (let index = 1; index < parts.length; index += 2) {
82
- live.files.push(parts[index]);
83
- try {
84
- live.routes.push(...routesOfDocument(parseYaml(parts[index + 1])).routes);
85
- } catch {
86
- live.unparsable.push(parts[index]);
74
+ const result = run(liveCommand(legacy.dynamicDir));
75
+ if (result.status !== 0)
76
+ return { error: `cannot read ${legacy.dynamicDir} on the manager: ${firstLine(result.stderr)}` };
77
+ const parts = String(result.stdout).split(FILE_MARK);
78
+ const live = { routes: [], files: [], unparsable: [] };
79
+ for (let index = 1; index < parts.length; index += 2) {
80
+ const name = parts[index];
81
+ live.files.push(name);
82
+ try {
83
+ live.routes.push(...routesOfDocument(parseYaml(parts[index + 1])).routes);
84
+ }
85
+ catch {
86
+ live.unparsable.push(name);
87
+ }
87
88
  }
88
- }
89
- return live;
89
+ return live;
90
90
  }
91
-
92
91
  /** The app's own routes file (relative to the environment dir): { routes, middlewares } or an empty set. */
93
92
  function readOwnRoutes(config, ingress) {
94
- if (ingress.routes === null) return { routes: [], middlewares: [] };
95
- try {
96
- return routesOfDocument(parseYaml(fs.readFileSync(path.join(config.envDir, ingress.routes), 'utf8')));
97
- } catch {
98
- throw refusal('STACK_INGRESS_ROUTES', `cannot read ${ingress.routes} (deploy.ingress.routes) as YAML`);
99
- }
100
- }
101
-
93
+ if (ingress.routes === null)
94
+ return { routes: [], middlewares: [] };
95
+ try {
96
+ return routesOfDocument(parseYaml(fs.readFileSync(path.join(config.envDir, ingress.routes), 'utf8')));
97
+ }
98
+ catch {
99
+ throw refusal('STACK_INGRESS_ROUTES', `cannot read ${ingress.routes} (deploy.ingress.routes) as YAML`);
100
+ }
101
+ }
102
102
  /** Declared host -> where it is declared (`upstream <name>` or `routes file`). */
103
103
  function declaredHosts(ingress, own) {
104
- const out = new Map();
105
- for (const entry of own.routes) out.set(entry.host, 'routes file');
106
- for (const host of ingress.drop) out.set(host, 'dropped');
107
- for (const entry of ingress.redirects) out.set(entry.host, `redirect to ${entry.to}`);
108
- for (const entry of ingress.upstreams) for (const host of entry.hosts) out.set(host, `upstream ${entry.name}`);
109
- return out;
110
- }
111
-
104
+ const out = new Map();
105
+ for (const entry of own.routes)
106
+ out.set(entry.host, 'routes file');
107
+ for (const host of ingress.drop)
108
+ out.set(host, 'dropped');
109
+ for (const entry of ingress.redirects)
110
+ out.set(entry.host, `redirect to ${entry.to}`);
111
+ for (const entry of ingress.upstreams)
112
+ for (const host of entry.hosts)
113
+ out.set(host, `upstream ${entry.name}`);
114
+ return out;
115
+ }
112
116
  function undeclaredMiddlewares(ingress, own) {
113
- const known = new Set([...Object.keys(ingress.middlewares), ...own.middlewares]);
114
- return ingress.upstreams.flatMap((entry) => entry.middlewares.filter((name) => !known.has(name)).map((name) => `${name} (upstream ${entry.name})`));
117
+ const known = new Set([...Object.keys(ingress.middlewares), ...own.middlewares]);
118
+ return ingress.upstreams.flatMap((entry) => entry.middlewares.filter((name) => !known.has(name)).map((name) => `${name} (upstream ${entry.name})`));
115
119
  }
116
-
117
120
  // ---- docker probes over ssh ---------------------------------------------------------------------------------------------------
118
-
119
121
  function containerState(run, container, network) {
120
- const inspect = remoteDocker(run, ['inspect', '--type', 'container', '--format', '{{.State.Running}}|{{json .NetworkSettings.Networks}}', container]);
121
- if (inspect.status !== 0) return { present: false, attached: false };
122
- const [running, json = 'null'] = String(inspect.stdout).trim().split(/\|(.*)/su);
123
- let networks = null;
124
- try { networks = JSON.parse(json); } catch { networks = null; }
125
- return { present: running === 'true', attached: plain(networks) && Object.hasOwn(networks, network) };
126
- }
127
-
122
+ const inspect = remoteDocker(run, ['inspect', '--type', 'container', '--format', '{{.State.Running}}|{{json .NetworkSettings.Networks}}', container]);
123
+ if (inspect.status !== 0)
124
+ return { present: false, attached: false };
125
+ const [running, json = 'null'] = String(inspect.stdout).trim().split(/\|(.*)/su);
126
+ let networks = null;
127
+ try {
128
+ networks = JSON.parse(json);
129
+ }
130
+ catch {
131
+ networks = null;
132
+ }
133
+ return { present: running === 'true', attached: plain(networks) && Object.hasOwn(networks, network) };
134
+ }
128
135
  /** 'missing' | 'wrong' (exists, not an attachable overlay) | 'ok'. */
129
136
  function networkState(run, network) {
130
- const inspect = remoteDocker(run, ['network', 'inspect', '--format', '{{.Driver}} {{.Attachable}}', network]);
131
- if (inspect.status !== 0) return 'missing';
132
- return String(inspect.stdout).trim() === 'overlay true' ? 'ok' : 'wrong';
137
+ const inspect = remoteDocker(run, ['network', 'inspect', '--format', '{{.Driver}} {{.Attachable}}', network]);
138
+ if (inspect.status !== 0)
139
+ return 'missing';
140
+ return String(inspect.stdout).trim() === 'overlay true' ? 'ok' : 'wrong';
133
141
  }
134
-
135
142
  const onOverlay = (ingress) => ingress.upstreams.filter((entry) => entry.container !== null);
136
-
137
143
  const volumePresent = (run, volume) => remoteDocker(run, ['volume', 'inspect', volume]).status === 0;
138
-
139
144
  // ---- plan ---------------------------------------------------------------------------------------------------------------------------
140
-
141
145
  function gather(run, config, ingress) {
142
- const own = readOwnRoutes(config, ingress);
143
- const live = readLive(run, ingress.legacy);
144
- if (live.error) return { error: live.error };
145
- const declared = declaredHosts(ingress, own);
146
- const liveHosts = new Map();
147
- for (const entry of live.routes) liveHosts.set(entry.host, sorted(new Set([...(liveHosts.get(entry.host) ?? []), ...entry.targets])));
148
- const containers = Object.fromEntries(onOverlay(ingress).map((entry) => [entry.container, containerState(run, entry.container, ingress.network)]));
149
- return {
150
- declared, liveHosts, live, own,
151
- missing: sorted([...liveHosts.keys()].filter((host) => !declared.has(host))),
152
- declaredNotLive: sorted([...declared.keys()].filter((host) => !liveHosts.has(host))),
153
- badMiddlewares: undeclaredMiddlewares(ingress, own),
154
- containers,
155
- network: networkState(run, ingress.network),
156
- certificates: volumePresent(run, ingress.acmeVolume),
157
- };
158
- }
159
-
146
+ const own = readOwnRoutes(config, ingress);
147
+ const live = readLive(run, ingress.legacy);
148
+ if (live.error !== undefined)
149
+ return { error: live.error };
150
+ const declared = declaredHosts(ingress, own);
151
+ const liveHosts = new Map();
152
+ for (const entry of live.routes)
153
+ liveHosts.set(entry.host, sorted(new Set([...(liveHosts.get(entry.host) ?? []), ...entry.targets])));
154
+ const containers = Object.fromEntries(onOverlay(ingress).map((entry) => [entry.container, containerState(run, entry.container, ingress.network)])); // onOverlay keeps only entries with a container
155
+ return {
156
+ declared, liveHosts, live, own,
157
+ missing: sorted([...liveHosts.keys()].filter((host) => !declared.has(host))),
158
+ declaredNotLive: sorted([...declared.keys()].filter((host) => !liveHosts.has(host))),
159
+ badMiddlewares: undeclaredMiddlewares(ingress, own),
160
+ containers,
161
+ network: networkState(run, ingress.network),
162
+ certificates: volumePresent(run, ingress.acmeVolume),
163
+ };
164
+ }
160
165
  function containerText(state) {
161
- if (!state.present) return 'MISSING or not running';
162
- return state.attached ? 'running, attached' : 'running, not attached';
166
+ if (!state.present)
167
+ return 'MISSING or not running';
168
+ return state.attached ? 'running, attached' : 'running, not attached';
163
169
  }
164
-
165
170
  function planLines(ingress, found) {
166
- const lines = [`live edge files: ${found.live.files.length === 0 ? 'none' : found.live.files.join(', ')}`];
167
- for (const file of found.live.unparsable) lines.push(`WARNING ${file} is not valid YAML: its routes could not be compared`);
168
- for (const host of sorted(found.liveHosts.keys())) {
169
- const state = found.declared.has(host) ? `declared (${found.declared.get(host)})` : 'NOT DECLARED';
170
- lines.push(`live ${host} -> ${found.liveHosts.get(host).join(', ') || '(no target)'}: ${state}`);
171
- }
172
- for (const host of found.declaredNotLive) lines.push(`declared, not live yet: ${host} (${found.declared.get(host)})`);
173
- for (const name of found.badMiddlewares) lines.push(`middleware ${name} is declared nowhere`);
174
- for (const [name, state] of Object.entries(found.containers)) {
175
- lines.push(`container ${name}: ${containerText(state)}`);
176
- }
177
- lines.push(`network ${ingress.network}: ${found.network}`, `certificates volume ${ingress.acmeVolume}: ${found.certificates ? 'present' : 'MISSING'}`);
178
- return lines;
179
- }
180
-
171
+ const lines = [`live edge files: ${found.live.files.length === 0 ? 'none' : found.live.files.join(', ')}`];
172
+ for (const file of found.live.unparsable)
173
+ lines.push(`WARNING ${file} is not valid YAML: its routes could not be compared`);
174
+ for (const host of sorted(found.liveHosts.keys())) {
175
+ const state = found.declared.has(host) ? `declared (${found.declared.get(host)})` : 'NOT DECLARED';
176
+ lines.push(`live ${host} -> ${(found.liveHosts.get(host) ?? []).join(', ') || '(no target)'}: ${state}`);
177
+ }
178
+ for (const host of found.declaredNotLive)
179
+ lines.push(`declared, not live yet: ${host} (${found.declared.get(host)})`);
180
+ for (const name of found.badMiddlewares)
181
+ lines.push(`middleware ${name} is declared nowhere`);
182
+ for (const [name, state] of Object.entries(found.containers)) {
183
+ lines.push(`container ${name}: ${containerText(state)}`);
184
+ }
185
+ lines.push(`network ${ingress.network}: ${found.network}`, `certificates volume ${ingress.acmeVolume}: ${found.certificates ? 'present' : 'MISSING'}`);
186
+ return lines;
187
+ }
181
188
  const planOk = (found) => found.missing.length === 0 && found.badMiddlewares.length === 0 && found.live.unparsable.length === 0;
182
-
183
189
  function planResult(ingress, found) {
184
- const lines = planLines(ingress, found);
185
- const ok = planOk(found);
186
- if (found.missing.length > 0) lines.push(`REFUSED: ${found.missing.length} live host(s) not covered by the declaration: ${found.missing.join(', ')}`);
187
- lines.push(ok ? 'plan green: every live host is covered' : 'plan NOT green');
188
- const data = {
189
- ok, steps: ['plan'], live: sorted(found.liveHosts.keys()), declared: sorted(found.declared.keys()), missing: found.missing,
190
- declaredNotLive: found.declaredNotLive, badMiddlewares: found.badMiddlewares, unparsable: found.live.unparsable,
191
- containers: found.containers, network: found.network, certificates: found.certificates,
192
- };
193
- return { ok, lines, data };
194
- }
195
-
190
+ const lines = planLines(ingress, found);
191
+ const ok = planOk(found);
192
+ if (found.missing.length > 0)
193
+ lines.push(`REFUSED: ${found.missing.length} live host(s) not covered by the declaration: ${found.missing.join(', ')}`);
194
+ lines.push(ok ? 'plan green: every live host is covered' : 'plan NOT green');
195
+ const data = {
196
+ ok, steps: ['plan'], live: sorted(found.liveHosts.keys()), declared: sorted(found.declared.keys()), missing: found.missing,
197
+ declaredNotLive: found.declaredNotLive, badMiddlewares: found.badMiddlewares, unparsable: found.live.unparsable,
198
+ containers: found.containers, network: found.network, certificates: found.certificates,
199
+ };
200
+ return { ok, lines, data };
201
+ }
196
202
  function planAction(run, config, ingress) {
197
- const found = gather(run, config, ingress);
198
- if (found.error) return { ok: false, lines: [found.error], data: { ok: false, steps: ['plan'], error: found.error } };
199
- return planResult(ingress, found);
203
+ const found = gather(run, config, ingress);
204
+ if (found.error !== undefined)
205
+ return { ok: false, lines: [found.error], data: { ok: false, steps: ['plan'], error: found.error } };
206
+ return planResult(ingress, found);
200
207
  }
201
-
202
208
  // ---- attach -------------------------------------------------------------------------------------------------------------------------
203
-
204
209
  function attachFailure(lines, steps, message) {
205
- lines.push(message);
206
- return { ok: false, lines, data: { ok: false, steps } };
210
+ lines.push(message);
211
+ return { ok: false, lines, data: { ok: false, steps } };
207
212
  }
208
-
209
213
  function connectArgs(ingress, entry) {
210
- return ['network', 'connect', ...entry.aliases.flatMap((alias) => ['--alias', alias]), '--gw-priority', '-1', ingress.network, entry.container];
214
+ return ['network', 'connect', ...entry.aliases.flatMap((alias) => ['--alias', alias]), '--gw-priority', '-1', ingress.network, entry.container];
211
215
  }
212
-
213
216
  /** Connects every upstream container; stops at the first failed command. */
214
217
  function connectAll(run, ingress, pending, lines, steps) {
215
- for (const entry of pending) {
216
- const done = remoteDocker(run, connectArgs(ingress, entry));
217
- if (done.status !== 0) return attachFailure(lines, steps, `connect ${entry.container} to ${ingress.network} FAILED: ${firstLine(done.stderr)}`);
218
- steps.push(`connect:${entry.container}`);
219
- lines.push(`connected ${entry.container} to ${ingress.network} (aliases ${entry.aliases.join(', ')})`);
220
- }
221
- return null;
222
- }
223
-
218
+ for (const entry of pending) {
219
+ const done = remoteDocker(run, connectArgs(ingress, entry));
220
+ if (done.status !== 0)
221
+ return attachFailure(lines, steps, `connect ${entry.container} to ${ingress.network} FAILED: ${firstLine(done.stderr)}`);
222
+ steps.push(`connect:${entry.container}`);
223
+ lines.push(`connected ${entry.container} to ${ingress.network} (aliases ${entry.aliases.join(', ')})`);
224
+ }
225
+ return null;
226
+ }
224
227
  /** Creates the overlay when it is missing (execute) or says it would; returns a failure result or null. */
225
228
  function ensureNetwork(run, ingress, { network, execute, lines, steps }) {
226
- if (network !== 'missing') return null;
227
- if (!execute) {
228
- lines.push(`dry run: would create the attachable overlay network ${ingress.network}`);
229
+ if (network !== 'missing')
230
+ return null;
231
+ if (!execute) {
232
+ lines.push(`dry run: would create the attachable overlay network ${ingress.network}`);
233
+ return null;
234
+ }
235
+ const created = remoteDocker(run, ['network', 'create', '--driver', 'overlay', '--attachable', ingress.network]);
236
+ if (created.status !== 0)
237
+ return attachFailure(lines, steps, `creating ${ingress.network} FAILED: ${firstLine(created.stderr)}`);
238
+ steps.push('network-create');
239
+ lines.push(`created the attachable overlay network ${ingress.network}`);
229
240
  return null;
230
- }
231
- const created = remoteDocker(run, ['network', 'create', '--driver', 'overlay', '--attachable', ingress.network]);
232
- if (created.status !== 0) return attachFailure(lines, steps, `creating ${ingress.network} FAILED: ${firstLine(created.stderr)}`);
233
- steps.push('network-create');
234
- lines.push(`created the attachable overlay network ${ingress.network}`);
235
- return null;
236
- }
237
-
241
+ }
238
242
  function attachAction(run, ingress, execute) {
239
- const lines = [];
240
- const steps = [];
241
- const states = onOverlay(ingress).map((entry) => ({ entry, state: containerState(run, entry.container, ingress.network) }));
242
- const absent = states.filter(({ state }) => !state.present).map(({ entry }) => entry.container);
243
- if (absent.length > 0) return attachFailure(lines, steps, `upstream container(s) missing or not running: ${absent.join(', ')}; nothing was changed`);
244
- const network = networkState(run, ingress.network);
245
- if (network === 'wrong') return attachFailure(lines, steps, `network ${ingress.network} exists but is not an attachable overlay; nothing was changed`);
246
- const pending = states.filter(({ state }) => !state.attached).map(({ entry }) => entry);
247
- for (const { entry, state } of states) if (state.attached) lines.push(`${entry.container} is already on ${ingress.network}: skipped`);
248
- const failedNetwork = ensureNetwork(run, ingress, { network, execute, lines, steps });
249
- if (failedNetwork) return failedNetwork;
250
- const done = { ok: true, lines, data: { ok: true, steps, network, pending: pending.map((entry) => entry.container) } };
251
- if (!execute) {
252
- for (const entry of pending) lines.push(`dry run: would connect ${entry.container} to ${ingress.network} (aliases ${entry.aliases.join(', ')})`);
253
- return done;
254
- }
255
- return connectAll(run, ingress, pending, lines, steps) ?? done;
256
- }
257
-
243
+ const lines = [];
244
+ const steps = [];
245
+ const states = onOverlay(ingress).map((entry) => ({ entry, state: containerState(run, entry.container, ingress.network) })); // onOverlay keeps only entries with a container
246
+ const absent = states.filter(({ state }) => !state.present).map(({ entry }) => entry.container);
247
+ if (absent.length > 0)
248
+ return attachFailure(lines, steps, `upstream container(s) missing or not running: ${absent.join(', ')}; nothing was changed`);
249
+ const network = networkState(run, ingress.network);
250
+ if (network === 'wrong')
251
+ return attachFailure(lines, steps, `network ${ingress.network} exists but is not an attachable overlay; nothing was changed`);
252
+ const pending = states.filter(({ state }) => !state.attached).map(({ entry }) => entry);
253
+ for (const { entry, state } of states)
254
+ if (state.attached)
255
+ lines.push(`${entry.container} is already on ${ingress.network}: skipped`);
256
+ const failedNetwork = ensureNetwork(run, ingress, { network, execute, lines, steps });
257
+ if (failedNetwork)
258
+ return failedNetwork;
259
+ const done = { ok: true, lines, data: { ok: true, steps, network, pending: pending.map((entry) => entry.container) } };
260
+ if (!execute) {
261
+ for (const entry of pending)
262
+ lines.push(`dry run: would connect ${entry.container} to ${ingress.network} (aliases ${entry.aliases.join(', ')})`);
263
+ return done;
264
+ }
265
+ return connectAll(run, ingress, pending, lines, steps) ?? done;
266
+ }
258
267
  // ---- takeover: probes ---------------------------------------------------------------------------------------------------------------
259
-
260
268
  /** The hosts to prove: the declared `verify` list or every plain declared host that is not dropped (path /). */
261
269
  function targetsOf(ingress, declared) {
262
- if (ingress.verify.length > 0) return ingress.verify;
263
- const plainHosts = [...declared.entries()].filter(([host, where]) => !host.startsWith('regexp:') && where !== 'dropped').map(([host]) => host);
264
- return sorted(plainHosts).map((host) => ({ host, path: '/', status: null }));
270
+ if (ingress.verify.length > 0)
271
+ return ingress.verify;
272
+ const plainHosts = [...declared.entries()].filter(([host, where]) => !host.startsWith('regexp:') && where !== 'dropped').map(([host]) => host);
273
+ return sorted(plainHosts).map((host) => ({ host, path: '/', status: null }));
265
274
  }
266
-
267
275
  const bashScript = (script) => `bash -c ${shellQuote(script)}`;
268
-
269
276
  /** The URL and the Host stay exactly as in production (a redirect rule that matches on the URL must see the same text): an alternate port is reached with --connect-to, never by putting the port in the URL. */
270
277
  function statusCommand(target, port) {
271
- const route = port === 443 ? ['--resolve', `${target.host}:443:127.0.0.1`] : ['--connect-to', `${target.host}:443:127.0.0.1:${port}`];
272
- const args = ['-sS', '--max-time', '10', ...route, '-o', '/dev/null', '-w', '%{http_code}', `https://${target.host}${target.path}`];
273
- return `curl ${args.map(shellQuote).join(' ')}`;
278
+ const route = port === 443 ? ['--resolve', `${target.host}:443:127.0.0.1`] : ['--connect-to', `${target.host}:443:127.0.0.1:${port}`];
279
+ const args = ['-sS', '--max-time', '10', ...route, '-o', '/dev/null', '-w', '%{http_code}', `https://${target.host}${target.path}`];
280
+ return `curl ${args.map(shellQuote).join(' ')}`;
274
281
  }
275
-
276
282
  function certificateCommand(target, port) {
277
- const connect = ['openssl', 's_client', '-connect', `127.0.0.1:${port}`, '-servername', target.host].map(shellQuote).join(' ');
278
- return bashScript(`printf '' | ${connect} 2>/dev/null | openssl x509 -noout -serial -subject`);
283
+ const connect = ['openssl', 's_client', '-connect', `127.0.0.1:${port}`, '-servername', target.host].map(shellQuote).join(' ');
284
+ return bashScript(`printf '' | ${connect} 2>/dev/null | openssl x509 -noout -serial -subject`);
279
285
  }
280
-
281
286
  /** What 127.0.0.1:<port> serves for one host: the HTTP status (0 = connection or TLS error) and the certificate serial and subject (public data). */
282
287
  function observe(run, target, port = 443) {
283
- const status = run(statusCommand(target, port));
284
- const code = String(status.stdout).trim();
285
- const certificate = run(certificateCommand(target, port));
286
- const text = String(certificate.stdout).trim();
287
- return {
288
- code: status.status === 0 && /^\d{3}$/u.test(code) ? Number(code) : 0,
289
- certificate: certificate.status === 0 && text !== '' ? text : null,
290
- };
291
- }
292
-
288
+ const status = run(statusCommand(target, port));
289
+ const code = String(status.stdout).trim();
290
+ const certificate = run(certificateCommand(target, port));
291
+ const text = String(certificate.stdout).trim();
292
+ return {
293
+ code: status.status === 0 && /^\d{3}$/u.test(code) ? Number(code) : 0,
294
+ certificate: certificate.status === 0 && text !== '' ? text : null,
295
+ };
296
+ }
293
297
  const label = (target) => `${target.host}${target.path}`;
294
298
  const answers = (code) => code >= 200 && code < 500;
295
-
296
299
  /**
297
300
  * The old edge is the baseline: it must answer every host and present a certificate, and whatever it answers IS the expectation of the takeover (the new
298
301
  * ingress serves the same dynamic directory, so every host must answer exactly the same). The declared `verify.status` values describe the state AFTER
299
302
  * `ingress publish` (for example a 308 redirect that only the app's own routes file creates) and are judged by publish and by `stack verify`.
300
303
  */
301
304
  function baselines(run, targets) {
302
- return targets.map((target) => {
303
- const seen = observe(run, target);
304
- return { target, ...seen, ok: answers(seen.code) && seen.certificate !== null };
305
- });
305
+ return targets.map((target) => {
306
+ const seen = observe(run, target);
307
+ return { target, ...seen, ok: answers(seen.code) && seen.certificate !== null };
308
+ });
306
309
  }
307
-
308
310
  /** Runs `check` until it is ok or the attempts are used up, sleeping between attempts. */
309
311
  async function settle(ctx, attempts, intervalMs, check) {
310
- const last = await check();
311
- if (last.ok || attempts <= 1) return last;
312
- await ctx.sleep(intervalMs);
313
- return settle(ctx, attempts - 1, intervalMs, check);
312
+ const last = await check();
313
+ if (last.ok || attempts <= 1)
314
+ return last;
315
+ await ctx.sleep(intervalMs);
316
+ return settle(ctx, attempts - 1, intervalMs, check);
314
317
  }
315
-
316
318
  /** Every host answers the baseline status and presents the baseline certificate (retried while the edge boots). */
317
319
  function matchBaseline(ctx, run, base) {
318
- return settle(ctx, SETTLE_ATTEMPTS, SETTLE_MS, () => {
319
- const rows = base.map((entry) => {
320
- const seen = observe(run, entry.target);
321
- return { target: entry.target, code: seen.code, ok: seen.code === entry.code && seen.certificate === entry.certificate };
320
+ return settle(ctx, SETTLE_ATTEMPTS, SETTLE_MS, () => {
321
+ const rows = base.map((entry) => {
322
+ const seen = observe(run, entry.target);
323
+ return { target: entry.target, code: seen.code, ok: seen.code === entry.code && seen.certificate === entry.certificate };
324
+ });
325
+ return { ok: rows.every((row) => row.ok), rows };
322
326
  });
323
- return { ok: rows.every((row) => row.ok), rows };
324
- });
325
327
  }
326
-
327
328
  const mismatches = (rows) => rows.filter((row) => !row.ok).map((row) => `${label(row.target)}=${row.code}`).join(', ');
328
-
329
329
  // ---- takeover: backup ---------------------------------------------------------------------------------------------------------------
330
-
331
330
  /** The first age recipient of the creation_rules of <repoRoot>/.sops.yaml (the custody's own public recipient). */
332
331
  function readRecipient(repoRoot) {
333
- let rules;
334
- try { rules = parseYaml(fs.readFileSync(path.join(repoRoot, '.sops.yaml'), 'utf8'))?.creation_rules; } catch { rules = null; }
335
- const found = AGE_RECIPIENT.exec(JSON.stringify(rules ?? null));
336
- if (!found) throw refusal('STACK_INGRESS_RECIPIENT', 'no age recipient found in the creation_rules of .sops.yaml');
337
- return found[0];
332
+ let rules;
333
+ try {
334
+ rules = parseYaml(fs.readFileSync(path.join(repoRoot, '.sops.yaml'), 'utf8'))?.creation_rules;
335
+ }
336
+ catch {
337
+ rules = null;
338
+ }
339
+ const found = AGE_RECIPIENT.exec(JSON.stringify(rules ?? null));
340
+ if (!found)
341
+ throw refusal('STACK_INGRESS_RECIPIENT', 'no age recipient found in the creation_rules of .sops.yaml');
342
+ return found[0];
338
343
  }
339
-
340
344
  const stampOf = (epochMs) => new Date(epochMs).toISOString().slice(0, 19).replaceAll('-', '').replaceAll(':', '').replace('T', '-');
341
-
342
345
  function archiveStep(run, { source, file, recipient, dir }) {
343
- const steps = [
344
- `mkdir -p ${shellQuote(dir)}`,
345
- `sudo -n tar -cf - -C ${shellQuote(source)} . | age -r ${shellQuote(recipient)} -o ${shellQuote(file)}`,
346
- `test -s ${shellQuote(file)}`,
347
- ];
348
- const done = run(bashScript(`set -o pipefail; umask 077; ${steps.join(' && ')}`));
349
- return done.status === 0 ? null : firstLine(done.stderr);
350
- }
351
-
346
+ const steps = [
347
+ `mkdir -p ${shellQuote(dir)}`,
348
+ `sudo -n tar -cf - -C ${shellQuote(source)} . | age -r ${shellQuote(recipient)} -o ${shellQuote(file)}`,
349
+ `test -s ${shellQuote(file)}`,
350
+ ];
351
+ const done = run(bashScript(`set -o pipefail; umask 077; ${steps.join(' && ')}`));
352
+ return done.status === 0 ? null : firstLine(done.stderr);
353
+ }
352
354
  /** Age-encrypted copies of the certificates store and of the dynamic directory, made ON the manager: { files } or { error }. */
353
355
  function backupStores(ctx, run, ingress, recipient) {
354
- const mount = remoteDocker(run, ['volume', 'inspect', '--format', '{{.Mountpoint}}', ingress.acmeVolume]);
355
- const mountpoint = String(mount.stdout).trim();
356
- if (mount.status !== 0 || !MOUNTPOINT.test(mountpoint)) return { error: `cannot locate the volume ${ingress.acmeVolume}` };
357
- const { dir } = ingress.backup;
358
- const stamp = stampOf(ctx.now());
359
- const jobs = [
360
- { source: mountpoint, file: `${dir}/acme-${stamp}.tar.age` },
361
- { source: ingress.legacy.dynamicDir, file: `${dir}/dynamic-${stamp}.tar.age` },
362
- ];
363
- for (const job of jobs) {
364
- const reason = archiveStep(run, { ...job, recipient, dir });
365
- if (reason !== null) return { error: `backup ${job.file} FAILED: ${reason}` };
366
- }
367
- return { files: jobs.map((job) => job.file) };
368
- }
369
-
356
+ const mount = remoteDocker(run, ['volume', 'inspect', '--format', '{{.Mountpoint}}', ingress.acmeVolume]);
357
+ const mountpoint = String(mount.stdout).trim();
358
+ if (mount.status !== 0 || !MOUNTPOINT.test(mountpoint))
359
+ return { error: `cannot locate the volume ${ingress.acmeVolume}` };
360
+ const { dir } = ingress.backup;
361
+ const stamp = stampOf(ctx.now());
362
+ const jobs = [
363
+ { source: mountpoint, file: `${dir}/acme-${stamp}.tar.age` },
364
+ { source: ingress.legacy.dynamicDir, file: `${dir}/dynamic-${stamp}.tar.age` },
365
+ ];
366
+ for (const job of jobs) {
367
+ const reason = archiveStep(run, { ...job, recipient, dir });
368
+ if (reason !== null)
369
+ return { error: `backup ${job.file} FAILED: ${reason}` };
370
+ }
371
+ return { files: jobs.map((job) => job.file) };
372
+ }
370
373
  // ---- takeover: preconditions --------------------------------------------------------------------------------------------------------
371
-
372
374
  const serviceName = (ingress) => `${ingress.name}_${ingress.service}`;
373
-
374
375
  /** { exists, desired, running } of a swarm service from the service listing (desired = the replica count the service is set to). */
375
376
  function serviceState(run, name) {
376
- const listed = remoteDocker(run, ['service', 'ls', '--filter', `name=${name}`, '--format', '{{.Name}} {{.Replicas}}']);
377
- const row = String(listed.stdout).split('\n').map((line) => line.trim().split(' ')).find(([found]) => found === name);
378
- if (listed.status !== 0 || !row) return { exists: false, desired: -1, running: -1 };
379
- const [running, desired] = String(row[1]).split('/').map(Number);
380
- return { exists: true, desired, running };
377
+ const listed = remoteDocker(run, ['service', 'ls', '--filter', `name=${name}`, '--format', '{{.Name}} {{.Replicas}}']);
378
+ const row = String(listed.stdout).split('\n').map((line) => line.trim().split(' ')).find(([found]) => found === name);
379
+ if (listed.status !== 0 || !row)
380
+ return { exists: false, desired: -1, running: -1 };
381
+ const [running, desired] = String(row[1]).split('/').map(Number);
382
+ return { exists: true, desired, running };
381
383
  }
382
-
383
384
  function preconditions(run, ingress, found) {
384
- const swarm = remoteDocker(run, ['info', '--format', '{{.Swarm.LocalNodeState}} {{.Swarm.ControlAvailable}}']);
385
- const service = serviceState(run, serviceName(ingress));
386
- const detached = Object.entries(found.containers).filter(([, state]) => !state.attached).map(([name]) => name);
387
- const checks = [
388
- ['plan green', planOk(found), `${found.missing.length} live host(s) not covered`],
389
- ['swarm active (manager)', swarm.status === 0 && String(swarm.stdout).trim() === 'active true', 'this host is not an active swarm manager'],
390
- [`swarm service ${serviceName(ingress)} exists with 0 replicas (host-mode ports bind only when a task starts)`, service.exists && service.desired === 0, 'create it with replicas 0 first'],
391
- [`network ${ingress.network} is an attachable overlay`, found.network === 'ok', `network state: ${found.network}`],
392
- ['every upstream container attached', detached.length === 0, `not attached: ${detached.join(', ')}`],
393
- [`certificates volume ${ingress.acmeVolume} present`, found.certificates, 'the persisted certificates volume is missing'],
394
- ];
395
- return checks.map(([name, ok, detail]) => ({ name, ok, detail }));
396
- }
397
-
385
+ const swarm = remoteDocker(run, ['info', '--format', '{{.Swarm.LocalNodeState}} {{.Swarm.ControlAvailable}}']);
386
+ const service = serviceState(run, serviceName(ingress));
387
+ const detached = Object.entries(found.containers).filter(([, state]) => !state.attached).map(([name]) => name);
388
+ const checks = [
389
+ ['plan green', planOk(found), `${found.missing.length} live host(s) not covered`],
390
+ ['swarm active (manager)', swarm.status === 0 && String(swarm.stdout).trim() === 'active true', 'this host is not an active swarm manager'],
391
+ [`swarm service ${serviceName(ingress)} exists with 0 replicas (host-mode ports bind only when a task starts)`, service.exists && service.desired === 0, 'create it with replicas 0 first'],
392
+ [`network ${ingress.network} is an attachable overlay`, found.network === 'ok', `network state: ${found.network}`],
393
+ ['every upstream container attached', detached.length === 0, `not attached: ${detached.join(', ')}`],
394
+ [`certificates volume ${ingress.acmeVolume} present`, found.certificates, 'the persisted certificates volume is missing'],
395
+ ];
396
+ return checks.map(([name, ok, detail]) => ({ name, ok, detail }));
397
+ }
398
398
  // ---- takeover: the switch -----------------------------------------------------------------------------------------------------------
399
-
400
399
  const dockerLine = (args) => ['docker', ...args.map(shellQuote)].join(' ');
401
-
402
400
  /** Waits until 127.0.0.1:443 accepts a TCP connection (every 100 ms, at most gateSeconds). */
403
401
  const tcpLoop = (gateSeconds) => bashScript(`for i in $(seq 1 ${gateSeconds * 10}); do if (exec 3<>/dev/tcp/127.0.0.1/443) 2>/dev/null; then exit 0; fi; sleep 0.1; done; exit 1`);
404
-
405
402
  /** T0: stop the old edge and start the ingress in one remote command; then wait for 443 and prove every host. Returns { failure, gapMs }. */
406
403
  async function swap(ctx, run, ingress, base) {
407
- const gateMs = ingress.takeover.gateSeconds * 1000;
408
- const t0 = ctx.now();
409
- const stop = dockerLine(['stop', '-t', '10', ingress.legacy.container]);
410
- const start = dockerLine(['service', 'scale', `${serviceName(ingress)}=1`]);
411
- const switched = run(`${stop} && ${start}`);
412
- if (switched.status !== 0) return { failure: `stop and scale FAILED: ${firstLine(switched.stderr)}`, gapMs: ctx.now() - t0 };
413
- const up = run(tcpLoop(ingress.takeover.gateSeconds));
414
- const gapMs = ctx.now() - t0;
415
- if (up.status !== 0 || gapMs > gateMs) return { failure: `port 443 was not accepting connections within ${ingress.takeover.gateSeconds} s`, gapMs };
416
- const proof = await matchBaseline(ctx, run, base);
417
- return proof.ok ? { failure: null, gapMs } : { failure: `hosts differ from the old edge: ${mismatches(proof.rows)}`, gapMs };
418
- }
419
-
404
+ const gateMs = ingress.takeover.gateSeconds * 1000;
405
+ const t0 = ctx.now();
406
+ const stop = dockerLine(['stop', '-t', '10', ingress.legacy.container]);
407
+ const start = dockerLine(['service', 'scale', `${serviceName(ingress)}=1`]);
408
+ const switched = run(`${stop} && ${start}`);
409
+ if (switched.status !== 0)
410
+ return { failure: `stop and scale FAILED: ${firstLine(switched.stderr)}`, gapMs: ctx.now() - t0 };
411
+ const up = run(tcpLoop(ingress.takeover.gateSeconds));
412
+ const gapMs = ctx.now() - t0;
413
+ if (up.status !== 0 || gapMs > gateMs)
414
+ return { failure: `port 443 was not accepting connections within ${ingress.takeover.gateSeconds} s`, gapMs };
415
+ const proof = await matchBaseline(ctx, run, base);
416
+ return proof.ok ? { failure: null, gapMs } : { failure: `hosts differ from the old edge: ${mismatches(proof.rows)}`, gapMs };
417
+ }
420
418
  /** Automatic fallback: free the ports, start the old edge, prove it answers every host as before. */
421
419
  async function fallback(ctx, run, ingress, base, steps) {
422
- const name = serviceName(ingress);
423
- const scale = remoteDocker(run, ['service', 'scale', `${name}=0`]);
424
- steps.push('fallback-scale-0');
425
- const manual = `docker service scale ${name}=0; docker start ${ingress.legacy.container}; check every host`;
426
- const started = await settle(ctx, START_ATTEMPTS, SETTLE_MS, () => ({ ok: remoteDocker(run, ['start', ingress.legacy.container]).status === 0 }));
427
- steps.push('fallback-start-old-edge');
428
- if (scale.status !== 0 || !started.ok) return { ok: false, line: `FALLBACK FAILED (manual: ${manual})` };
429
- const proof = await matchBaseline(ctx, run, base);
430
- steps.push('fallback-verify');
431
- if (!proof.ok) return { ok: false, line: `FALLBACK FAILED (manual: the old edge started but does not answer as before (${mismatches(proof.rows)}); ${manual})` };
432
- return { ok: true, line: `FALLBACK ok: ${ingress.legacy.container} answers every host again; the ingress service is scaled to 0 (docker service scale ${name}=1 after fixing the cause)` };
433
- }
434
-
420
+ const name = serviceName(ingress);
421
+ const scale = remoteDocker(run, ['service', 'scale', `${name}=0`]);
422
+ steps.push('fallback-scale-0');
423
+ const manual = `docker service scale ${name}=0; docker start ${ingress.legacy.container}; check every host`;
424
+ const started = await settle(ctx, START_ATTEMPTS, SETTLE_MS, () => ({ ok: remoteDocker(run, ['start', ingress.legacy.container]).status === 0 }));
425
+ steps.push('fallback-start-old-edge');
426
+ if (scale.status !== 0 || !started.ok)
427
+ return { ok: false, line: `FALLBACK FAILED (manual: ${manual})` };
428
+ const proof = await matchBaseline(ctx, run, base);
429
+ steps.push('fallback-verify');
430
+ if (!proof.ok)
431
+ return { ok: false, line: `FALLBACK FAILED (manual: the old edge started but does not answer as before (${mismatches(proof.rows)}); ${manual})` };
432
+ return { ok: true, line: `FALLBACK ok: ${ingress.legacy.container} answers every host again; the ingress service is scaled to 0 (docker service scale ${name}=1 after fixing the cause)` };
433
+ }
435
434
  const result = (ok, lines, steps, extra = {}) => ({ ok, lines, data: { ok, steps, ...extra } });
436
-
437
435
  async function cutover(ctx, run, ingress, context) {
438
- const { base, lines, steps, files } = context;
439
- const outcome = await swap(ctx, run, ingress, base);
440
- steps.push('stop-and-scale');
441
- lines.push(`measured unavailability gap: ${outcome.gapMs} ms (gate ${ingress.takeover.gateSeconds} s)`);
442
- if (outcome.failure === null) {
443
- lines.push(`takeover complete: the swarm ingress serves ${base.length} host(s) with the old edge's certificates; ${ingress.legacy.container} is stopped, not removed`);
444
- return result(true, lines, steps, { gapMs: outcome.gapMs, backups: files, fallback: null });
445
- }
446
- lines.push(`takeover FAILED: ${outcome.failure}`);
447
- const back = await fallback(ctx, run, ingress, base, steps);
448
- lines.push(back.line);
449
- return result(false, lines, steps, { gapMs: outcome.gapMs, backups: files, fallback: back.ok ? 'ok' : 'failed' });
450
- }
451
-
436
+ const { base, lines, steps, files } = context;
437
+ const outcome = await swap(ctx, run, ingress, base);
438
+ steps.push('stop-and-scale');
439
+ lines.push(`measured unavailability gap: ${outcome.gapMs} ms (gate ${ingress.takeover.gateSeconds} s)`);
440
+ if (outcome.failure === null) {
441
+ lines.push(`takeover complete: the swarm ingress serves ${base.length} host(s) with the old edge's certificates; ${ingress.legacy.container} is stopped, not removed`);
442
+ return result(true, lines, steps, { gapMs: outcome.gapMs, backups: files, fallback: null });
443
+ }
444
+ lines.push(`takeover FAILED: ${outcome.failure}`);
445
+ const back = await fallback(ctx, run, ingress, base, steps);
446
+ lines.push(back.line);
447
+ return result(false, lines, steps, { gapMs: outcome.gapMs, backups: files, fallback: back.ok ? 'ok' : 'failed' });
448
+ }
452
449
  async function takeoverAction(ctx, run, config, ingress, execute) {
453
- if (ingress.backup === null) throw refusal('STACK_INGRESS_BACKUP_UNDECLARED', 'deploy.ingress.backup (dir) is not declared: the takeover needs an encrypted backup');
454
- const recipient = readRecipient(config.repoRoot);
455
- const steps = ['plan'];
456
- const found = gather(run, config, ingress);
457
- if (found.error) return result(false, [found.error], steps);
458
- const checks = preconditions(run, ingress, found);
459
- const lines = checks.map((check) => (check.ok ? `precondition ok: ${check.name}` : `precondition FAILED: ${check.name} (${check.detail})`));
460
- steps.push('preconditions');
461
- if (checks.some((check) => !check.ok)) return result(false, [...lines, 'takeover refused: nothing was changed'], steps);
462
- const base = baselines(run, targetsOf(ingress, found.declared));
463
- steps.push('baseline');
464
- for (const row of base) lines.push(`${row.ok ? 'baseline ok' : 'baseline FAILED'} ${label(row.target)}: old edge answers ${row.code}`);
465
- if (base.some((row) => !row.ok)) return result(false, [...lines, 'takeover refused: the old edge does not answer as declared; nothing was changed'], steps);
466
- if (!execute) {
467
- lines.push(`dry run: would back up the certificates store and the dynamic directory (age) to ${ingress.backup.dir}, stop ${ingress.legacy.container} and scale ${serviceName(ingress)} to 1, verify ${base.length} host(s) and fall back on any failure`);
468
- return result(true, lines, steps);
469
- }
470
- const saved = backupStores(ctx, run, ingress, recipient);
471
- if (saved.error) return result(false, [...lines, saved.error, 'takeover refused: nothing was changed'], steps);
472
- steps.push('backup');
473
- lines.push(`backup written: ${saved.files.join(', ')}`);
474
- return cutover(ctx, run, ingress, { base, lines, steps, files: saved.files });
475
- }
476
-
450
+ if (!declaresBackup(ingress))
451
+ throw refusal('STACK_INGRESS_BACKUP_UNDECLARED', 'deploy.ingress.backup (dir) is not declared: the takeover needs an encrypted backup');
452
+ const recipient = readRecipient(config.repoRoot);
453
+ const steps = ['plan'];
454
+ const found = gather(run, config, ingress);
455
+ if (found.error !== undefined)
456
+ return result(false, [found.error], steps);
457
+ const checks = preconditions(run, ingress, found);
458
+ const lines = checks.map((check) => (check.ok ? `precondition ok: ${check.name}` : `precondition FAILED: ${check.name} (${check.detail})`));
459
+ steps.push('preconditions');
460
+ if (checks.some((check) => !check.ok))
461
+ return result(false, [...lines, 'takeover refused: nothing was changed'], steps);
462
+ const base = baselines(run, targetsOf(ingress, found.declared));
463
+ steps.push('baseline');
464
+ for (const row of base)
465
+ lines.push(`${row.ok ? 'baseline ok' : 'baseline FAILED'} ${label(row.target)}: old edge answers ${row.code}`);
466
+ if (base.some((row) => !row.ok))
467
+ return result(false, [...lines, 'takeover refused: the old edge does not answer as declared; nothing was changed'], steps);
468
+ if (!execute) {
469
+ lines.push(`dry run: would back up the certificates store and the dynamic directory (age) to ${ingress.backup.dir}, stop ${ingress.legacy.container} and scale ${serviceName(ingress)} to 1, verify ${base.length} host(s) and fall back on any failure`);
470
+ return result(true, lines, steps);
471
+ }
472
+ const saved = backupStores(ctx, run, ingress, recipient);
473
+ if (saved.files === undefined)
474
+ return result(false, [...lines, saved.error, 'takeover refused: nothing was changed'], steps);
475
+ steps.push('backup');
476
+ lines.push(`backup written: ${saved.files.join(', ')}`);
477
+ return cutover(ctx, run, ingress, { base, lines, steps, files: saved.files });
478
+ }
477
479
  // ---- rehearse -----------------------------------------------------------------------------------------------------------------------
478
480
  // A throwaway copy of the ingress on alternate host ports, proving that the persisted certificates and the dynamic routes serve every host
479
481
  // exactly as the live edge does. The live edge, its container, the production certificates volume and ports 80/443 are never touched: the
480
482
  // store is only READ (read-only mount of a helper container) into a NEW volume, the service reads the legacy dynamic directory read-only,
481
483
  // the ACME http challenge is moved to an unused internal entrypoint so no certificate can be requested, and everything created is torn
482
484
  // down in a finally path.
483
-
484
485
  const UNUSED_ENTRYPOINT = 'rehearsal-unused';
485
486
  const CHALLENGE_ARG = /^(--certificatesresolvers\.[^.]+\.acme\.httpchallenge\.entrypoint)=.*$/iu;
486
487
  const ACME_ACTIVITY = /\b(?:obtain|obtaining|registering|new account|authorization)\b/iu;
487
488
  const READY_ATTEMPTS = 12;
488
-
489
489
  const rehearsalName = (ingress) => `${ingress.name}-rehearsal`;
490
490
  const rehearsalVolume = (ingress) => `${ingress.acmeVolume}-rehearsal`;
491
491
  const classOf = (code) => Math.floor(code / 100);
492
-
493
492
  /** The static arguments of the ingress service with the ACME http challenge moved to an unused internal entrypoint. */
494
493
  function rehearsalArgs(args) {
495
- const quiet = args.map((arg) => arg.replace(CHALLENGE_ARG, `$1=${UNUSED_ENTRYPOINT}`));
496
- return [...quiet, `--entrypoints.${UNUSED_ENTRYPOINT}.address=127.0.0.1:1`];
494
+ const quiet = args.map((arg) => arg.replace(CHALLENGE_ARG, `$1=${UNUSED_ENTRYPOINT}`));
495
+ return [...quiet, `--entrypoints.${UNUSED_ENTRYPOINT}.address=127.0.0.1:1`];
497
496
  }
498
-
499
497
  /** Image, static arguments and the container paths of the two mounts the rehearsal copies; null when the service cannot be rehearsed. */
500
498
  function ingressSpec(run, ingress) {
501
- const inspect = remoteDocker(run, ['service', 'inspect', '--format', '{{json .Spec.TaskTemplate.ContainerSpec}}', serviceName(ingress)]);
502
- let spec = null;
503
- try { spec = JSON.parse(inspect.stdout); } catch { spec = null; }
504
- const mounts = Array.isArray(spec?.Mounts) ? spec.Mounts : [];
505
- const targetOf = (source) => mounts.find((mount) => asMap(mount).Source === source)?.Target;
506
- const out = {
507
- image: spec?.Image,
508
- args: (Array.isArray(spec?.Args) ? spec.Args : []).filter((arg) => typeof arg === 'string'),
509
- dynamicTarget: targetOf(ingress.legacy.dynamicDir),
510
- acmeTarget: targetOf(ingress.acmeVolume),
511
- };
512
- const complete = typeof out.image === 'string' && out.args.length > 0 && typeof out.dynamicTarget === 'string' && typeof out.acmeTarget === 'string';
513
- return inspect.status === 0 && complete ? out : null;
514
- }
515
-
499
+ const inspect = remoteDocker(run, ['service', 'inspect', '--format', '{{json .Spec.TaskTemplate.ContainerSpec}}', serviceName(ingress)]);
500
+ // reason: `docker service inspect` ContainerSpec, parsed JSON read defensively and checked for completeness below
501
+ let spec = null;
502
+ try {
503
+ spec = JSON.parse(inspect.stdout);
504
+ }
505
+ catch {
506
+ spec = null;
507
+ }
508
+ const mounts = Array.isArray(spec?.Mounts) ? spec.Mounts : [];
509
+ const targetOf = (source) => mounts.find((mount) => asMap(mount).Source === source)?.Target;
510
+ const out = {
511
+ image: spec?.Image,
512
+ args: (Array.isArray(spec?.Args) ? spec.Args : []).filter((arg) => typeof arg === 'string'),
513
+ dynamicTarget: targetOf(ingress.legacy.dynamicDir),
514
+ acmeTarget: targetOf(ingress.acmeVolume),
515
+ };
516
+ const complete = typeof out.image === 'string' && out.args.length > 0 && typeof out.dynamicTarget === 'string' && typeof out.acmeTarget === 'string';
517
+ return inspect.status === 0 && complete ? out : null; // complete: every field was checked above
518
+ }
516
519
  function rehearseChecks(run, config, ingress, found, spec) {
517
- const leftoverService = remoteDocker(run, ['service', 'inspect', rehearsalName(ingress)]).status === 0;
518
- const leftoverVolume = volumePresent(run, rehearsalVolume(ingress));
519
- const checks = [
520
- ['plan green', planOk(found), `${found.missing.length} live host(s) not covered`],
521
- [`network ${ingress.network} is an attachable overlay`, found.network === 'ok', `network state: ${found.network}`],
522
- [`certificates volume ${ingress.acmeVolume} present`, found.certificates, 'the persisted certificates volume is missing'],
523
- ['helper image declared (deploy.backup.image)', typeof config.backup?.image === 'string', 'the copy of the store needs a digest-pinned helper image'],
524
- [`swarm service ${serviceName(ingress)} readable (image, arguments, dynamic and certificates mounts)`, spec !== null, 'create the ingress service first'],
525
- [`no leftover ${rehearsalName(ingress)} service or ${rehearsalVolume(ingress)} volume`, !leftoverService && !leftoverVolume, 'remove the leftover of an earlier rehearsal first'],
526
- ];
527
- return checks.map(([name, ok, detail]) => ({ name, ok, detail }));
528
- }
529
-
520
+ const leftoverService = remoteDocker(run, ['service', 'inspect', rehearsalName(ingress)]).status === 0;
521
+ const leftoverVolume = volumePresent(run, rehearsalVolume(ingress));
522
+ const checks = [
523
+ ['plan green', planOk(found), `${found.missing.length} live host(s) not covered`],
524
+ [`network ${ingress.network} is an attachable overlay`, found.network === 'ok', `network state: ${found.network}`],
525
+ [`certificates volume ${ingress.acmeVolume} present`, found.certificates, 'the persisted certificates volume is missing'],
526
+ ['helper image declared (deploy.backup.image)', typeof config.backup?.image === 'string', 'the copy of the store needs a digest-pinned helper image'],
527
+ [`swarm service ${serviceName(ingress)} readable (image, arguments, dynamic and certificates mounts)`, spec !== null, 'create the ingress service first'],
528
+ [`no leftover ${rehearsalName(ingress)} service or ${rehearsalVolume(ingress)} volume`, !leftoverService && !leftoverVolume, 'remove the leftover of an earlier rehearsal first'],
529
+ ];
530
+ return checks.map(([name, ok, detail]) => ({ name, ok, detail }));
531
+ }
530
532
  function serviceCreateArgs(ingress, spec) {
531
- const ports = ingress.takeover.rehearsalPorts;
532
- const mounts = [
533
- `type=bind,source=${ingress.legacy.dynamicDir},target=${spec.dynamicTarget},readonly`,
534
- `type=volume,source=${rehearsalVolume(ingress)},target=${spec.acmeTarget}`,
535
- ];
536
- return [
537
- 'service', 'create', '--detach', '--name', rehearsalName(ingress), '--replicas', '1', '--restart-condition', 'none', '--network', ingress.network,
538
- '--publish', `mode=host,target=80,published=${ports.http}`, '--publish', `mode=host,target=443,published=${ports.https}`,
539
- ...mounts.flatMap((mount) => ['--mount', mount]), spec.image, ...rehearsalArgs(spec.args),
540
- ];
541
- }
542
-
533
+ const ports = ingress.takeover.rehearsalPorts;
534
+ const mounts = [
535
+ `type=bind,source=${ingress.legacy.dynamicDir},target=${spec.dynamicTarget},readonly`,
536
+ `type=volume,source=${rehearsalVolume(ingress)},target=${spec.acmeTarget}`,
537
+ ];
538
+ return [
539
+ 'service', 'create', '--detach', '--name', rehearsalName(ingress), '--replicas', '1', '--restart-condition', 'none', '--network', ingress.network,
540
+ '--publish', `mode=host,target=80,published=${ports.http}`, '--publish', `mode=host,target=443,published=${ports.https}`,
541
+ ...mounts.flatMap((mount) => ['--mount', mount]), spec.image, ...rehearsalArgs(spec.args),
542
+ ];
543
+ }
543
544
  /** Volume, read-only copy of the store, service: stops at the first failed step. Returns a failure message or null. */
544
545
  async function rehearsalUp(ctx, run, config, ingress, context) {
545
- const { spec, steps, created } = context;
546
- const volume = rehearsalVolume(ingress);
547
- const made = remoteDocker(run, ['volume', 'create', volume]);
548
- if (made.status !== 0) return `creating ${volume} FAILED: ${firstLine(made.stderr)}`;
549
- created.volume = true;
550
- steps.push('volume-create');
551
- const copy = remoteDocker(run, ['run', '--rm', '--network', 'none', '-v', `${ingress.acmeVolume}:/from:ro`, '-v', `${volume}:/to`, config.backup.image, 'cp', '-a', '/from/.', '/to/']);
552
- if (copy.status !== 0) return `copying the certificates store FAILED: ${firstLine(copy.stderr)}`;
553
- steps.push('volume-copy');
554
- const service = remoteDocker(run, serviceCreateArgs(ingress, spec));
555
- if (service.status !== 0) return `creating ${rehearsalName(ingress)} FAILED: ${firstLine(service.stderr)}`;
556
- created.service = true;
557
- steps.push('service-create');
558
- const ready = await settle(ctx, READY_ATTEMPTS, SETTLE_MS, () => {
559
- const state = serviceState(run, rehearsalName(ingress));
560
- return { ok: state.desired > 0 && state.running === state.desired };
561
- });
562
- steps.push('service-ready');
563
- return ready.ok ? null : `${rehearsalName(ingress)} did not start`;
564
- }
565
-
546
+ const { spec, steps, created } = context;
547
+ const volume = rehearsalVolume(ingress);
548
+ const made = remoteDocker(run, ['volume', 'create', volume]);
549
+ if (made.status !== 0)
550
+ return `creating ${volume} FAILED: ${firstLine(made.stderr)}`;
551
+ created.volume = true;
552
+ steps.push('volume-create');
553
+ const copy = remoteDocker(run, ['run', '--rm', '--network', 'none', '-v', `${ingress.acmeVolume}:/from:ro`, '-v', `${volume}:/to`, config.backup.image, 'cp', '-a', '/from/.', '/to/']);
554
+ if (copy.status !== 0)
555
+ return `copying the certificates store FAILED: ${firstLine(copy.stderr)}`;
556
+ steps.push('volume-copy');
557
+ const service = remoteDocker(run, serviceCreateArgs(ingress, spec));
558
+ if (service.status !== 0)
559
+ return `creating ${rehearsalName(ingress)} FAILED: ${firstLine(service.stderr)}`;
560
+ created.service = true;
561
+ steps.push('service-create');
562
+ const ready = await settle(ctx, READY_ATTEMPTS, SETTLE_MS, () => {
563
+ const state = serviceState(run, rehearsalName(ingress));
564
+ return { ok: state.desired > 0 && state.running === state.desired };
565
+ });
566
+ steps.push('service-ready');
567
+ return ready.ok ? null : `${rehearsalName(ingress)} did not start`;
568
+ }
566
569
  /** Status class and certificate of every host on the rehearsal ports against the live edge baseline; hosts without a live certificate are skipped. */
567
570
  function compareRehearsal(run, ingress, base) {
568
- const port = ingress.takeover.rehearsalPorts.https;
569
- return base.map((entry) => {
570
- if (entry.certificate === null) return { target: entry.target, live: entry.code, skipped: true, ok: true };
571
- const seen = observe(run, entry.target, port);
572
- const sameStatus = classOf(seen.code) === classOf(entry.code);
573
- const sameCertificate = seen.certificate === entry.certificate;
574
- return { target: entry.target, live: entry.code, code: seen.code, sameCertificate, skipped: false, ok: sameStatus && sameCertificate };
575
- });
576
- }
577
-
571
+ const port = ingress.takeover.rehearsalPorts.https;
572
+ return base.map((entry) => {
573
+ if (entry.certificate === null)
574
+ return { target: entry.target, live: entry.code, skipped: true, ok: true };
575
+ const seen = observe(run, entry.target, port);
576
+ const sameStatus = classOf(seen.code) === classOf(entry.code);
577
+ const sameCertificate = seen.certificate === entry.certificate;
578
+ return { target: entry.target, live: entry.code, code: seen.code, sameCertificate, skipped: false, ok: sameStatus && sameCertificate };
579
+ });
580
+ }
578
581
  function certificateVerdict(row) {
579
- if (row.skipped) return 'skipped';
580
- return row.sameCertificate ? 'equal' : 'different';
582
+ if (row.skipped)
583
+ return 'skipped';
584
+ return row.sameCertificate ? 'equal' : 'different';
581
585
  }
582
-
583
586
  function rowLine(row) {
584
- if (row.skipped) return `skipped: needs fresh issuance ${label(row.target)} (the live edge presents no certificate for it)`;
585
- return `${row.ok ? 'ok' : 'MISMATCH'} ${label(row.target)}: status ${row.code} (live ${row.live}), certificate serial and subject ${certificateVerdict(row)}`;
587
+ if (row.skipped)
588
+ return `skipped: needs fresh issuance ${label(row.target)} (the live edge presents no certificate for it)`;
589
+ return `${row.ok ? 'ok' : 'MISMATCH'} ${label(row.target)}: status ${row.code} (live ${row.live}), certificate serial and subject ${certificateVerdict(row)}`;
586
590
  }
587
-
588
591
  /** The rehearsal log must show no certificate request (obtain, register, authorization). Returns a failure message or null. */
589
592
  function acmeLogProblem(run, ingress) {
590
- const logs = remoteDocker(run, ['service', 'logs', '--raw', '--no-task-ids', rehearsalName(ingress)]);
591
- if (logs.status !== 0) return 'cannot read the rehearsal log: no proof that no certificate was requested';
592
- return String(logs.stdout).split('\n').some((line) => ACME_ACTIVITY.test(line)) ? 'the rehearsal log shows certificate activity (a certificate was requested)' : null;
593
+ const logs = remoteDocker(run, ['service', 'logs', '--raw', '--no-task-ids', rehearsalName(ingress)]);
594
+ if (logs.status !== 0)
595
+ return 'cannot read the rehearsal log: no proof that no certificate was requested';
596
+ return String(logs.stdout).split('\n').some((line) => ACME_ACTIVITY.test(line)) ? 'the rehearsal log shows certificate activity (a certificate was requested)' : null;
593
597
  }
594
-
595
598
  /** Brings the rehearsal up, probes every host and checks the log. Returns { failure, rows }. */
596
599
  async function rehearsalBody(ctx, run, config, ingress, context) {
597
- const failure = await rehearsalUp(ctx, run, config, ingress, context);
598
- if (failure !== null) return { failure, rows: [] };
599
- const compared = await settle(ctx, SETTLE_ATTEMPTS, SETTLE_MS, () => {
600
- const rows = compareRehearsal(run, ingress, context.base);
601
- return { ok: rows.every((row) => row.ok), rows };
602
- });
603
- context.steps.push('probe');
604
- const problem = acmeLogProblem(run, ingress);
605
- context.steps.push('acme-log');
606
- const mismatched = compared.rows.filter((row) => !row.ok).map((row) => label(row.target));
607
- const failures = [mismatched.length > 0 ? `mismatch on ${mismatched.join(', ')}` : null, problem].filter(Boolean);
608
- return { failure: failures.length > 0 ? failures.join('; ') : null, rows: compared.rows };
609
- }
610
-
600
+ const failure = await rehearsalUp(ctx, run, config, ingress, context);
601
+ if (failure !== null)
602
+ return { failure, rows: [] };
603
+ const compared = await settle(ctx, SETTLE_ATTEMPTS, SETTLE_MS, () => {
604
+ const rows = compareRehearsal(run, ingress, context.base);
605
+ return { ok: rows.every((row) => row.ok), rows };
606
+ });
607
+ context.steps.push('probe');
608
+ const problem = acmeLogProblem(run, ingress);
609
+ context.steps.push('acme-log');
610
+ const mismatched = compared.rows.filter((row) => !row.ok).map((row) => label(row.target));
611
+ const failures = [mismatched.length > 0 ? `mismatch on ${mismatched.join(', ')}` : null, problem].filter((item) => Boolean(item));
612
+ return { failure: failures.length > 0 ? failures.join('; ') : null, rows: compared.rows };
613
+ }
611
614
  /** Removes what was created: the service, then its tasks, then the volume. Always reports each step. */
612
615
  async function rehearsalTeardown(ctx, run, ingress, created, steps) {
613
- const lines = [];
614
- let clean = true;
615
- const record = (name, done, text) => {
616
- steps.push(`teardown:${name}`);
617
- lines.push(done ? `teardown: ${text}` : `TEARDOWN FAILED: ${text} not done`);
618
- clean = clean && done;
619
- };
620
- if (created.service) {
621
- record('service-rm', remoteDocker(run, ['service', 'rm', rehearsalName(ingress)]).status === 0, 'service removed');
622
- const gone = await settle(ctx, READY_ATTEMPTS, SETTLE_MS, () => ({ ok: String(remoteDocker(run, ['ps', '-q', '--filter', `label=com.docker.swarm.service.name=${rehearsalName(ingress)}`]).stdout).trim() === '' }));
623
- record('tasks-gone', gone.ok, 'tasks gone');
624
- }
625
- if (created.volume) {
626
- const removed = await settle(ctx, SETTLE_ATTEMPTS, SETTLE_MS, () => ({ ok: remoteDocker(run, ['volume', 'rm', rehearsalVolume(ingress)]).status === 0 }));
627
- record('volume-rm', removed.ok, 'volume removed');
628
- }
629
- if (!clean) lines.push(`manual: remove the service ${rehearsalName(ingress)} and the volume ${rehearsalVolume(ingress)}`);
630
- return { clean, lines };
631
- }
632
-
616
+ const lines = [];
617
+ let clean = true;
618
+ const record = (name, done, text) => {
619
+ steps.push(`teardown:${name}`);
620
+ lines.push(done ? `teardown: ${text}` : `TEARDOWN FAILED: ${text} not done`);
621
+ clean = clean && done;
622
+ };
623
+ if (created.service) {
624
+ record('service-rm', remoteDocker(run, ['service', 'rm', rehearsalName(ingress)]).status === 0, 'service removed');
625
+ const gone = await settle(ctx, READY_ATTEMPTS, SETTLE_MS, () => ({ ok: String(remoteDocker(run, ['ps', '-q', '--filter', `label=com.docker.swarm.service.name=${rehearsalName(ingress)}`]).stdout).trim() === '' }));
626
+ record('tasks-gone', gone.ok, 'tasks gone');
627
+ }
628
+ if (created.volume) {
629
+ const removed = await settle(ctx, SETTLE_ATTEMPTS, SETTLE_MS, () => ({ ok: remoteDocker(run, ['volume', 'rm', rehearsalVolume(ingress)]).status === 0 }));
630
+ record('volume-rm', removed.ok, 'volume removed');
631
+ }
632
+ if (!clean)
633
+ lines.push(`manual: remove the service ${rehearsalName(ingress)} and the volume ${rehearsalVolume(ingress)}`);
634
+ return { clean, lines };
635
+ }
633
636
  async function rehearsalRun(ctx, run, config, ingress, context) {
634
- const created = { volume: false, service: false };
635
- let body = { failure: 'rehearsal did not run', rows: [] };
636
- let down = { clean: true, lines: [] };
637
- try {
638
- body = await rehearsalBody(ctx, run, config, ingress, { ...context, created });
639
- } finally {
640
- down = await rehearsalTeardown(ctx, run, ingress, created, context.steps);
641
- }
642
- const lines = [...context.lines, ...body.rows.map(rowLine), ...down.lines];
643
- if (body.failure !== null) lines.push(`rehearsal FAILED: ${body.failure}`);
644
- const ok = body.failure === null && down.clean;
645
- if (ok) lines.push('rehearsal passed: every host matches the live edge and nothing of the rehearsal is left');
646
- const hosts = body.rows.map((row) => ({ host: row.target.host, path: row.target.path, status: row.code ?? null, certificate: certificateVerdict(row) }));
647
- return result(ok, lines, context.steps, { hosts, teardown: down.clean ? 'clean' : 'failed' });
648
- }
649
-
637
+ const created = { volume: false, service: false };
638
+ let body = { failure: 'rehearsal did not run', rows: [] };
639
+ let down = { clean: true, lines: [] };
640
+ try {
641
+ body = await rehearsalBody(ctx, run, config, ingress, { ...context, created });
642
+ }
643
+ finally {
644
+ down = await rehearsalTeardown(ctx, run, ingress, created, context.steps);
645
+ }
646
+ const lines = [...context.lines, ...body.rows.map(rowLine), ...down.lines];
647
+ if (body.failure !== null)
648
+ lines.push(`rehearsal FAILED: ${body.failure}`);
649
+ const ok = body.failure === null && down.clean;
650
+ if (ok)
651
+ lines.push('rehearsal passed: every host matches the live edge and nothing of the rehearsal is left');
652
+ const hosts = body.rows.map((row) => ({ host: row.target.host, path: row.target.path, status: row.code ?? null, certificate: certificateVerdict(row) }));
653
+ return result(ok, lines, context.steps, { hosts, teardown: down.clean ? 'clean' : 'failed' });
654
+ }
650
655
  async function rehearseAction(ctx, run, config, ingress, execute) {
651
- const steps = ['plan'];
652
- const found = gather(run, config, ingress);
653
- if (found.error) return result(false, [found.error], steps);
654
- const spec = ingressSpec(run, ingress);
655
- const checks = rehearseChecks(run, config, ingress, found, spec);
656
- const lines = checks.map((check) => (check.ok ? `precondition ok: ${check.name}` : `precondition FAILED: ${check.name} (${check.detail})`));
657
- steps.push('preconditions');
658
- if (checks.some((check) => !check.ok)) return result(false, [...lines, 'rehearsal refused: nothing was changed'], steps);
659
- const base = baselines(run, targetsOf(ingress, found.declared));
660
- steps.push('baseline');
661
- for (const row of base) lines.push(`live edge ${label(row.target)}: status ${row.code}`);
662
- if (!execute) {
663
- const ports = ingress.takeover.rehearsalPorts;
664
- lines.push(`dry run: would copy ${ingress.acmeVolume} read-only into ${rehearsalVolume(ingress)}, start ${rehearsalName(ingress)} on host ports ${ports.http}/${ports.https} with no certificate requests, compare ${base.length} host(s) with the live edge and tear everything down`);
665
- return result(true, lines, steps);
666
- }
667
- return rehearsalRun(ctx, run, config, ingress, { spec, base, lines, steps });
656
+ const steps = ['plan'];
657
+ const found = gather(run, config, ingress);
658
+ if (found.error !== undefined)
659
+ return result(false, [found.error], steps);
660
+ const spec = ingressSpec(run, ingress);
661
+ const checks = rehearseChecks(run, config, ingress, found, spec);
662
+ const readySpec = spec; // a missing spec fails the checks below, which return before it is used
663
+ const lines = checks.map((check) => (check.ok ? `precondition ok: ${check.name}` : `precondition FAILED: ${check.name} (${check.detail})`));
664
+ steps.push('preconditions');
665
+ if (checks.some((check) => !check.ok))
666
+ return result(false, [...lines, 'rehearsal refused: nothing was changed'], steps);
667
+ const base = baselines(run, targetsOf(ingress, found.declared));
668
+ steps.push('baseline');
669
+ for (const row of base)
670
+ lines.push(`live edge ${label(row.target)}: status ${row.code}`);
671
+ if (!execute) {
672
+ const ports = ingress.takeover.rehearsalPorts;
673
+ lines.push(`dry run: would copy ${ingress.acmeVolume} read-only into ${rehearsalVolume(ingress)}, start ${rehearsalName(ingress)} on host ports ${ports.http}/${ports.https} with no certificate requests, compare ${base.length} host(s) with the live edge and tear everything down`);
674
+ return result(true, lines, steps);
675
+ }
676
+ return rehearsalRun(ctx, run, config, ingress, { spec: readySpec, base, lines, steps });
668
677
  }
669
-
670
678
  // ---- entry --------------------------------------------------------------------------------------------------------------------------
671
-
672
679
  /** The probe helpers the publish action shares with the takeover (injected so the two modules never import each other). */
673
680
  const PUBLISH_KIT = {
674
- readRecipient,
675
- stampOf,
676
- observe,
677
- baselines,
678
- settle,
679
- serviceState,
680
- liveCommand,
681
- targets: (config, ingress) => targetsOf(ingress, declaredHosts(ingress, readOwnRoutes(config, ingress))),
681
+ readRecipient,
682
+ stampOf,
683
+ observe,
684
+ baselines,
685
+ settle,
686
+ serviceState,
687
+ liveCommand,
688
+ targets: (config, ingress) => targetsOf(ingress, declaredHosts(ingress, readOwnRoutes(config, ingress))),
682
689
  };
683
-
684
690
  const ACTIONS = ['plan', 'attach', 'create', 'takeover', 'rehearse', 'publish'];
685
-
691
+ const isAction = (value) => ACTIONS.includes(value);
686
692
  /**
687
693
  * `starci app stack ingress <plan|attach|create|takeover|rehearse|publish>`; effects need execute === true.
688
694
  * deps.ssh an injected ssh runner (specs).
689
695
  */
690
696
  export async function runIngress(ctx, config, { action, execute = false, args = [], sshCmd = null }, deps = {}) {
691
- if (!ACTIONS.includes(action)) throw refusal('STACK_USAGE', `starci app stack ingress takes ${ACTIONS.join(' | ')}`);
692
- if (args.length > 0) throw refusal('STACK_USAGE', `ingress ${action} takes no further argument (got ${args.join(' ')})`);
693
- const { ingress } = config;
694
- if (!ingress) throw refusal('STACK_INGRESS_UNDECLARED', 'deploy.ingress is not declared for this environment');
695
- return withRunner(ctx, config, { sshCmd }, deps, async (run, notes) => {
696
- const outcome = await {
697
- plan: () => planAction(run, config, ingress),
698
- attach: () => attachAction(run, ingress, execute),
699
- create: () => createIngress(run, ingress, execute),
700
- takeover: () => takeoverAction(ctx, run, config, ingress, execute),
701
- rehearse: () => rehearseAction(ctx, run, config, ingress, execute),
702
- publish: () => publishIngress(ctx, run, config, ingress, execute, PUBLISH_KIT),
703
- }[action]();
704
- return { ...outcome, lines: [...notes, ...outcome.lines], data: { action, ...outcome.data } };
705
- });
697
+ if (!isAction(action))
698
+ throw refusal('STACK_USAGE', `starci app stack ingress takes ${ACTIONS.join(' | ')}`);
699
+ if (args.length > 0)
700
+ throw refusal('STACK_USAGE', `ingress ${action} takes no further argument (got ${args.join(' ')})`);
701
+ const { ingress } = config;
702
+ if (!ingress)
703
+ throw refusal('STACK_INGRESS_UNDECLARED', 'deploy.ingress is not declared for this environment');
704
+ return withRunner(ctx, config, { sshCmd }, deps, async (run, notes) => {
705
+ const table = {
706
+ plan: () => planAction(run, config, ingress),
707
+ attach: () => attachAction(run, ingress, execute),
708
+ create: () => createIngress(run, ingress, execute),
709
+ takeover: () => takeoverAction(ctx, run, config, ingress, execute),
710
+ rehearse: () => rehearseAction(ctx, run, config, ingress, execute),
711
+ publish: () => publishIngress(ctx, run, config, ingress, execute, PUBLISH_KIT),
712
+ };
713
+ const outcome = await table[action]();
714
+ return { ...outcome, lines: [...notes, ...outcome.lines], data: { action, ...outcome.data } };
715
+ });
706
716
  }