hecks 3.4.2 → 3.6.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 (1099) hide show
  1. checksums.yaml +4 -4
  2. data/examples/pizzas/bluebook/hecksagon/mock_stripe_payment_adapter.rb +1 -1
  3. data/lib/hecks/adapters/driven/claude_code.rb +11 -7
  4. data/lib/hecks/adapters/driven/d1/connection.rb +90 -0
  5. data/lib/hecks/adapters/driven/d1.rb +48 -314
  6. data/lib/hecks/adapters/driven/folder.rb +20 -10
  7. data/lib/hecks/adapters/driven/google_authentication.rb +18 -4
  8. data/lib/hecks/adapters/driven/governance_authorization.rb +2 -2
  9. data/lib/hecks/adapters/driven/heki/journal.rb +11 -10
  10. data/lib/hecks/adapters/driven/heki/saga_store.rb +4 -6
  11. data/lib/hecks/adapters/driven/heki/sagas.rb +55 -0
  12. data/lib/hecks/adapters/driven/heki.rb +21 -66
  13. data/lib/hecks/adapters/driven/in_memory_ordering.rb +37 -12
  14. data/lib/hecks/adapters/driven/lambda/client.rb +1 -1
  15. data/lib/hecks/adapters/driven/lambda.rb +1 -8
  16. data/lib/hecks/adapters/driven/memory/outbox_rows.rb +76 -0
  17. data/lib/hecks/adapters/driven/memory/shared_elements/values.rb +117 -0
  18. data/lib/hecks/adapters/driven/memory/shared_elements.rb +10 -97
  19. data/lib/hecks/adapters/driven/memory.rb +3 -65
  20. data/lib/hecks/adapters/driven/postgres/codec.rb +29 -26
  21. data/lib/hecks/adapters/driven/postgres/ddl.rb +50 -0
  22. data/lib/hecks/adapters/driven/postgres/dialect.rb +93 -0
  23. data/lib/hecks/adapters/driven/postgres/events.rb +57 -0
  24. data/lib/hecks/adapters/driven/postgres/journal.rb +127 -0
  25. data/lib/hecks/adapters/driven/postgres/outbox.rb +39 -31
  26. data/lib/hecks/adapters/driven/postgres/repository.rb +190 -0
  27. data/lib/hecks/adapters/driven/postgres/sagas.rb +72 -0
  28. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +13 -41
  29. data/lib/hecks/adapters/driven/postgres.rb +61 -528
  30. data/lib/hecks/adapters/driven/prism.rb +1 -1
  31. data/lib/hecks/adapters/driven/sql_query_builder.rb +45 -53
  32. data/lib/hecks/adapters/driven/sql_value_members.rb +57 -0
  33. data/lib/hecks/adapters/driven/sqlite/codec.rb +32 -29
  34. data/lib/hecks/adapters/driven/sqlite/ddl.rb +68 -0
  35. data/lib/hecks/adapters/driven/sqlite/dialect.rb +55 -0
  36. data/lib/hecks/adapters/driven/sqlite/events.rb +61 -0
  37. data/lib/hecks/adapters/driven/sqlite/journal.rb +125 -0
  38. data/lib/hecks/adapters/driven/sqlite/outbox.rb +98 -0
  39. data/lib/hecks/adapters/driven/sqlite/projection.rb +82 -63
  40. data/lib/hecks/adapters/driven/sqlite/repository.rb +146 -0
  41. data/lib/hecks/adapters/driven/sqlite/sagas.rb +69 -0
  42. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +8 -55
  43. data/lib/hecks/adapters/driven/sqlite.rb +36 -513
  44. data/lib/hecks/adapters/driven/tenant_provisioner.rb +13 -7
  45. data/lib/hecks/adapters/driving/github_webhook.rb +11 -8
  46. data/lib/hecks/behaviors/dsl.rb +17 -12
  47. data/lib/hecks/behaviors/expectations/boot.rb +81 -0
  48. data/lib/hecks/behaviors/expectations/checks.rb +178 -0
  49. data/lib/hecks/behaviors/expectations/dispatching.rb +93 -0
  50. data/lib/hecks/behaviors/expectations/qualify.rb +81 -0
  51. data/lib/hecks/behaviors/expectations.rb +39 -355
  52. data/lib/hecks/behaviors/rspec.rb +12 -12
  53. data/lib/hecks/behaviors/runner.rb +20 -11
  54. data/lib/hecks/bench/cli.rb +16 -13
  55. data/lib/hecks/bench/environment.rb +1 -1
  56. data/lib/hecks/bench/report.rb +11 -7
  57. data/lib/hecks/bench/ruby_runner.rb +13 -8
  58. data/lib/hecks/bench/rust_runner.rb +19 -11
  59. data/lib/hecks/bench/suite.rb +48 -20
  60. data/lib/hecks/bench/workload.rb +42 -31
  61. data/lib/hecks/bluebook/aggregate.rb +15 -21
  62. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +18 -21
  63. data/lib/hecks/bluebook/assembly/build.rb +12 -7
  64. data/lib/hecks/bluebook/assembly/contracts/read_models.rb +70 -0
  65. data/lib/hecks/bluebook/assembly/contracts/rules.rb +84 -0
  66. data/lib/hecks/bluebook/assembly/contracts/sagas.rb +109 -0
  67. data/lib/hecks/bluebook/assembly/contracts/structure.rb +91 -0
  68. data/lib/hecks/bluebook/assembly/contracts.rb +9 -319
  69. data/lib/hecks/bluebook/assembly/marks.rb +9 -8
  70. data/lib/hecks/bluebook/attribute.rb +13 -10
  71. data/lib/hecks/bluebook/behaviour/aggregate.rb +10 -3
  72. data/lib/hecks/bluebook/behaviour/read_model.rb +26 -13
  73. data/lib/hecks/bluebook/chapter.rb +22 -14
  74. data/lib/hecks/bluebook/command.rb +14 -18
  75. data/lib/hecks/bluebook/domain_port.rb +11 -8
  76. data/lib/hecks/bluebook/dsl/aggregate_builder/correction_sealing.rb +78 -0
  77. data/lib/hecks/bluebook/dsl/aggregate_builder/draining.rb +74 -0
  78. data/lib/hecks/bluebook/dsl/aggregate_builder/query_field_sealing.rb +105 -0
  79. data/lib/hecks/bluebook/dsl/aggregate_builder/query_sealing.rb +121 -0
  80. data/lib/hecks/bluebook/dsl/aggregate_builder/relationships.rb +128 -0
  81. data/lib/hecks/bluebook/dsl/aggregate_builder/rules.rb +84 -0
  82. data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +61 -249
  83. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +51 -285
  84. data/lib/hecks/bluebook/dsl/attribute_collector/identity_resolution.rb +113 -0
  85. data/lib/hecks/bluebook/dsl/attribute_collector.rb +58 -117
  86. data/lib/hecks/bluebook/dsl/binding_proxy.rb +24 -19
  87. data/lib/hecks/bluebook/dsl/bluebook_builder/pending_givens.rb +109 -0
  88. data/lib/hecks/bluebook/dsl/bluebook_builder/validation/correlation_keys.rb +105 -0
  89. data/lib/hecks/bluebook/dsl/bluebook_builder/validation/event_shapes.rb +126 -0
  90. data/lib/hecks/bluebook/dsl/bluebook_builder/validation/hop_tails.rb +79 -0
  91. data/lib/hecks/bluebook/dsl/bluebook_builder/validation/projected_fields.rb +76 -0
  92. data/lib/hecks/bluebook/dsl/bluebook_builder/validation/provisions.rb +65 -0
  93. data/lib/hecks/bluebook/dsl/bluebook_builder/validation/query_hops.rb +123 -0
  94. data/lib/hecks/bluebook/dsl/bluebook_builder/validation/references.rb +112 -0
  95. data/lib/hecks/bluebook/dsl/bluebook_builder/validation/with_projections.rb +126 -0
  96. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +34 -595
  97. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +18 -107
  98. data/lib/hecks/bluebook/dsl/command_builder/implicit_attributes.rb +132 -0
  99. data/lib/hecks/bluebook/dsl/command_builder/mutations.rb +190 -0
  100. data/lib/hecks/bluebook/dsl/command_builder/owner.rb +22 -0
  101. data/lib/hecks/bluebook/dsl/command_builder/rules.rb +57 -0
  102. data/lib/hecks/bluebook/dsl/command_builder.rb +60 -360
  103. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +19 -13
  104. data/lib/hecks/bluebook/dsl/entity_builder/rules.rb +96 -0
  105. data/lib/hecks/bluebook/dsl/entity_builder/scope.rb +25 -0
  106. data/lib/hecks/bluebook/dsl/entity_builder/sealing.rb +94 -0
  107. data/lib/hecks/bluebook/dsl/entity_builder.rb +35 -169
  108. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +53 -24
  109. data/lib/hecks/bluebook/dsl/hecksagon_builder/chapter_scope.rb +133 -0
  110. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +3 -97
  111. data/lib/hecks/bluebook/dsl/identity_declaration.rb +73 -73
  112. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +7 -3
  113. data/lib/hecks/bluebook/dsl/need_word.rb +1 -1
  114. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +34 -29
  115. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +57 -41
  116. data/lib/hecks/bluebook/dsl/query_builder.rb +12 -15
  117. data/lib/hecks/bluebook/dsl/read_model_builder/sealing.rb +116 -0
  118. data/lib/hecks/bluebook/dsl/read_model_builder.rb +25 -102
  119. data/lib/hecks/bluebook/dsl/rule_reference.rb +26 -12
  120. data/lib/hecks/bluebook/dsl/translation_builder.rb +24 -14
  121. data/lib/hecks/bluebook/dsl/value_object_builder.rb +29 -19
  122. data/lib/hecks/bluebook/dsl/word_gate.rb +34 -27
  123. data/lib/hecks/bluebook/entity.rb +13 -17
  124. data/lib/hecks/bluebook/expression/ast_json/emitters.rb +93 -0
  125. data/lib/hecks/bluebook/expression/ast_json/emitters.rb-e +88 -0
  126. data/lib/hecks/bluebook/expression/ast_json.rb +49 -76
  127. data/lib/hecks/bluebook/expression/ast_reader.rb +95 -28
  128. data/lib/hecks/bluebook/expression/canonical_form.rb +6 -26
  129. data/lib/hecks/bluebook/expression/evaluator/parser.rb +62 -0
  130. data/lib/hecks/bluebook/expression/evaluator/scanning.rb +122 -0
  131. data/lib/hecks/bluebook/expression/evaluator.rb +11 -111
  132. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +28 -44
  133. data/lib/hecks/bluebook/expression/resolver/lookups.rb +77 -0
  134. data/lib/hecks/bluebook/expression/resolver/operations.rb +78 -0
  135. data/lib/hecks/bluebook/expression/resolver/parsing.rb +110 -0
  136. data/lib/hecks/bluebook/expression/resolver/scanning.rb +139 -0
  137. data/lib/hecks/bluebook/expression/resolver/values.rb +115 -0
  138. data/lib/hecks/bluebook/expression/resolver.rb +16 -414
  139. data/lib/hecks/bluebook/{hexagon.rb → hecksagon.rb} +22 -14
  140. data/lib/hecks/bluebook/keyword_fields.rb +42 -0
  141. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +7 -6
  142. data/lib/hecks/bluebook/meta_validator/judge/cells.rb +99 -0
  143. data/lib/hecks/bluebook/meta_validator/judge/dispatching.rb +111 -0
  144. data/lib/hecks/bluebook/meta_validator/judge/identity.rb +108 -0
  145. data/lib/hecks/bluebook/meta_validator/judge/walking.rb +141 -0
  146. data/lib/hecks/bluebook/meta_validator/judge.rb +31 -376
  147. data/lib/hecks/bluebook/meta_validator/modes.rb +137 -0
  148. data/lib/hecks/bluebook/meta_validator/plan/category.rb +44 -0
  149. data/lib/hecks/bluebook/meta_validator/plan/verbs.rb +73 -0
  150. data/lib/hecks/bluebook/meta_validator/plan.rb +38 -122
  151. data/lib/hecks/bluebook/meta_validator/readings/fields.rb +100 -0
  152. data/lib/hecks/bluebook/meta_validator/readings.rb +10 -95
  153. data/lib/hecks/bluebook/meta_validator/reconstruction/heads.rb +89 -0
  154. data/lib/hecks/bluebook/meta_validator/reconstruction/reading.rb +96 -0
  155. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +25 -151
  156. data/lib/hecks/bluebook/meta_validator/shapes/helpers.rb +83 -0
  157. data/lib/hecks/bluebook/meta_validator/shapes.rb +8 -75
  158. data/lib/hecks/bluebook/meta_validator/syntax_boot/disk_cache.rb +77 -0
  159. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +31 -95
  160. data/lib/hecks/bluebook/meta_validator/translation_judge/rules.rb +74 -0
  161. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +15 -65
  162. data/lib/hecks/bluebook/meta_validator/verdict_cache/tagging.rb +63 -0
  163. data/lib/hecks/bluebook/meta_validator/verdict_cache.rb +22 -43
  164. data/lib/hecks/bluebook/meta_validator/world_judge.rb +8 -4
  165. data/lib/hecks/bluebook/meta_validator.rb +59 -175
  166. data/lib/hecks/bluebook/model_check/lifecycle_checks.rb +122 -0
  167. data/lib/hecks/bluebook/model_check/policy_checks.rb +129 -0
  168. data/lib/hecks/bluebook/model_check/saga_checks.rb +134 -0
  169. data/lib/hecks/bluebook/model_check/verbs.rb +69 -0
  170. data/lib/hecks/bluebook/model_check.rb +73 -413
  171. data/lib/hecks/bluebook/pattern_subset/refusals.rb +54 -0
  172. data/lib/hecks/bluebook/pattern_subset/scan.rb +58 -0
  173. data/lib/hecks/bluebook/pattern_subset.rb +93 -134
  174. data/lib/hecks/bluebook/process_manager.rb +10 -9
  175. data/lib/hecks/bluebook/project_discovery.rb +10 -7
  176. data/lib/hecks/bluebook/project_register.rb +42 -41
  177. data/lib/hecks/bluebook/query.rb +19 -12
  178. data/lib/hecks/bluebook/read_model.rb +41 -39
  179. data/lib/hecks/bluebook/smoke_test.rb +51 -36
  180. data/lib/hecks/bluebook/synthesizer.rb +13 -5
  181. data/lib/hecks/bluebook/translation.rb +12 -12
  182. data/lib/hecks/bluebook.rb +1 -1
  183. data/lib/hecks/chapters.rb +32 -16
  184. data/lib/hecks/cli/behaviors.rb +22 -15
  185. data/lib/hecks/cli/check_era.rb +2 -2
  186. data/lib/hecks/cli/command_table.rb +72 -0
  187. data/lib/hecks/cli/console.rb +10 -7
  188. data/lib/hecks/cli/deploy_template_diff.rb +22 -9
  189. data/lib/hecks/cli/document.rb +14 -10
  190. data/lib/hecks/cli/domain_stub.rb +1 -1
  191. data/lib/hecks/cli/domain_writer.rb +1 -1
  192. data/lib/hecks/cli/expression_projection.rb +8 -6
  193. data/lib/hecks/cli/follow.rb +10 -4
  194. data/lib/hecks/cli/interview_aggregate.rb +132 -0
  195. data/lib/hecks/cli/interview_draft.rb +16 -200
  196. data/lib/hecks/cli/interview_record.rb +130 -0
  197. data/lib/hecks/cli/interview_run.rb +19 -8
  198. data/lib/hecks/cli/interview_session/agent_state.rb +70 -0
  199. data/lib/hecks/cli/interview_session/catalog.rb +76 -0
  200. data/lib/hecks/cli/interview_session/manual_findings.rb +48 -0
  201. data/lib/hecks/cli/interview_session/proposals.rb +90 -0
  202. data/lib/hecks/cli/interview_session.rb +8 -205
  203. data/lib/hecks/cli/interview_shape.rb +13 -6
  204. data/lib/hecks/cli/ir.rb +8 -11
  205. data/lib/hecks/cli/launcher_words.rb +168 -0
  206. data/lib/hecks/cli/model_check/examination.rb +93 -0
  207. data/lib/hecks/cli/model_check/sources.rb +57 -0
  208. data/lib/hecks/cli/model_check.rb +28 -134
  209. data/lib/hecks/cli/project_cli/launcher_source.rb +177 -0
  210. data/lib/hecks/cli/project_cli/templates.rb +106 -0
  211. data/lib/hecks/cli/project_cli.rb +57 -236
  212. data/lib/hecks/cli/project_diagrams.rb +26 -4
  213. data/lib/hecks/cli/project_glossary.rb +19 -9
  214. data/lib/hecks/cli/refresh_rspec_runtime_baseline.rb +29 -13
  215. data/lib/hecks/cli/release.rb +29 -22
  216. data/lib/hecks/cli/release_gem.rb +12 -6
  217. data/lib/hecks/cli/rspec_io_parallel_files.rb +87 -30
  218. data/lib/hecks/cli/rspec_shard_files.rb +24 -9
  219. data/lib/hecks/cli/run.rb +25 -44
  220. data/lib/hecks/cli/run_expectations.rb +63 -0
  221. data/lib/hecks/cli/seed_semantics_corpus.rb +37 -17
  222. data/lib/hecks/cli/smoke_http/checks.rb +66 -0
  223. data/lib/hecks/cli/smoke_http/settings.rb +63 -0
  224. data/lib/hecks/cli/smoke_http/transport.rb +45 -0
  225. data/lib/hecks/cli/smoke_http.rb +8 -142
  226. data/lib/hecks/cli/smoke_test.rb +18 -4
  227. data/lib/hecks/cli/stores.rb +7 -4
  228. data/lib/hecks/cli/stress_concurrency_specs/reporting.rb +44 -0
  229. data/lib/hecks/cli/stress_concurrency_specs/usage.rb +28 -0
  230. data/lib/hecks/cli/stress_concurrency_specs.rb +82 -80
  231. data/lib/hecks/cli.rb +32 -208
  232. data/lib/hecks/codemod/runner.rb +92 -0
  233. data/lib/hecks/codemod/sweep.rb +94 -0
  234. data/lib/hecks/codemod.rb +22 -165
  235. data/lib/hecks/corpus/routes.rb +34 -0
  236. data/lib/hecks/corpus/rust.rb +193 -0
  237. data/lib/hecks/corpus/sources.rb +46 -0
  238. data/lib/hecks/corpus.rb +28 -265
  239. data/lib/hecks/deploy/bluebook/deploy.bluebook +1510 -3
  240. data/lib/hecks/deploy/bluebook/deploy.hecksagon +57 -0
  241. data/lib/hecks/deploy/oidc.json +225 -0
  242. data/lib/hecks/doc/reference/coverage.rb +70 -0
  243. data/lib/hecks/doc/reference/harvester.rb +55 -0
  244. data/lib/hecks/doc/reference/readme.rb +89 -0
  245. data/lib/hecks/doc/reference/rendering.rb +149 -0
  246. data/lib/hecks/doc/reference.rb +14 -273
  247. data/lib/hecks/doors/cli_door/nesting.rb +34 -0
  248. data/lib/hecks/doors/cli_door/shorthand.rb +85 -0
  249. data/lib/hecks/doors/cli_door.rb +23 -95
  250. data/lib/hecks/doors/cli_runner/answers.rb +138 -0
  251. data/lib/hecks/doors/cli_runner/resolution.rb +146 -0
  252. data/lib/hecks/doors/cli_runner/settling.rb +115 -0
  253. data/lib/hecks/doors/cli_runner/streaming.rb +90 -0
  254. data/lib/hecks/doors/cli_runner/suggestions.rb +43 -0
  255. data/lib/hecks/doors/cli_runner.rb +12 -336
  256. data/lib/hecks/doors/command_request.rb +42 -31
  257. data/lib/hecks/doors/handle.rb +32 -30
  258. data/lib/hecks/doors/launcher_options.rb +62 -7
  259. data/lib/hecks/doors/mcp_door/properties.rb +47 -0
  260. data/lib/hecks/doors/mcp_door/tool_calls.rb +113 -0
  261. data/lib/hecks/doors/mcp_door/tool_definitions/auditing.rb +51 -0
  262. data/lib/hecks/doors/mcp_door/tool_definitions/calling.rb +69 -0
  263. data/lib/hecks/doors/mcp_door/tool_definitions/reading.rb +97 -0
  264. data/lib/hecks/doors/mcp_door/tool_definitions.rb +15 -0
  265. data/lib/hecks/doors/mcp_door.rb +55 -301
  266. data/lib/hecks/doors/mcp_door_scope/argument_policy.rb +92 -0
  267. data/lib/hecks/doors/mcp_door_scope/env_problems.rb +55 -0
  268. data/lib/hecks/doors/mcp_door_scope/presenter.rb +78 -0
  269. data/lib/hecks/doors/mcp_door_scope.rb +16 -172
  270. data/lib/hecks/doors/ruby_door/aggregate_door.rb +1 -1
  271. data/lib/hecks/doors/ruby_door/chapter.rb +33 -16
  272. data/lib/hecks/doors/ruby_door.rb +14 -11
  273. data/lib/hecks/doors/usage_cache.rb +14 -8
  274. data/lib/hecks/doors.rb +10 -4
  275. data/lib/hecks/embryonaut_bluebook/manifest.rb +11 -3
  276. data/lib/hecks/embryonaut_bluebook/registry/changelog.rb +26 -0
  277. data/lib/hecks/embryonaut_bluebook/registry/versions.rb +27 -0
  278. data/lib/hecks/embryonaut_bluebook/registry.rb +27 -37
  279. data/lib/hecks/embryonaut_bluebook/vendor/acceptance.rb +37 -0
  280. data/lib/hecks/embryonaut_bluebook/vendor.rb +22 -36
  281. data/lib/hecks/embryonaut_bluebook/vendor_cli.rb +9 -6
  282. data/lib/hecks/embryonaut_bluebook.rb +16 -11
  283. data/lib/hecks/engine_agreement.rb +24 -17
  284. data/lib/hecks/forms/app/commands.rb +98 -0
  285. data/lib/hecks/forms/app/queries.rb +62 -0
  286. data/lib/hecks/forms/app/records.rb +67 -0
  287. data/lib/hecks/forms/app/responses.rb +46 -0
  288. data/lib/hecks/forms/app.rb +33 -221
  289. data/lib/hecks/forms/banking_presentation.rb +5 -0
  290. data/lib/hecks/forms/command_form_renderer.rb +55 -14
  291. data/lib/hecks/forms/field_controls.rb +95 -0
  292. data/lib/hecks/forms/field_renderer.rb +34 -88
  293. data/lib/hecks/forms/field_shape.rb +9 -34
  294. data/lib/hecks/forms/html.rb +1 -1
  295. data/lib/hecks/forms/index_renderer.rb +14 -9
  296. data/lib/hecks/forms/page.rb +2 -106
  297. data/lib/hecks/forms/page_assets.rb +88 -0
  298. data/lib/hecks/forms/page_script.rb +28 -0
  299. data/lib/hecks/forms/params.rb +17 -8
  300. data/lib/hecks/forms/query_form_renderer.rb +11 -7
  301. data/lib/hecks/forms/record_renderer.rb +28 -19
  302. data/lib/hecks/forms/reference_options.rb +1 -1
  303. data/lib/hecks/forms/text_shape.rb +40 -0
  304. data/lib/hecks/fqn.rb +64 -32
  305. data/lib/hecks/framework.rb +10 -3
  306. data/lib/hecks/freezer.rb +10 -1
  307. data/lib/hecks/fuzzing/bounded_exhaustive_expressions/fragments.rb +61 -0
  308. data/lib/hecks/fuzzing/bounded_exhaustive_expressions/productions.rb +217 -0
  309. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +5 -194
  310. data/lib/hecks/fuzzing/combination_miner.rb +23 -12
  311. data/lib/hecks/fuzzing/concurrent_dispatch/racing.rb +64 -0
  312. data/lib/hecks/fuzzing/concurrent_dispatch/world_copy.rb +66 -0
  313. data/lib/hecks/fuzzing/concurrent_dispatch.rb +31 -107
  314. data/lib/hecks/fuzzing/coverage_campaign.rb +7 -7
  315. data/lib/hecks/fuzzing/differential/wire_comparison.rb +74 -0
  316. data/lib/hecks/fuzzing/differential.rb +59 -84
  317. data/lib/hecks/fuzzing/domain_generator/block_rendering.rb +70 -0
  318. data/lib/hecks/fuzzing/domain_generator/builder/attribute_shapes.rb +105 -0
  319. data/lib/hecks/fuzzing/domain_generator/builder/shapes.rb +97 -0
  320. data/lib/hecks/fuzzing/domain_generator/builder/wiring.rb +128 -0
  321. data/lib/hecks/fuzzing/domain_generator/builder.rb +105 -0
  322. data/lib/hecks/fuzzing/domain_generator/pruning.rb +90 -0
  323. data/lib/hecks/fuzzing/domain_generator/removals.rb +62 -0
  324. data/lib/hecks/fuzzing/domain_generator/rendering.rb +94 -0
  325. data/lib/hecks/fuzzing/domain_generator/tokens.rb +38 -0
  326. data/lib/hecks/fuzzing/domain_generator.rb +7 -555
  327. data/lib/hecks/fuzzing/era_boundary.rb +34 -24
  328. data/lib/hecks/fuzzing/form_census.rb +18 -8
  329. data/lib/hecks/fuzzing/generated_domain_check.rb +23 -11
  330. data/lib/hecks/fuzzing/isolated_boot/copying.rb +62 -0
  331. data/lib/hecks/fuzzing/isolated_boot/postgres_scratch.rb +133 -0
  332. data/lib/hecks/fuzzing/isolated_boot/worlds.rb +102 -0
  333. data/lib/hecks/fuzzing/isolated_boot.rb +25 -252
  334. data/lib/hecks/fuzzing/persistence_parity.rb +5 -5
  335. data/lib/hecks/fuzzing/properties/aggregation_recompute.rb +121 -0
  336. data/lib/hecks/fuzzing/properties/catalog.rb +138 -0
  337. data/lib/hecks/fuzzing/properties/corrections.rb +23 -20
  338. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +64 -185
  339. data/lib/hecks/fuzzing/properties/group_by_recompute.rb +69 -0
  340. data/lib/hecks/fuzzing/properties/guards.rb +76 -108
  341. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +66 -227
  342. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +46 -35
  343. data/lib/hecks/fuzzing/properties/mutation_recompute.rb +162 -0
  344. data/lib/hecks/fuzzing/properties/outbox.rb +41 -24
  345. data/lib/hecks/fuzzing/properties/piece_invariants.rb +61 -0
  346. data/lib/hecks/fuzzing/properties/policy_wiring.rb +140 -0
  347. data/lib/hecks/fuzzing/properties/query_rows.rb +67 -0
  348. data/lib/hecks/fuzzing/properties/querying.rb +69 -90
  349. data/lib/hecks/fuzzing/properties/references.rb +100 -0
  350. data/lib/hecks/fuzzing/properties/tenant_scope.rb +84 -0
  351. data/lib/hecks/fuzzing/properties.rb +6 -119
  352. data/lib/hecks/fuzzing/qa_settings.rb +23 -17
  353. data/lib/hecks/fuzzing/replay/fan_out.rb +78 -0
  354. data/lib/hecks/fuzzing/replay/filters.rb +72 -0
  355. data/lib/hecks/fuzzing/replay/guard_check.rb +88 -0
  356. data/lib/hecks/fuzzing/replay/mutation_trace.rb +117 -0
  357. data/lib/hecks/fuzzing/replay/observation.rb +60 -0
  358. data/lib/hecks/fuzzing/replay/read_only_steps.rb +84 -0
  359. data/lib/hecks/fuzzing/replay/session.rb +151 -0
  360. data/lib/hecks/fuzzing/replay.rb +35 -442
  361. data/lib/hecks/fuzzing/rust_gap_manifest.rb +3 -4
  362. data/lib/hecks/fuzzing/self_consistency/saga_redelivery.rb +133 -0
  363. data/lib/hecks/fuzzing/self_consistency/sagas.rb +93 -0
  364. data/lib/hecks/fuzzing/self_consistency/value_objects.rb +87 -0
  365. data/lib/hecks/fuzzing/self_consistency.rb +8 -213
  366. data/lib/hecks/fuzzing/sequence_generator/adversary/argument_mutations.rb +116 -0
  367. data/lib/hecks/fuzzing/sequence_generator/adversary/caller_draw.rb +68 -0
  368. data/lib/hecks/fuzzing/sequence_generator/adversary/identity_mutations.rb +62 -0
  369. data/lib/hecks/fuzzing/sequence_generator/adversary/precedence_mutations.rb +126 -0
  370. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +23 -283
  371. data/lib/hecks/fuzzing/sequence_generator/argument_drawing.rb +78 -0
  372. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +69 -53
  373. data/lib/hecks/fuzzing/sequence_generator/identity_shaping.rb +85 -0
  374. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +20 -16
  375. data/lib/hecks/fuzzing/sequence_generator/picker.rb +12 -6
  376. data/lib/hecks/fuzzing/sequence_generator/prefixing.rb +45 -0
  377. data/lib/hecks/fuzzing/sequence_generator/query_binding.rb +19 -8
  378. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +43 -144
  379. data/lib/hecks/fuzzing/sequence_generator.rb +65 -63
  380. data/lib/hecks/fuzzing/shrinker.rb +33 -29
  381. data/lib/hecks/fuzzing/target_capabilities.rb +10 -4
  382. data/lib/hecks/fuzzing/value_generator.rb +27 -17
  383. data/lib/hecks/gate/stages.yml +10 -0
  384. data/lib/hecks/grammar/evolve/arguments.rb +90 -0
  385. data/lib/hecks/grammar/evolve/keywords.rb +81 -0
  386. data/lib/hecks/grammar/evolve/seeds.rb +172 -0
  387. data/lib/hecks/grammar/evolve.rb +22 -263
  388. data/lib/hecks/grammar/operators.rb +111 -0
  389. data/lib/hecks/grammar.rb +53 -126
  390. data/lib/hecks/hecks/adapters/bluebook_report.rb +91 -0
  391. data/lib/hecks/hecks/adapters/codebase/codemods.rb +1 -1
  392. data/lib/hecks/hecks/adapters/codebase/corpus_tasks.rb +23 -9
  393. data/lib/hecks/hecks/adapters/codebase/language/edits.rb +75 -0
  394. data/lib/hecks/hecks/adapters/codebase/language.rb +30 -72
  395. data/lib/hecks/hecks/adapters/codebase/promotion/candidates.rb +54 -0
  396. data/lib/hecks/hecks/adapters/codebase/promotion/gate.rb +50 -0
  397. data/lib/hecks/hecks/adapters/codebase/promotion/move.rb +70 -0
  398. data/lib/hecks/hecks/adapters/codebase/promotion/standing.rb +88 -0
  399. data/lib/hecks/hecks/adapters/codebase/promotion/watch.rb +83 -0
  400. data/lib/hecks/hecks/adapters/codebase/promotion.rb +115 -0
  401. data/lib/hecks/hecks/adapters/codebase/publishing.rb +31 -12
  402. data/lib/hecks/hecks/adapters/codebase/regeneration.rb +43 -6
  403. data/lib/hecks/hecks/adapters/codebase/release_facts.rb +16 -5
  404. data/lib/hecks/hecks/adapters/codebase/source_tree.rb +9 -1
  405. data/lib/hecks/hecks/adapters/codebase/sqlite_fixture.rb +1 -1
  406. data/lib/hecks/hecks/adapters/codebase/style.rb +1 -1
  407. data/lib/hecks/hecks/adapters/codebase/test_runner.rb +11 -5
  408. data/lib/hecks/hecks/adapters/codebase/test_suite.rb +27 -8
  409. data/lib/hecks/hecks/adapters/console_capture.rb +21 -17
  410. data/lib/hecks/hecks/adapters/cost_explorer.rb +11 -6
  411. data/lib/hecks/hecks/adapters/deploy_plans.rb +49 -0
  412. data/lib/hecks/hecks/adapters/deploy_toolchain/answers.rb +141 -0
  413. data/lib/hecks/hecks/adapters/deploy_toolchain/scripts.rb +95 -0
  414. data/lib/hecks/hecks/adapters/deploy_toolchain/statuses.rb +74 -0
  415. data/lib/hecks/hecks/adapters/deploy_toolchain.rb +122 -59
  416. data/lib/hecks/hecks/adapters/finding_github.rb +2 -2
  417. data/lib/hecks/hecks/adapters/first_admin.rb +22 -8
  418. data/lib/hecks/hecks/adapters/git.rb +18 -5
  419. data/lib/hecks/hecks/adapters/git_refs.rb +144 -0
  420. data/lib/hecks/hecks/adapters/github_rulesets.rb +126 -0
  421. data/lib/hecks/hecks/adapters/host_http.rb +16 -6
  422. data/lib/hecks/hecks/adapters/in_process_boot.rb +9 -5
  423. data/lib/hecks/hecks/adapters/in_process_operations/tailing.rb +61 -0
  424. data/lib/hecks/hecks/adapters/in_process_operations.rb +39 -37
  425. data/lib/hecks/hecks/adapters/journal_store/changes.rb +21 -15
  426. data/lib/hecks/hecks/adapters/journal_store/edge_audit.rb +29 -12
  427. data/lib/hecks/hecks/adapters/journal_store/readings/attestation.rb +52 -0
  428. data/lib/hecks/hecks/adapters/journal_store/readings/scaffolding.rb +52 -0
  429. data/lib/hecks/hecks/adapters/journal_store/readings.rb +5 -62
  430. data/lib/hecks/hecks/adapters/local_files.rb +27 -10
  431. data/lib/hecks/hecks/adapters/pg_admin/ownership.rb +115 -0
  432. data/lib/hecks/hecks/adapters/pg_admin.rb +14 -87
  433. data/lib/hecks/hecks/adapters/process_pool.rb +54 -22
  434. data/lib/hecks/hecks/adapters/rust_toolchain.rb +12 -4
  435. data/lib/hecks/hecks/adapters/site_toolchain.rb +19 -10
  436. data/lib/hecks/hecks/adapters/terminal.rb +13 -9
  437. data/lib/hecks/hecks/codebase.bluebook +322 -5
  438. data/lib/hecks/hecks/custodian.bluebook +12 -2
  439. data/lib/hecks/hecks/hecks.hecksagon +73 -0
  440. data/lib/hecks/hecks/hecks.world +15 -3
  441. data/lib/hecks/kernel_capabilities/templates.rb +92 -0
  442. data/lib/hecks/kernel_capabilities.rb +18 -85
  443. data/lib/hecks/language/bluebook/vocabulary.bluebook +43 -1
  444. data/lib/hecks/literal/item_splitter.rb +50 -0
  445. data/lib/hecks/literal.rb +29 -38
  446. data/lib/hecks/mcp_stdio_guard.rb +8 -8
  447. data/lib/hecks/naming.rb +9 -3
  448. data/lib/hecks/persistence_legacy_fixture/postgres_stores.rb +81 -0
  449. data/lib/hecks/persistence_legacy_fixture/regenerate.rb +26 -66
  450. data/lib/hecks/persistence_legacy_fixture.rb +13 -10
  451. data/lib/hecks/ports/access_control.rb +13 -10
  452. data/lib/hecks/ports/agent/answers.rb +2 -2
  453. data/lib/hecks/ports/agent.rb +13 -10
  454. data/lib/hecks/ports/authentication.rb +13 -10
  455. data/lib/hecks/ports/authorization.rb +13 -10
  456. data/lib/hecks/ports/clock.rb +13 -10
  457. data/lib/hecks/ports/extraction.rb +13 -12
  458. data/lib/hecks/ports/identity_assignment.rb +13 -10
  459. data/lib/hecks/ports/identity_generation.rb +13 -10
  460. data/lib/hecks/ports/identity_resolution.rb +13 -10
  461. data/lib/hecks/ports/key_vault.rb +13 -10
  462. data/lib/hecks/ports/persistence/append_only.rb +5 -5
  463. data/lib/hecks/ports/persistence/binding_policy.rb +33 -9
  464. data/lib/hecks/ports/persistence/codec_boundary.rb +22 -11
  465. data/lib/hecks/ports/persistence/constants.rb +10 -0
  466. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +18 -16
  467. data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +27 -18
  468. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +52 -23
  469. data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +14 -8
  470. data/lib/hecks/ports/persistence/plugins/era/expected_era.rb +17 -6
  471. data/lib/hecks/ports/persistence/plugins/era/lineage/coverage.rb +58 -0
  472. data/lib/hecks/ports/persistence/plugins/era/lineage/rewrite.rb +130 -0
  473. data/lib/hecks/ports/persistence/plugins/era/lineage/rules.rb +25 -0
  474. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +20 -180
  475. data/lib/hecks/ports/persistence/plugins/era/postgres_era/boot.rb +41 -0
  476. data/lib/hecks/ports/persistence/plugins/era/postgres_era/class_methods.rb +105 -0
  477. data/lib/hecks/ports/persistence/plugins/era/postgres_era/dialect.rb +137 -0
  478. data/lib/hecks/ports/persistence/plugins/era/postgres_era/events.rb +25 -28
  479. data/lib/hecks/ports/persistence/plugins/era/postgres_era/head_writes.rb +62 -0
  480. data/lib/hecks/ports/persistence/plugins/era/postgres_era/journal.rb +128 -0
  481. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/base_sql.rb +74 -0
  482. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/chain_sql.rb +169 -0
  483. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/domain_rename.rb +91 -0
  484. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_integrity.rb +99 -0
  485. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +0 -73
  486. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +55 -39
  487. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +51 -222
  488. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/merge_writes.rb +120 -0
  489. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +85 -60
  490. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +97 -181
  491. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +19 -19
  492. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/sql_templates.rb +88 -0
  493. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +77 -118
  494. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +3 -117
  495. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_sql.rb +116 -0
  496. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +3 -1
  497. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/context.rb +38 -0
  498. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +43 -25
  499. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +62 -38
  500. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +21 -16
  501. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +66 -86
  502. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/refusals.rb +75 -0
  503. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +27 -9
  504. data/lib/hecks/ports/persistence/plugins/era/postgres_era/query_phases.rb +63 -0
  505. data/lib/hecks/ports/persistence/plugins/era/postgres_era/sagas.rb +78 -0
  506. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +28 -482
  507. data/lib/hecks/ports/persistence/plugins/era/translation/approval_file.rb +40 -12
  508. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +22 -15
  509. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +39 -19
  510. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +16 -16
  511. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +48 -25
  512. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +60 -99
  513. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/path_diff.rb +57 -0
  514. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +21 -11
  515. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +13 -4
  516. data/lib/hecks/ports/persistence/postgres_dump/connection.rb +4 -2
  517. data/lib/hecks/ports/persistence/postgres_dump.rb +13 -7
  518. data/lib/hecks/ports/persistence/repository_factory.rb +35 -11
  519. data/lib/hecks/ports/persistence/state_codec.rb +10 -0
  520. data/lib/hecks/ports/persistence.rb +1 -10
  521. data/lib/hecks/ports/projection.rb +34 -21
  522. data/lib/hecks/ports/query/in_memory.rb +16 -8
  523. data/lib/hecks/projection_files/expression_tables.rb +64 -0
  524. data/lib/hecks/projection_files/texts.rb +67 -0
  525. data/lib/hecks/projection_files.rb +19 -103
  526. data/lib/hecks/projections/bootstrap_table.rb +43 -29
  527. data/lib/hecks/projections/deploy/bastion_lines.rb +32 -0
  528. data/lib/hecks/projections/deploy/box/access.rb +73 -0
  529. data/lib/hecks/projections/deploy/box/caddy.rb +87 -0
  530. data/lib/hecks/projections/deploy/box/compose.rb +70 -0
  531. data/lib/hecks/projections/deploy/box/hosting.rb +16 -108
  532. data/lib/hecks/projections/deploy/box/hosting_parts.rb +97 -0
  533. data/lib/hecks/projections/deploy/box/hosting_settings.rb +1 -1
  534. data/lib/hecks/projections/deploy/box/roll_recipe.rb +62 -0
  535. data/lib/hecks/projections/deploy/box/roll_scripts.rb +135 -0
  536. data/lib/hecks/projections/deploy/box/settings/checks.rb +38 -0
  537. data/lib/hecks/projections/deploy/box/settings/containers.rb +123 -0
  538. data/lib/hecks/projections/deploy/box/settings/origin.rb +56 -0
  539. data/lib/hecks/projections/deploy/box/settings/patterns.rb +43 -0
  540. data/lib/hecks/projections/deploy/box/settings/types.rb +72 -0
  541. data/lib/hecks/projections/deploy/box/settings.rb +58 -263
  542. data/lib/hecks/projections/deploy/box/stack_yaml.rb +107 -0
  543. data/lib/hecks/projections/deploy/box/templates/MIGRATION.md.tmpl +39 -0
  544. data/lib/hecks/projections/deploy/box/templates/Makefile.tmpl +24 -0
  545. data/lib/hecks/projections/deploy/box/templates/deploy-box.sh.tmpl +51 -4
  546. data/lib/hecks/projections/deploy/box/templates/deploy-service.services-roll.part +4 -3
  547. data/lib/hecks/projections/deploy/box/templates/deploy-service.sh.tmpl +16 -4
  548. data/lib/hecks/projections/deploy/box/templates/deploy-service.taskdef-roll.part +8 -7
  549. data/lib/hecks/projections/deploy/box/templates/deploy-service.taskdef.part +1 -1
  550. data/lib/hecks/projections/deploy/box/templates/deploy-usage-services.tmpl +4 -0
  551. data/lib/hecks/projections/deploy/box/templates/deploy-usage-taskdef.tmpl +5 -0
  552. data/lib/hecks/projections/deploy/box/templates/ecr-repository.yaml.tmpl +9 -0
  553. data/lib/hecks/projections/deploy/box/templates/hosting.mk.tmpl +6 -3
  554. data/lib/hecks/projections/deploy/box/templates/mode-summary-services.tmpl +2 -0
  555. data/lib/hecks/projections/deploy/box/templates/mode-summary-taskdef.tmpl +4 -0
  556. data/lib/hecks/projections/deploy/box/templates/restore-to-rds.sh.tmpl +8 -3
  557. data/lib/hecks/projections/deploy/box/templates/services-block.tmpl +9 -0
  558. data/lib/hecks/projections/deploy/box/templates/tunnel-egress.yaml.tmpl +11 -0
  559. data/lib/hecks/projections/deploy/box/templates/verify-copy.sh.tmpl +2 -2
  560. data/lib/hecks/projections/deploy/box/templates/writable-secrets.yaml.tmpl +7 -0
  561. data/lib/hecks/projections/deploy/box.rb +37 -503
  562. data/lib/hecks/projections/deploy/fargate/assembly.rb +13 -142
  563. data/lib/hecks/projections/deploy/fargate/cdn/behaviors.rb +76 -0
  564. data/lib/hecks/projections/deploy/fargate/cdn/lines.rb +112 -0
  565. data/lib/hecks/projections/deploy/fargate/cdn/options.rb +85 -0
  566. data/lib/hecks/projections/deploy/fargate/cdn.rb +23 -215
  567. data/lib/hecks/projections/deploy/fargate/check.rb +16 -6
  568. data/lib/hecks/projections/deploy/fargate/containers/definition_lines.rb +61 -0
  569. data/lib/hecks/projections/deploy/fargate/containers/reading.rb +86 -0
  570. data/lib/hecks/projections/deploy/fargate/containers/rendering.rb +132 -0
  571. data/lib/hecks/projections/deploy/fargate/containers/routing.rb +93 -0
  572. data/lib/hecks/projections/deploy/fargate/containers.rb +10 -286
  573. data/lib/hecks/projections/deploy/fargate/extras/reading.rb +90 -0
  574. data/lib/hecks/projections/deploy/fargate/extras/rendering.rb +81 -0
  575. data/lib/hecks/projections/deploy/fargate/extras.rb +9 -158
  576. data/lib/hecks/projections/deploy/fargate/monitoring/alarms.rb +115 -0
  577. data/lib/hecks/projections/deploy/fargate/monitoring/reading.rb +84 -0
  578. data/lib/hecks/projections/deploy/fargate/monitoring/topic.rb +59 -0
  579. data/lib/hecks/projections/deploy/fargate/monitoring/warmer.rb +90 -0
  580. data/lib/hecks/projections/deploy/fargate/monitoring/warmer_roles.rb +76 -0
  581. data/lib/hecks/projections/deploy/fargate/monitoring.rb +14 -348
  582. data/lib/hecks/projections/deploy/fargate/sections.rb +115 -0
  583. data/lib/hecks/projections/deploy/fargate/settings/ids.rb +55 -0
  584. data/lib/hecks/projections/deploy/fargate/settings/tuning.rb +77 -0
  585. data/lib/hecks/projections/deploy/fargate/settings.rb +17 -104
  586. data/lib/hecks/projections/deploy/fargate/stack/blocks.rb +91 -0
  587. data/lib/hecks/projections/deploy/fargate/stack/readers.rb +87 -0
  588. data/lib/hecks/projections/deploy/fargate/stack/recipes.rb +106 -0
  589. data/lib/hecks/projections/deploy/fargate/stack.rb +146 -0
  590. data/lib/hecks/projections/deploy/fargate/task_sections.rb +56 -0
  591. data/lib/hecks/projections/deploy/fargate/yaml.rb +1 -1
  592. data/lib/hecks/projections/deploy/fargate.rb +51 -756
  593. data/lib/hecks/projections/deploy/lambda/pii.rb +49 -0
  594. data/lib/hecks/projections/deploy/lambda/stack/blocks.rb +118 -0
  595. data/lib/hecks/projections/deploy/lambda/stack/checks.rb +64 -0
  596. data/lib/hecks/projections/deploy/lambda/stack/deploy_chain.rb +119 -0
  597. data/lib/hecks/projections/deploy/lambda/stack/readers.rb +62 -0
  598. data/lib/hecks/projections/deploy/lambda/stack/reading.rb +170 -0
  599. data/lib/hecks/projections/deploy/lambda/stack/reading_web.rb +53 -0
  600. data/lib/hecks/projections/deploy/lambda/stack/recipes.rb +112 -0
  601. data/lib/hecks/projections/deploy/lambda/stack/web.rb +57 -0
  602. data/lib/hecks/projections/deploy/lambda/stack.rb +54 -0
  603. data/lib/hecks/projections/deploy/lambda.rb +77 -1885
  604. data/lib/hecks/projections/deploy/preview/access_yaml.rb +8 -74
  605. data/lib/hecks/projections/deploy/preview/containers.rb +18 -7
  606. data/lib/hecks/projections/deploy/preview/routing_yaml.rb +15 -87
  607. data/lib/hecks/projections/deploy/preview/settings.rb +7 -4
  608. data/lib/hecks/projections/deploy/preview/task_yaml.rb +42 -106
  609. data/lib/hecks/projections/deploy/preview/template.rb +11 -106
  610. data/lib/hecks/projections/deploy/preview/yaml_text.rb +1 -1
  611. data/lib/hecks/projections/deploy/scripts/hosting.mk.tmpl +1 -1
  612. data/lib/hecks/projections/deploy/scripts/settings.rb +14 -6
  613. data/lib/hecks/projections/deploy/scripts.rb +11 -9
  614. data/lib/hecks/projections/deploy/shared/rows.rb +48 -0
  615. data/lib/hecks/projections/deploy/shared.rb +89 -449
  616. data/lib/hecks/projections/deploy/smoke.rb +15 -10
  617. data/lib/hecks/projections/deploy/template_diff/comparer.rb +21 -147
  618. data/lib/hecks/projections/deploy/template_diff/normalizer.rb +60 -0
  619. data/lib/hecks/projections/deploy/template_diff/value_diff.rb +136 -0
  620. data/lib/hecks/projections/deploy/template_diff.rb +8 -3
  621. data/lib/hecks/projections/deploy/templates/fargate/Dockerfile.tmpl +28 -0
  622. data/lib/hecks/projections/deploy/templates/fargate/Makefile.tmpl +63 -0
  623. data/lib/hecks/projections/deploy/templates/fargate/always_params.tmpl +4 -0
  624. data/lib/hecks/projections/deploy/templates/fargate/deploy_plain.tmpl +1 -0
  625. data/lib/hecks/projections/deploy/templates/fargate/deploy_shared.tmpl +11 -0
  626. data/lib/hecks/projections/deploy/templates/fargate/mint_era_own.tmpl +44 -0
  627. data/lib/hecks/projections/deploy/templates/fargate/mint_era_shared.tmpl +8 -0
  628. data/lib/hecks/projections/deploy/templates/fargate/missing_owner.tmpl +9 -0
  629. data/lib/hecks/projections/deploy/templates/fargate/oauth_params.tmpl +7 -0
  630. data/lib/hecks/projections/deploy/templates/fargate/oauth_task_env.tmpl +4 -0
  631. data/lib/hecks/projections/deploy/templates/fargate/oauth_task_policy.tmpl +7 -0
  632. data/lib/hecks/projections/deploy/templates/fargate/owning_params.tmpl +33 -0
  633. data/lib/hecks/projections/deploy/templates/fargate/template.yaml.tmpl +281 -0
  634. data/lib/hecks/projections/deploy/templates/lambda/Makefile.tmpl +218 -0
  635. data/lib/hecks/projections/deploy/templates/lambda/aurora_depends.tmpl +18 -0
  636. data/lib/hecks/projections/deploy/templates/lambda/build_dispatch_none.tmpl +10 -0
  637. data/lib/hecks/projections/deploy/templates/lambda/build_pg.tmpl +15 -0
  638. data/lib/hecks/projections/deploy/templates/lambda/build_plain.tmpl +2 -0
  639. data/lib/hecks/projections/deploy/templates/lambda/build_web.tmpl +11 -0
  640. data/lib/hecks/projections/deploy/templates/lambda/env_blind_guard.tmpl +4 -0
  641. data/lib/hecks/projections/deploy/templates/lambda/mint_era_own.tmpl +122 -0
  642. data/lib/hecks/projections/deploy/templates/lambda/mint_era_shared.tmpl +47 -0
  643. data/lib/hecks/projections/deploy/templates/lambda/missing_owner.tmpl +9 -0
  644. data/lib/hecks/projections/deploy/templates/lambda/nat_comment_oauth.tmpl +14 -0
  645. data/lib/hecks/projections/deploy/templates/lambda/nat_comment_plain.tmpl +3 -0
  646. data/lib/hecks/projections/deploy/templates/lambda/oauth_note.tmpl +8 -0
  647. data/lib/hecks/projections/deploy/templates/lambda/params_oauth.tmpl +13 -0
  648. data/lib/hecks/projections/deploy/templates/lambda/params_shared.tmpl +33 -0
  649. data/lib/hecks/projections/deploy/templates/lambda/pg_native.tmpl +71 -0
  650. data/lib/hecks/projections/deploy/templates/lambda/pii_oac.tmpl +8 -0
  651. data/lib/hecks/projections/deploy/templates/lambda/pii_permission.tmpl +8 -0
  652. data/lib/hecks/projections/deploy/templates/lambda/pii_resources.tmpl +116 -0
  653. data/lib/hecks/projections/deploy/templates/lambda/predeploy_bridge.tmpl +30 -0
  654. data/lib/hecks/projections/deploy/templates/lambda/predeploy_bridge_oauth.tmpl +12 -0
  655. data/lib/hecks/projections/deploy/templates/lambda/predeploy_bridge_plain.tmpl +7 -0
  656. data/lib/hecks/projections/deploy/templates/lambda/rename_schema_own.tmpl +93 -0
  657. data/lib/hecks/projections/deploy/templates/lambda/rename_schema_shared.tmpl +7 -0
  658. data/lib/hecks/projections/deploy/templates/lambda/rust_oauth_env.tmpl +17 -0
  659. data/lib/hecks/projections/deploy/templates/lambda/rust_oauth_policy.tmpl +16 -0
  660. data/lib/hecks/projections/deploy/templates/lambda/rust_session_secret.tmpl +13 -0
  661. data/lib/hecks/projections/deploy/templates/lambda/samconfig.toml.tmpl +12 -0
  662. data/lib/hecks/projections/deploy/templates/lambda/shared_note.tmpl +6 -0
  663. data/lib/hecks/projections/deploy/templates/lambda/sync_oauth.tmpl +23 -0
  664. data/lib/hecks/projections/deploy/templates/lambda/template.yaml.tmpl +187 -0
  665. data/lib/hecks/projections/deploy/templates/lambda/translation_own.tmpl +46 -0
  666. data/lib/hecks/projections/deploy/templates/lambda/translation_shared.tmpl +2 -0
  667. data/lib/hecks/projections/deploy/templates/lambda/web_function.tmpl +207 -0
  668. data/lib/hecks/projections/deploy/templates/lambda/web_google_oauth_env.tmpl +12 -0
  669. data/lib/hecks/projections/deploy/templates/lambda/web_no_oauth_env.tmpl +7 -0
  670. data/lib/hecks/projections/deploy/templates/lambda/web_oauth_policy.tmpl +4 -0
  671. data/lib/hecks/projections/deploy/templates/lambda/webhook_secret_resource.tmpl +20 -0
  672. data/lib/hecks/projections/deploy/templates/preview/alb.tmpl +8 -0
  673. data/lib/hecks/projections/deploy/templates/preview/alb_security_group.tmpl +11 -0
  674. data/lib/hecks/projections/deploy/templates/preview/assume_role.tmpl +6 -0
  675. data/lib/hecks/projections/deploy/templates/preview/base_parameters.tmpl +24 -0
  676. data/lib/hecks/projections/deploy/templates/preview/cluster.tmpl +4 -0
  677. data/lib/hecks/projections/deploy/templates/preview/container_secret.tmpl +9 -0
  678. data/lib/hecks/projections/deploy/templates/preview/db_init_script.sh.tmpl +23 -0
  679. data/lib/hecks/projections/deploy/templates/preview/db_init_secrets.tmpl +5 -0
  680. data/lib/hecks/projections/deploy/templates/preview/db_init_task.tmpl +39 -0
  681. data/lib/hecks/projections/deploy/templates/preview/desired_count.tmpl +5 -0
  682. data/lib/hecks/projections/deploy/templates/preview/distribution.tmpl +24 -0
  683. data/lib/hecks/projections/deploy/templates/preview/execution_role.tmpl +16 -0
  684. data/lib/hecks/projections/deploy/templates/preview/header.tmpl +16 -0
  685. data/lib/hecks/projections/deploy/templates/preview/ingress.tmpl +8 -0
  686. data/lib/hecks/projections/deploy/templates/preview/listener.tmpl +9 -0
  687. data/lib/hecks/projections/deploy/templates/preview/listener_rule.tmpl +11 -0
  688. data/lib/hecks/projections/deploy/templates/preview/log_group.tmpl +5 -0
  689. data/lib/hecks/projections/deploy/templates/preview/log_options.tmpl +5 -0
  690. data/lib/hecks/projections/deploy/templates/preview/outputs.tmpl +14 -0
  691. data/lib/hecks/projections/deploy/templates/preview/service.tmpl +17 -0
  692. data/lib/hecks/projections/deploy/templates/preview/session_secret.tmpl +11 -0
  693. data/lib/hecks/projections/deploy/templates/preview/target_group.tmpl +9 -0
  694. data/lib/hecks/projections/deploy/templates/preview/task_definition.tmpl +15 -0
  695. data/lib/hecks/projections/deploy/templates/preview/task_role.tmpl +22 -0
  696. data/lib/hecks/projections/deploy/templates/shared/aurora_db.tmpl +52 -0
  697. data/lib/hecks/projections/deploy/templates/shared/bastion.tmpl +74 -0
  698. data/lib/hecks/projections/deploy/templates/shared/bastion_own_public.tmpl +41 -0
  699. data/lib/hecks/projections/deploy/templates/shared/bastion_reuse_public.tmpl +10 -0
  700. data/lib/hecks/projections/deploy/templates/shared/nat_gateway.tmpl +102 -0
  701. data/lib/hecks/projections/deploy/templates/shared/oauth_egress.tmpl +17 -0
  702. data/lib/hecks/projections/deploy/templates/shared/plain_db.tmpl +16 -0
  703. data/lib/hecks/projections/deploy/templates/shared/vpc_and_database.tmpl +67 -0
  704. data/lib/hecks/projections/deploy/text_template.rb +35 -0
  705. data/lib/hecks/projections/diagrams/dispatch.rb +114 -0
  706. data/lib/hecks/projections/diagrams/statecharts.rb +59 -0
  707. data/lib/hecks/projections/diagrams/structure.rb +114 -0
  708. data/lib/hecks/projections/diagrams.rb +33 -256
  709. data/lib/hecks/projections/glossary/entries.rb +140 -0
  710. data/lib/hecks/projections/glossary/html/page.rb +96 -0
  711. data/lib/hecks/projections/glossary/html/parser.rb +66 -0
  712. data/lib/hecks/projections/glossary/html/sections.rb +78 -0
  713. data/lib/hecks/projections/glossary/html.rb +7 -179
  714. data/lib/hecks/projections/glossary/index.rb +45 -0
  715. data/lib/hecks/projections/glossary/markdown.rb +22 -11
  716. data/lib/hecks/projections/glossary/mermaid.rb +44 -31
  717. data/lib/hecks/projections/glossary/sensitivity.rb +7 -4
  718. data/lib/hecks/projections/glossary/sentences/value_objects.rb +77 -0
  719. data/lib/hecks/projections/glossary/sentences.rb +42 -81
  720. data/lib/hecks/projections/glossary/slugs.rb +33 -0
  721. data/lib/hecks/projections/glossary.rb +22 -190
  722. data/lib/hecks/projections/model.rb +20 -14
  723. data/lib/hecks/projections/rust_vocabulary/arguments/support.rb +33 -0
  724. data/lib/hecks/projections/rust_vocabulary/arguments.rb +171 -0
  725. data/lib/hecks/projections/rust_vocabulary/enum_body.rb +114 -0
  726. data/lib/hecks/projections/rust_vocabulary/tests.rb +144 -0
  727. data/lib/hecks/projections/rust_vocabulary.rb +24 -293
  728. data/lib/hecks/projections/site/admin.rb +146 -0
  729. data/lib/hecks/projections/site/admin_cms/templates/membership.rb +115 -0
  730. data/lib/hecks/projections/site/admin_cms/templates/session_strategy.rb +90 -0
  731. data/lib/hecks/projections/site/admin_cms/templates/sso.rb +101 -0
  732. data/lib/hecks/projections/site/admin_cms/templates/sso_tail.rb +54 -0
  733. data/lib/hecks/projections/site/admin_cms/templates/users.rb +83 -0
  734. data/lib/hecks/projections/site/admin_cms/templates.rb +33 -0
  735. data/lib/hecks/projections/site/admin_cms.rb +78 -0
  736. data/lib/hecks/projections/site/edge/behaviours.rb +10 -6
  737. data/lib/hecks/projections/site/edge/checks.rb +30 -80
  738. data/lib/hecks/projections/site/edge/findings.rb +28 -0
  739. data/lib/hecks/projections/site/edge/route_checks.rb +110 -0
  740. data/lib/hecks/projections/site/edge/row_reader.rb +52 -0
  741. data/lib/hecks/projections/site/edge/rules.rb +28 -26
  742. data/lib/hecks/projections/site/edge.rb +20 -31
  743. data/lib/hecks/projections/site/live_edge.rb +9 -7
  744. data/lib/hecks/projections/site/payload/fields/definitions.rb +90 -0
  745. data/lib/hecks/projections/site/payload/fields/readers.rb +69 -0
  746. data/lib/hecks/projections/site/payload/fields.rb +137 -0
  747. data/lib/hecks/projections/site/payload/lifecycle/text.rb +84 -0
  748. data/lib/hecks/projections/site/payload/lifecycle/text_tail.rb +106 -0
  749. data/lib/hecks/projections/site/payload/lifecycle.rb +33 -0
  750. data/lib/hecks/projections/site/payload/model.rb +128 -0
  751. data/lib/hecks/projections/site/payload/specs/wire.rb +69 -0
  752. data/lib/hecks/projections/site/payload/specs.rb +135 -0
  753. data/lib/hecks/projections/site/payload_driver.rb +95 -0
  754. data/lib/hecks/projections/site/root_rows.rb +67 -0
  755. data/lib/hecks/projections/site/routes_ts/helpers.rb +79 -0
  756. data/lib/hecks/projections/site/routes_ts/listings.rb +75 -0
  757. data/lib/hecks/projections/site/routes_ts/navigation.rb +79 -0
  758. data/lib/hecks/projections/site/routes_ts.rb +27 -175
  759. data/lib/hecks/projections/site/site_admin/access.rb +104 -0
  760. data/lib/hecks/projections/site/site_admin/session.rb +127 -0
  761. data/lib/hecks/projections/site/site_admin.rb +85 -0
  762. data/lib/hecks/projections/site/site_cdn.rb +12 -7
  763. data/lib/hecks/projections/site/site_host/dockerfile.rb +75 -0
  764. data/lib/hecks/projections/site/site_host/templates.rb +109 -0
  765. data/lib/hecks/projections/site/site_host.rb +58 -0
  766. data/lib/hecks/projections/site/site_root/workflow.rb +65 -0
  767. data/lib/hecks/projections/site/site_root.rb +128 -0
  768. data/lib/hecks/projections/site/table/checks.rb +28 -57
  769. data/lib/hecks/projections/site/table/link_builder.rb +14 -5
  770. data/lib/hecks/projections/site/table/navigation_checks.rb +75 -0
  771. data/lib/hecks/projections/site/table/row_builder.rb +31 -53
  772. data/lib/hecks/projections/site/table/row_defaults.rb +65 -0
  773. data/lib/hecks/projections/site/table.rb +9 -5
  774. data/lib/hecks/projections/vocabulary.rb +34 -31
  775. data/lib/hecks/projector/cli_audience.rb +42 -0
  776. data/lib/hecks/projector/cli_projector/claims.rb +149 -0
  777. data/lib/hecks/projector/cli_projector/command_help.rb +65 -0
  778. data/lib/hecks/projector/cli_projector/command_specs.rb +133 -0
  779. data/lib/hecks/projector/cli_projector/listing.rb +100 -0
  780. data/lib/hecks/projector/cli_projector/option_specs.rb +116 -0
  781. data/lib/hecks/projector/cli_projector/query_specs.rb +67 -0
  782. data/lib/hecks/projector/cli_projector/usage.rb +101 -0
  783. data/lib/hecks/projector/cli_projector.rb +15 -532
  784. data/lib/hecks/projector/docs_projector/attributes.rb +78 -0
  785. data/lib/hecks/projector/docs_projector/behaviour.rb +127 -0
  786. data/lib/hecks/projector/docs_projector/closing.rb +40 -0
  787. data/lib/hecks/projector/docs_projector.rb +58 -192
  788. data/lib/hecks/projector/exporter/bindings.rb +174 -0
  789. data/lib/hecks/projector/exporter/translations.rb +127 -0
  790. data/lib/hecks/projector/exporter.rb +8 -279
  791. data/lib/hecks/projector/narrate_projector/commands.rb +108 -0
  792. data/lib/hecks/projector/narrate_projector/reactions.rb +33 -0
  793. data/lib/hecks/projector/narrate_projector.rb +60 -160
  794. data/lib/hecks/projector.rb +22 -13
  795. data/lib/hecks/quality_control/adapters/agent_profile.rb +25 -16
  796. data/lib/hecks/quality_control/adapters/github_checks.rb +42 -6
  797. data/lib/hecks/quality_control/adapters/github_ci_webhook.rb +1 -1
  798. data/lib/hecks/quality_control/adapters/github_issues.rb +22 -10
  799. data/lib/hecks/quality_control/cli/qa_discover_external_domains/parsing.rb +54 -0
  800. data/lib/hecks/quality_control/cli/qa_discover_external_domains/reporting.rb +44 -0
  801. data/lib/hecks/quality_control/cli/qa_discover_external_domains/scanning.rb +119 -0
  802. data/lib/hecks/quality_control/cli/qa_discover_external_domains.rb +28 -155
  803. data/lib/hecks/quality_control/cli/qa_domain_novelty/verdict.rb +76 -0
  804. data/lib/hecks/quality_control/cli/qa_domain_novelty.rb +33 -66
  805. data/lib/hecks/quality_control/cli/qa_generated_domains/arguments.rb +62 -0
  806. data/lib/hecks/quality_control/cli/qa_generated_domains/blueprints.rb +45 -0
  807. data/lib/hecks/quality_control/cli/qa_generated_domains/evaluation.rb +107 -0
  808. data/lib/hecks/quality_control/cli/qa_generated_domains/promotion.rb +107 -0
  809. data/lib/hecks/quality_control/cli/qa_generated_domains/reporting.rb +97 -0
  810. data/lib/hecks/quality_control/cli/qa_generated_domains/shrinking.rb +70 -0
  811. data/lib/hecks/quality_control/cli/qa_generated_domains.rb +43 -327
  812. data/lib/hecks/quality_control/cli/qa_log_bug/arguments.rb +55 -0
  813. data/lib/hecks/quality_control/cli/qa_log_bug/proof.rb +65 -0
  814. data/lib/hecks/quality_control/cli/qa_log_bug.rb +18 -81
  815. data/lib/hecks/quality_control/cli/qa_mine_combinations/arguments.rb +54 -0
  816. data/lib/hecks/quality_control/cli/qa_mine_combinations/booting.rb +96 -0
  817. data/lib/hecks/quality_control/cli/qa_mine_combinations.rb +45 -109
  818. data/lib/hecks/quality_control/cli/qa_open_pr/arguments.rb +55 -0
  819. data/lib/hecks/quality_control/cli/qa_open_pr/recording.rb +79 -0
  820. data/lib/hecks/quality_control/cli/qa_open_pr/rules.rb +122 -0
  821. data/lib/hecks/quality_control/cli/qa_open_pr.rb +41 -200
  822. data/lib/hecks/quality_control/cli/qa_postgres_migrate/copying.rb +45 -0
  823. data/lib/hecks/quality_control/cli/qa_postgres_migrate/reporting.rb +33 -0
  824. data/lib/hecks/quality_control/cli/qa_postgres_migrate.rb +37 -63
  825. data/lib/hecks/quality_control/cli/qa_postgres_role/ownership.rb +57 -0
  826. data/lib/hecks/quality_control/cli/qa_postgres_role.rb +66 -71
  827. data/lib/hecks/quality_control/cli/qa_pr_check/checking.rb +118 -0
  828. data/lib/hecks/quality_control/cli/qa_pr_check/reporting.rb +54 -0
  829. data/lib/hecks/quality_control/cli/qa_pr_check.rb +28 -145
  830. data/lib/hecks/quality_control/cli/qa_seed_angles/seed.rb +80 -0
  831. data/lib/hecks/quality_control/cli/qa_seed_angles.rb +13 -86
  832. data/lib/hecks/quality_control/cli/qa_sweep/all_mode/children.rb +80 -0
  833. data/lib/hecks/quality_control/cli/qa_sweep/all_mode/report.rb +125 -0
  834. data/lib/hecks/quality_control/cli/qa_sweep/all_mode.rb +63 -193
  835. data/lib/hecks/quality_control/cli/qa_sweep/checks/boundary.rb +92 -0
  836. data/lib/hecks/quality_control/cli/qa_sweep/checks/engines.rb +92 -0
  837. data/lib/hecks/quality_control/cli/qa_sweep/checks/seeds.rb +86 -0
  838. data/lib/hecks/quality_control/cli/qa_sweep/checks/shrinking.rb +69 -0
  839. data/lib/hecks/quality_control/cli/qa_sweep/checks.rb +10 -248
  840. data/lib/hecks/quality_control/cli/qa_sweep/conclusion.rb +60 -0
  841. data/lib/hecks/quality_control/cli/qa_sweep/depth.rb +61 -0
  842. data/lib/hecks/quality_control/cli/qa_sweep/dials.rb +26 -13
  843. data/lib/hecks/quality_control/cli/qa_sweep/finding_report/shrunk.rb +42 -0
  844. data/lib/hecks/quality_control/cli/qa_sweep/finding_report.rb +40 -41
  845. data/lib/hecks/quality_control/cli/qa_sweep/parsing/validation.rb +70 -0
  846. data/lib/hecks/quality_control/cli/qa_sweep/parsing.rb +35 -65
  847. data/lib/hecks/quality_control/cli/qa_sweep/release_mode.rb +41 -22
  848. data/lib/hecks/quality_control/cli/qa_sweep/scratch.rb +67 -0
  849. data/lib/hecks/quality_control/cli/qa_sweep/seat.rb +94 -0
  850. data/lib/hecks/quality_control/cli/qa_sweep/seed_loop.rb +149 -0
  851. data/lib/hecks/quality_control/cli/qa_sweep/target_setup.rb +115 -0
  852. data/lib/hecks/quality_control/cli/qa_sweep.rb +22 -445
  853. data/lib/hecks/quality_control/cli/qa_tick/output.rb +107 -0
  854. data/lib/hecks/quality_control/cli/qa_tick/preflight.rb +95 -0
  855. data/lib/hecks/quality_control/cli/qa_tick.rb +42 -175
  856. data/lib/hecks/query_ir/formatting.rb +86 -0
  857. data/lib/hecks/query_ir/rules.rb +132 -0
  858. data/lib/hecks/query_ir/touchpoints.rb +104 -0
  859. data/lib/hecks/query_ir.rb +33 -281
  860. data/lib/hecks/query_ir_mcp/tools.rb +68 -0
  861. data/lib/hecks/query_ir_mcp.rb +27 -71
  862. data/lib/hecks/query_specification/common/comparison.rb +25 -5
  863. data/lib/hecks/query_specification/common/dsl.rb +1 -1
  864. data/lib/hecks/query_specification/common/null_policy.rb +9 -6
  865. data/lib/hecks/query_specification/common/options.rb +21 -14
  866. data/lib/hecks/query_specification/field_path.rb +5 -2
  867. data/lib/hecks/query_specification/hop_path.rb +15 -18
  868. data/lib/hecks/release/lane.rb +22 -0
  869. data/lib/hecks/release/runner/clean_tree.rb +2 -2
  870. data/lib/hecks/release/runner/commands.rb +1 -1
  871. data/lib/hecks/release/runner/options.rb +82 -0
  872. data/lib/hecks/release/runner/preflight.rb +12 -7
  873. data/lib/hecks/release/runner/publishing.rb +86 -0
  874. data/lib/hecks/release/runner/tagger.rb +8 -3
  875. data/lib/hecks/release/runner.rb +40 -156
  876. data/lib/hecks/rendering.rb +13 -9
  877. data/lib/hecks/router/door.rb +37 -0
  878. data/lib/hecks/router/namespace_installer.rb +45 -83
  879. data/lib/hecks/router/options_proxy.rb +44 -0
  880. data/lib/hecks/runtime/adapter_lookup.rb +45 -24
  881. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +16 -8
  882. data/lib/hecks/runtime/command_interpreter/delegation.rb +116 -0
  883. data/lib/hecks/runtime/command_interpreter/enforcement.rb +38 -0
  884. data/lib/hecks/runtime/command_interpreter/hydration.rb +139 -0
  885. data/lib/hecks/runtime/command_interpreter/list_mutations.rb +98 -0
  886. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +67 -114
  887. data/lib/hecks/runtime/command_interpreter/persisting.rb +96 -0
  888. data/lib/hecks/runtime/command_interpreter.rb +57 -352
  889. data/lib/hecks/runtime/command_rules/admissibility.rb +64 -147
  890. data/lib/hecks/runtime/command_rules/arithmetic.rb +13 -72
  891. data/lib/hecks/runtime/command_rules/authorization.rb +9 -9
  892. data/lib/hecks/runtime/command_rules/emission.rb +13 -14
  893. data/lib/hecks/runtime/command_rules/guard_state.rb +49 -0
  894. data/lib/hecks/runtime/command_rules/invariants.rb +84 -0
  895. data/lib/hecks/runtime/command_rules/operands.rb +88 -0
  896. data/lib/hecks/runtime/command_rules/references.rb +59 -57
  897. data/lib/hecks/runtime/command_rules/tenant_boundary.rb +69 -0
  898. data/lib/hecks/runtime/dependency_planning/analyzer.rb +90 -0
  899. data/lib/hecks/runtime/dependency_planning/expression_reads.rb +73 -0
  900. data/lib/hecks/runtime/dependency_planning/mutation_analysis.rb +89 -0
  901. data/lib/hecks/runtime/dependency_planning/rule_analysis.rb +82 -0
  902. data/lib/hecks/runtime/dependency_planning.rb +3 -250
  903. data/lib/hecks/runtime/dispatcher/queries.rb +62 -0
  904. data/lib/hecks/runtime/dispatcher/result.rb +55 -0
  905. data/lib/hecks/runtime/dispatcher/routing.rb +107 -0
  906. data/lib/hecks/runtime/dispatcher.rb +64 -209
  907. data/lib/hecks/runtime/entity_element/location.rb +124 -0
  908. data/lib/hecks/runtime/entity_element/mutations.rb +150 -0
  909. data/lib/hecks/runtime/entity_element.rb +5 -177
  910. data/lib/hecks/runtime/entity_interpreter/enforcement.rb +51 -0
  911. data/lib/hecks/runtime/entity_interpreter/locating.rb +62 -0
  912. data/lib/hecks/runtime/entity_interpreter/resolution.rb +42 -0
  913. data/lib/hecks/runtime/entity_interpreter/saving.rb +49 -0
  914. data/lib/hecks/runtime/entity_interpreter.rb +45 -164
  915. data/lib/hecks/runtime/identity.rb +15 -9
  916. data/lib/hecks/runtime/instance.rb +11 -7
  917. data/lib/hecks/runtime/interpreting.rb +16 -8
  918. data/lib/hecks/runtime/invocation.rb +3 -155
  919. data/lib/hecks/runtime/invocation_builder.rb +132 -0
  920. data/lib/hecks/runtime/invocation_facts.rb +87 -0
  921. data/lib/hecks/runtime/loader/boot_steps.rb +96 -0
  922. data/lib/hecks/runtime/loader/described.rb +27 -0
  923. data/lib/hecks/runtime/loader.rb +27 -114
  924. data/lib/hecks/runtime/outbox/consumer_runner.rb +40 -0
  925. data/lib/hecks/runtime/outbox/fanout.rb +82 -0
  926. data/lib/hecks/runtime/outbox/relay.rb +149 -0
  927. data/lib/hecks/runtime/outbox.rb +3 -214
  928. data/lib/hecks/runtime/policy_interpreter/arguments.rb +67 -0
  929. data/lib/hecks/runtime/policy_interpreter/fan_out.rb +96 -0
  930. data/lib/hecks/runtime/policy_interpreter.rb +22 -137
  931. data/lib/hecks/runtime/port_operation_interpreter.rb +18 -17
  932. data/lib/hecks/runtime/query_interpreter/arguments.rb +51 -0
  933. data/lib/hecks/runtime/query_interpreter/entity_rows.rb +82 -0
  934. data/lib/hecks/runtime/query_interpreter/outside_answer.rb +89 -0
  935. data/lib/hecks/runtime/query_interpreter.rb +43 -191
  936. data/lib/hecks/runtime/reaction_invocation/identities.rb +92 -0
  937. data/lib/hecks/runtime/reaction_invocation/target_resolution.rb +88 -0
  938. data/lib/hecks/runtime/reaction_invocation.rb +66 -183
  939. data/lib/hecks/runtime/reaction_outcome.rb +7 -3
  940. data/lib/hecks/runtime/read_model_interpreter/head_ordering.rb +49 -0
  941. data/lib/hecks/runtime/read_model_interpreter/reductions.rb +123 -0
  942. data/lib/hecks/runtime/read_model_interpreter/shaping.rb +100 -0
  943. data/lib/hecks/runtime/read_model_interpreter.rb +71 -214
  944. data/lib/hecks/runtime/reference_hop.rb +11 -11
  945. data/lib/hecks/runtime/refusal_wording.rb +11 -8
  946. data/lib/hecks/runtime/registry/chapter_verification.rb +84 -0
  947. data/lib/hecks/runtime/registry/declarations.rb +17 -0
  948. data/lib/hecks/runtime/registry/durability_warnings.rb +71 -0
  949. data/lib/hecks/runtime/registry/hecksagon_rules.rb +124 -0
  950. data/lib/hecks/runtime/registry/merging.rb +49 -0
  951. data/lib/hecks/runtime/registry/providers.rb +139 -0
  952. data/lib/hecks/runtime/registry/query_verification.rb +144 -0
  953. data/lib/hecks/runtime/registry/read_repository.rb +61 -0
  954. data/lib/hecks/runtime/registry/runtime_state.rb +27 -0
  955. data/lib/hecks/runtime/registry/saga_persistence.rb +12 -8
  956. data/lib/hecks/runtime/registry/verification.rb +45 -389
  957. data/lib/hecks/runtime/registry/world_defaults.rb +3 -3
  958. data/lib/hecks/runtime/registry.rb +58 -296
  959. data/lib/hecks/runtime/remote_dispatcher.rb +47 -31
  960. data/lib/hecks/runtime/saga_interpreter/compensation.rb +90 -0
  961. data/lib/hecks/runtime/saga_interpreter/correlation.rb +19 -10
  962. data/lib/hecks/runtime/saga_interpreter/delivery.rb +147 -0
  963. data/lib/hecks/runtime/saga_interpreter/lifecycle.rb +63 -0
  964. data/lib/hecks/runtime/saga_interpreter/transitions.rb +112 -0
  965. data/lib/hecks/runtime/saga_interpreter.rb +41 -320
  966. data/lib/hecks/runtime/tenant_check.rb +14 -9
  967. data/lib/hecks/runtime/value/admission.rb +28 -19
  968. data/lib/hecks/runtime/value/coercion.rb +55 -404
  969. data/lib/hecks/runtime/value/entity_list_coercion.rb +27 -29
  970. data/lib/hecks/runtime/value/field_checks.rb +57 -100
  971. data/lib/hecks/runtime/value/flags.rb +65 -0
  972. data/lib/hecks/runtime/value/identifiers.rb +64 -0
  973. data/lib/hecks/runtime/value/references.rb +195 -0
  974. data/lib/hecks/runtime/value/shape_checks.rb +61 -0
  975. data/lib/hecks/runtime/value/validation.rb +125 -0
  976. data/lib/hecks/runtime/value.rb +18 -6
  977. data/lib/hecks/rust_build/conformance/surface.rb +75 -0
  978. data/lib/hecks/rust_build/conformance.rb +27 -59
  979. data/lib/hecks/rust_build/conformance_fuzz.rb +26 -15
  980. data/lib/hecks/rust_build/coverage.rb +32 -104
  981. data/lib/hecks/rust_build/coverage_parts/expected.rb +68 -0
  982. data/lib/hecks/rust_build/coverage_parts/printer.rb +75 -0
  983. data/lib/hecks/rust_build/host.rb +23 -13
  984. data/lib/hecks/rust_build/kernel_input.rb +19 -12
  985. data/lib/hecks/rust_build/native_build.rb +19 -15
  986. data/lib/hecks/rust_build/project_rust/domain_loader.rb +75 -0
  987. data/lib/hecks/rust_build/project_rust/root_mod/notes.rb +66 -0
  988. data/lib/hecks/rust_build/project_rust/root_mod.rb +22 -55
  989. data/lib/hecks/rust_build/project_rust/target_ir.rb +74 -0
  990. data/lib/hecks/rust_build/project_rust.rb +21 -89
  991. data/lib/hecks/rust_build/wasm.rb +11 -4
  992. data/lib/hecks/rust_build/wasm_browser.rb +7 -7
  993. data/lib/hecks/rust_build/write_if_changed.rb +3 -6
  994. data/lib/hecks/rust_build.rb +7 -3
  995. data/lib/hecks/site/bluebook/site.bluebook +7 -1
  996. data/lib/hecks/storehouse/audit.rb +106 -0
  997. data/lib/hecks/storehouse/callers.rb +73 -0
  998. data/lib/hecks/storehouse/dispatching.rb +149 -0
  999. data/lib/hecks/storehouse/introspection.rb +145 -0
  1000. data/lib/hecks/storehouse/querying.rb +78 -0
  1001. data/lib/hecks/storehouse.rb +43 -451
  1002. data/lib/hecks/tickets/bluebook/tickets.bluebook +29 -0
  1003. data/lib/hecks/tools/argument_gate_matrix/dispatching.rb +69 -0
  1004. data/lib/hecks/tools/argument_gate_matrix/rows.rb +121 -0
  1005. data/lib/hecks/tools/argument_gate_matrix/violations.rb +108 -0
  1006. data/lib/hecks/tools/argument_gate_matrix.rb +43 -212
  1007. data/lib/hecks/tools/ci_gate_decision.rb +21 -14
  1008. data/lib/hecks/tools/ci_gates.rb +40 -26
  1009. data/lib/hecks/tools/comment_style/baseline.rb +41 -0
  1010. data/lib/hecks/tools/comment_style/caps_words.rb +104 -0
  1011. data/lib/hecks/tools/comment_style/cli.rb +121 -0
  1012. data/lib/hecks/tools/comment_style/dictionary.rb +60 -0
  1013. data/lib/hecks/tools/comment_style/fixing.rb +119 -0
  1014. data/lib/hecks/tools/comment_style/long_blocks.rb +51 -0
  1015. data/lib/hecks/tools/comment_style/masking.rb +94 -0
  1016. data/lib/hecks/tools/comment_style/patterns.rb +61 -0
  1017. data/lib/hecks/tools/comment_style/run.rb +161 -0
  1018. data/lib/hecks/tools/comment_style/signature.rb +59 -0
  1019. data/lib/hecks/tools/comment_style/source_file.rb +105 -0
  1020. data/lib/hecks/tools/comment_style/structure_checks.rb +113 -0
  1021. data/lib/hecks/tools/comment_style/text_checks.rb +67 -0
  1022. data/lib/hecks/tools/comment_style/vocabulary.rb +55 -0
  1023. data/lib/hecks/tools/comment_style/walker.rb +136 -0
  1024. data/lib/hecks/tools/comment_style.rb +17 -1000
  1025. data/lib/hecks/tools/corpus_report.rb +26 -16
  1026. data/lib/hecks/tools/deploy_recipe/targets.rb +61 -0
  1027. data/lib/hecks/tools/deploy_recipe/worlds.rb +114 -0
  1028. data/lib/hecks/tools/deploy_recipe.rb +60 -174
  1029. data/lib/hecks/tools/deploy_recipe_lint/checks.rb +78 -0
  1030. data/lib/hecks/tools/deploy_recipe_lint/fixtures.rb +88 -0
  1031. data/lib/hecks/tools/deploy_recipe_lint/makefile.rb +84 -0
  1032. data/lib/hecks/tools/deploy_recipe_lint.rb +29 -192
  1033. data/lib/hecks/tools/drop_implicit_append_fields.rb +20 -12
  1034. data/lib/hecks/tools/era_projection_backfill.rb +50 -33
  1035. data/lib/hecks/tools/era_reattest/reporting.rb +55 -0
  1036. data/lib/hecks/tools/era_reattest.rb +32 -43
  1037. data/lib/hecks/tools/evolve_run/arguments.rb +49 -0
  1038. data/lib/hecks/tools/evolve_run/words.rb +60 -0
  1039. data/lib/hecks/tools/evolve_run.rb +43 -90
  1040. data/lib/hecks/tools/fuzz_sweep/findings.rb +106 -0
  1041. data/lib/hecks/tools/fuzz_sweep/options.rb +69 -0
  1042. data/lib/hecks/tools/fuzz_sweep/pool.rb +74 -0
  1043. data/lib/hecks/tools/fuzz_sweep/reporting.rb +88 -0
  1044. data/lib/hecks/tools/fuzz_sweep.rb +75 -273
  1045. data/lib/hecks/tools/gate.rb +31 -16
  1046. data/lib/hecks/tools/heki_compaction.rb +63 -42
  1047. data/lib/hecks/tools/hoist_local_givens/rewriting.rb +119 -0
  1048. data/lib/hecks/tools/hoist_local_givens.rb +89 -157
  1049. data/lib/hecks/tools/journal_compaction.rb +40 -31
  1050. data/lib/hecks/tools/lanes/files.rb +82 -0
  1051. data/lib/hecks/tools/lanes/live.rb +41 -0
  1052. data/lib/hecks/tools/lanes/rows.rb +45 -0
  1053. data/lib/hecks/tools/lanes/ruleset.rb +44 -0
  1054. data/lib/hecks/tools/lanes/watch_workflow.rb +82 -0
  1055. data/lib/hecks/tools/lanes/workflow.rb +79 -0
  1056. data/lib/hecks/tools/lanes.rb +71 -0
  1057. data/lib/hecks/tools/oidc_manifests.rb +10 -9
  1058. data/lib/hecks/tools/query_ir_run.rb +7 -3
  1059. data/lib/hecks/tools/regeneration_run.rb +40 -26
  1060. data/lib/hecks/tools/rust_comment_style/caps_words.rb +98 -0
  1061. data/lib/hecks/tools/rust_comment_style/fixing.rb +102 -0
  1062. data/lib/hecks/tools/rust_comment_style/masking.rb +85 -0
  1063. data/lib/hecks/tools/rust_comment_style/run.rb +94 -0
  1064. data/lib/hecks/tools/rust_comment_style/source_file.rb +71 -0
  1065. data/lib/hecks/tools/rust_comment_style/structure.rb +79 -0
  1066. data/lib/hecks/tools/rust_comment_style/text_checks.rb +63 -0
  1067. data/lib/hecks/tools/rust_comment_style/tokenizer.rb +102 -0
  1068. data/lib/hecks/tools/rust_comment_style.rb +38 -535
  1069. data/lib/hecks/tools/sequence_script.rb +20 -14
  1070. data/lib/hecks/tools/site_routes/cli.rb +70 -0
  1071. data/lib/hecks/tools/site_routes/root_files.rb +59 -0
  1072. data/lib/hecks/tools/site_routes/templates.rb +77 -0
  1073. data/lib/hecks/tools/site_routes.rb +102 -108
  1074. data/lib/hecks/tools/tail_merge.rb +45 -37
  1075. data/lib/hecks/tools/tenant_provisioning.rb +45 -31
  1076. data/lib/hecks/tools/tools_doc.rb +42 -19
  1077. data/lib/hecks/tools/translation_audit/domain_loading.rb +71 -0
  1078. data/lib/hecks/tools/translation_audit/edges.rb +71 -0
  1079. data/lib/hecks/tools/translation_audit/verdicts.rb +107 -0
  1080. data/lib/hecks/tools/translation_audit.rb +30 -161
  1081. data/lib/hecks/tools/translation_scaffold/reporting.rb +40 -0
  1082. data/lib/hecks/tools/translation_scaffold.rb +77 -60
  1083. data/lib/hecks/tools.rb +1 -0
  1084. data/lib/hecks/vendoring.rb +37 -11
  1085. data/lib/hecks/version.rb +1 -1
  1086. data/lib/hecks/vocabulary.rb +16 -1
  1087. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +7 -9
  1088. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +8 -6
  1089. data/rust/.cargo/mutants.toml +8 -0
  1090. data/rust/host/Cargo.toml +5 -0
  1091. data/rust/host/HECKS_RELEASE +1 -1
  1092. data/rust/host/src/auth.rs +32 -0
  1093. data/rust/host/src/web.rs +2 -3
  1094. data/rust/src/kernel/dispatch.rs +4 -0
  1095. data/rust/src/kernel/dispatch_guard_tests.rs +506 -0
  1096. data/rust/src/kernel/orchestrate.rs +4 -0
  1097. data/rust/src/kernel/orchestrate_guard_tests.rs +708 -0
  1098. metadata +607 -3
  1099. /data/lib/hecks/bluebook/behaviour/{hexagon.rb → hecksagon.rb} +0 -0
@@ -1,5 +1,8 @@
1
1
  require_relative "../../projector"
2
2
  require_relative "shared"
3
+ require_relative "text_template"
4
+ require_relative "lambda/stack"
5
+ require_relative "lambda/pii"
3
6
 
4
7
  module Hecks
5
8
  module Projections
@@ -22,1901 +25,90 @@ module Hecks
22
25
  # @return [Hash{String => String}] generated file contents by filename
23
26
  # @raise [ArgumentError] if `deployed_to("AwsLambda")` is invalid
24
27
  def call(bluebook:, options: {})
25
- world = options.fetch(:world)
26
- domain = options.fetch(:domain_dir)
27
- root = options.fetch(:root)
28
- world_file = options.fetch(:world_file)
29
- cross_domain_registry = options.fetch(:cross_domain_registry)
30
- tenant_options = options[:tenant] || {}
31
-
32
- domain_name = File.basename(domain)
33
- declared_domain_name = world.domain
34
-
35
- deploy_settings = world.for_verb("deployed_to")
36
-
37
- # Structural-only load (no `run_boot_gates!`/live persistence adapter)
38
- # populates `registry.pending_privacy_markings` for pii detection below
39
- # without requiring a real Postgres connection to generate a template.
40
- pii_registry = Hecks::Runtime::Registry.new(root: File.expand_path(domain))
41
- Hecks.with_registry(pii_registry) do
42
- bootstrap = Hecks::Ports::Loading.bootstrap
43
- bluebook_dir = File.join(domain, "bluebook")
44
- bootstrap.load_library
45
- bootstrap.load_project(bootstrap.shared_root(nil, bluebook_dir))
46
- bootstrap.load_bluebooks(bluebook_dir)
47
- Dir.glob(File.join(bluebook_dir, "*.hecksagon")).each { |file| Kernel.load(file) }
48
- end
49
-
50
- # Only `category: "pii"` triggers CloudFront/WAF provisioning; other
51
- # marking categories (e.g. "phi") stay a Governance/redaction concern.
52
- pii_detected = pii_registry.pending_privacy_markings.any? { |marking| marking[:category].to_s == "pii" }
53
-
54
- # Applied before infra_name/hecks_schema below, so both read the
55
- # override the same way they read any other `deployed_to` setting.
56
- if tenant_options[:tenant]
57
- base_stack_name = deploy_settings[:stack_name] || domain_name
58
- deploy_settings = deploy_settings.merge(
59
- stack_name: "#{base_stack_name}-#{tenant_options[:tenant]}",
60
- schema: tenant_options[:schema] || tenant_options[:tenant]
61
- )
62
- end
63
-
64
- # Every AWS-facing name (stack, logical ids, S3 prefixes, secrets)
65
- # reads `infra_name`, not `domain_name`, so a `stack_name` override
66
- # survives a later `formerly_known_as` rename of the declared identity
67
- # without renaming (and disconnecting from) the live AWS stack.
68
- infra_name = deploy_settings[:stack_name] || domain_name
69
-
70
- # RDS's own DBName/DatabaseName parameter refuses non-alphanumeric
71
- # characters; every downstream consumer (Environment vars, Makefile
72
- # shell commands) has to read the same sanitized spelling.
73
- db_name = infra_name.gsub(/[^a-zA-Z0-9]/, "")
74
-
75
- # Dispatches into LambdaTarget.Declare's given/invariant machinery for
76
- # named, corpus-testable refusals (memory/timeout checked as real
77
- # Integers against Lambda's 128-10240 MB / 900s ceilings).
78
- deploy_dispatcher = Hecks.boot(File.expand_path("../../deploy", __dir__))
79
- begin
80
- target = deploy_dispatcher.dispatch(
81
- "Deploy::LambdaTarget.Declare",
82
- with: {
83
- domain: { value: declared_domain_name },
84
- region: { value: deploy_settings[:region] },
85
- memory: { value: deploy_settings.fetch(:memory, 512) },
86
- timeout: { value: deploy_settings.fetch(:timeout, 10) },
87
- database: { value: deploy_settings.fetch(:database, "Postgres") },
88
- web: { value: deploy_settings.fetch(:web, "None") },
89
- **%i[dispatch handler_module secret_env].select { |key| deploy_settings.key?(key) }
90
- .to_h { |key| [key, { value: deploy_settings[key].to_s }] }
91
- }
92
- ).instance
93
- rescue *Hecks::Runtime::DOMAIN_REFUSALS => e
94
- raise ArgumentError, "#{world_file}'s deployed_to(\"AwsLambda\") is invalid: #{e.message}"
95
- end
96
-
97
- # `dispatch "None"` skips generating a rust/host dispatch Lambda for a
98
- # domain with no `.bluebook` command surface to dispatch through (e.g.
99
- # QualityControl's GitHub webhook, dispatching straight into Ruby);
100
- # such a domain's WebFunction becomes its only Lambda.
101
- dispatch_none = deploy_settings[:dispatch].to_s == "None"
102
-
103
- # Names the env var WebFunction's `lambda_handler.rb` finds the fetched
104
- # secret's plaintext under, once it resolves `#{secret_env}_ARN`.
105
- webhook_secret_env = deploy_settings[:secret_env]
106
-
107
- # The module name `lambda_handler.rb` actually defines; named here
108
- # since `dispatch "None"` domains may each pick their own.
109
- webhook_handler_module = deploy_settings.fetch(:handler_module, "WebLambdaHandler")
110
-
111
- # Scans every loaded chapter, not just this domain's own — a
112
- # `attaches`-attached chapter can itself declare a cross-domain
113
- # policy, including one nested inside `aggregate "X" do ... end`.
114
- cross_domain_lambda_targets = cross_domain_registry.bluebooks.flat_map { |chapter_name, bluebook|
115
- bluebook.policies.select(&:target_domain).map(&:target_domain)
116
- }.uniq.sort
117
-
118
-
119
- region = target.state[:region].value
120
- memory = target.state[:memory].value
121
- timeout = target.state[:timeout].value
122
- database = target.state[:database].value
123
- aurora = database == "Aurora"
124
- shared = database == "Shared"
125
- rust_web = target.state[:web].value == "Rust"
126
-
127
- # AWS::WAFv2::WebACL with Scope: CLOUDFRONT is refused by
128
- # CloudFormation outside us-east-1; refused here before writing a file.
129
- if pii_detected && region != "us-east-1"
130
- raise ArgumentError, "#{world_file} marks a field \"pii\" but deployed_to(\"AwsLambda\") sets region " \
131
- "#{region.inspect} — a CloudFront-scoped WAFv2 WebACL can only be created in " \
132
- "us-east-1. Set region \"us-east-1\", or remove the pii marking if this domain " \
133
- "genuinely holds none."
134
- end
135
-
136
- # Read off `deploy_settings` directly, not `target` — these two are
137
- # optional and pii-only, unlike every always-validated `target.state`.
138
- geo_restriction_type = deploy_settings[:geo_restriction] || "none"
139
- geo_restriction_countries = deploy_settings[:geo_restriction_countries] || []
140
-
141
- # Shared mode provisions no RDS/VPC of its own; it borrows another
142
- # already-deployed domain's instance, isolated by Postgres schema.
143
- if shared
144
- owner_domain_name = deploy_settings[:owner] or raise ArgumentError, <<~MSG
145
- #{world_file}'s deployed_to("AwsLambda") declares database "Shared" but no owner. Add one, e.g.:
146
-
147
- deployed_to("AwsLambda") do
148
- ...
149
- database "Shared"
150
- owner "Core"
151
- end
152
-
153
- naming the already-deployed domain whose Postgres instance this one borrows.
154
- MSG
155
- # `owner_stack` is the escape hatch for an owner whose live stack
156
- # name predates the "hecks-<lowercase name>" convention below.
157
- owner_stack_name = deploy_settings[:owner_stack] || "hecks-#{owner_domain_name.downcase}"
158
- owner_db_name = owner_domain_name.downcase
159
- end
160
-
161
- # rust/host's optional counterpart to Ruby's `settings[:schema]`.
162
- # Automatic (this domain's lowercase name) in Shared mode; otherwise
163
- # explicit via `deployed_to("AwsLambda") { schema "..." }`.
164
- hecks_schema = shared ? infra_name : deploy_settings[:schema]
165
-
166
- logical_id = "#{infra_name.split(/[_-]/).map(&:capitalize).join}Function"
167
- # The "hecks-" half of this domain's own stack name; overridable for a
168
- # live stack that predates the convention. Everything downstream
169
- # (FunctionNames, OAuth secret, bastion stack, samconfig) reads
170
- # `stack_name`; `infra_name` (logical ids, DB name) is untouched.
171
- stack_prefix = deploy_settings[:stack_prefix] || "hecks"
172
- stack_name = "#{stack_prefix}-#{infra_name}"
173
-
174
- db_id = "#{logical_id.sub(/Function\z/, '')}Db"
175
- # Aurora splits the database into a DBCluster plus DBInstance(s), so
176
- # every `.Endpoint.Address`/`.MasterUserSecret` reference below has to
177
- # point at the cluster, not `db_id`, when Aurora is chosen.
178
- db_ref_id = aurora ? "#{db_id}Cluster" : db_id
179
-
180
- # Aurora uses a self-managed secret (ManageMasterUserPassword rotation
181
- # would break a Lambda's static Environment); plain RDS uses its own
182
- # MasterUserSecret attribute. `secret_sub` is for `!Sub "${...}"`
183
- # interpolation; `secret_intrinsic` is the same fact as a bare value.
184
- secret_sub = aurora ? "#{db_id}Secret" : "#{db_ref_id}.MasterUserSecret.SecretArn"
185
- secret_intrinsic = aurora ? "!Ref #{db_id}Secret" : "!GetAtt #{db_ref_id}.MasterUserSecret.SecretArn"
186
- # Shared mode has no `secret_sub` of its own; `OwningDatabaseSecretArn`
187
- # (this template's Parameter) is already a bare ARN string.
188
- db_secret_ref = shared ? "OwningDatabaseSecretArn" : secret_sub
189
-
190
- # Opt-in by file presence: a domain wanting its own web app on Lambda
191
- # drops a `lambda_handler.rb` (the Rack-to-Function-URL-event adapter
192
- # Sinatra runs through) at its own root. No separate .world verb.
193
- web_handler_present = File.exist?(File.join(domain, "lambda_handler.rb"))
194
-
195
- # A domain declares one web story, not both.
196
- if rust_web && web_handler_present
197
- raise ArgumentError, "#{domain} declares both web \"Rust\" (#{world_file}) and a lambda_handler.rb (#{File.join(domain, 'lambda_handler.rb')}) — pick one."
198
- end
199
-
200
- # Read from the domain's own Gemfile.lock, not hardcoded — the
201
- # `patch-pg-native` build recipe below must fetch the same pg version
202
- # Bundler resolved, or the from-source extension can drift from it.
203
- pg_version = nil
204
- if web_handler_present
205
- # `dispatch "None"` reads this project's own root Gemfile.lock, not
206
- # #{domain}'s: #{domain} has no Gemfile of its own to bundle (it
207
- # needs `lib/hecks` from one level up), so CodeUri is `root` there.
208
- pg_version_path = File.join(dispatch_none ? root : domain, "Gemfile.lock")
209
- pg_version = File.read(pg_version_path)[/^\s+pg \(([\d.]+)\)/, 1]
210
- end
211
- web_logical_id = "WebFunction"
212
-
213
- # Same `dispatch "None"` split as `pg_version_path` above: #{domain}
214
- # alone has no Gemfile/lib/hecks to zip, so CodeUri is `root` instead.
215
- web_code_uri = dispatch_none ? root : domain
216
- web_handler_relpath = dispatch_none ? "#{domain}/lambda_handler" : "lambda_handler"
217
-
218
- # `.env.local` is #{domain}'s gitignored local-dev secrets file, read
219
- # only to detect the key is present; `make sync-google-oauth` syncs
220
- # the real value into Secrets Manager at deploy time.
221
- google_oauth_present = (web_handler_present || rust_web) &&
222
- File.exist?(File.join(domain, ".env.local")) &&
223
- File.read(File.join(domain, ".env.local")).match?(/^GOOGLE_CLIENT_ID=\S/)
28
+ stack = Stack.new(options)
29
+ files = { "template.yaml" => template_yaml(stack) }
30
+ # A separate, sibling stack for the few minutes `make mint-era` needs it, then deleted:
31
+ # SSM Session Manager only, never SSH, no inbound rule at all. Skipped when `shared`:
32
+ # era-minting reuses the owner's own standing infrastructure instead.
33
+ files["bastion.yaml"] = bastion_yaml(stack) unless stack.shared
34
+ files["Makefile"] = TextTemplate.render_from("lambda/Makefile.tmpl", stack)
35
+ # Everything `sam deploy --guided` would ask interactively is already known from the
36
+ # domain's own `deployed_to("AwsLambda")` block.
37
+ files["samconfig.toml"] = TextTemplate.render_from("lambda/samconfig.toml.tmpl", stack)
38
+ files
39
+ end
224
40
 
225
- # A shared-instance Ruby WebFunction needs cross-stack DATABASE_URL
226
- # wiring this generator doesn't build yet.
227
- if shared && web_handler_present
228
- raise ArgumentError, "#{domain} declares both database \"Shared\" and a lambda_handler.rb — a shared-instance Ruby WebFunction isn't supported yet."
229
- end
41
+ # Builds the CloudFront/WAFv2/logging resources a pii-marked domain is fronted by.
42
+ #
43
+ # @param fronted_logical_id [String] the function the distribution fronts
44
+ # @param use_oac [Boolean] whether the distribution signs its requests to the function
45
+ # @param geo_restriction_type [String] `none`, `whitelist` or `blacklist`
46
+ # @param geo_restriction_countries [Array<String>] the country codes the restriction lists
47
+ # @return [String] the resources, indented two spaces under `Resources:`
48
+ def pii_cloudfront_yaml(fronted_logical_id:, use_oac:, geo_restriction_type:, geo_restriction_countries:)
49
+ Pii.cloudfront_yaml(fronted_logical_id: fronted_logical_id, use_oac: use_oac,
50
+ geo_restriction_type: geo_restriction_type,
51
+ geo_restriction_countries: geo_restriction_countries)
52
+ end
230
53
 
231
- # `dispatch "None"` means WebFunction is the domain's only Lambda.
232
- if dispatch_none && !web_handler_present
233
- raise ArgumentError, "#{domain} declares dispatch \"None\" but has no lambda_handler.rb — dispatch \"None\" means no rust/host dispatch Lambda at all, so a WebFunction (lambda_handler.rb) has to exist to be the domain's only Lambda."
234
- end
235
- if dispatch_none && rust_web
236
- raise ArgumentError, "#{domain} declares both dispatch \"None\" and web \"Rust\" — dispatch \"None\" already means there is no rust/host Lambda for rust_web's own in-process web UI to run inside."
54
+ # Renders `template.yaml`, then the edits that are made after it renders, not interpolated
55
+ # inside it: a `#{...}` marker at column 0 would drag the template's own dedent computation
56
+ # to zero and leave every other line un-stripped.
57
+ #
58
+ # @param stack [Stack] the stack the world declares
59
+ # @return [String] the SAM template
60
+ def template_yaml(stack)
61
+ text = TextTemplate.render_from("lambda/template.yaml.tmpl", stack)
62
+ text = text.sub(/^([ \t]*)# TMPL:cross_domain_lambda_policies\n/) do
63
+ Shared.cross_domain_invoke_policy_yaml(stack.cross_domain_targets, Regexp.last_match(1))
237
64
  end
65
+ text = with_pii_distribution(text, stack) if stack.pii_detected
66
+ stack.dispatch_none ? without_dispatch_function(text, stack) : text
67
+ end
238
68
 
239
- # A Shared-mode rust_web domain with OAuth needs no new NAT Gateway:
240
- # it already runs inside the owner's borrowed private subnets and
241
- # security group, which already route to the internet for OAuth.
242
-
243
- # The stack-bastion contract and the least-privilege cross-domain
244
- # invoke grant are shared with `Fargate` — see `Shared`'s header.
245
- stack_outputs = Shared.stack_outputs(
246
- shared: shared, db_id: db_id, db_ref_id: db_ref_id, secret_intrinsic: secret_intrinsic,
247
- compute_security_group_ref: "!Ref #{logical_id}SecurityGroup", google_oauth_present: google_oauth_present
69
+ # Operates on the already-rendered string: the pii resources are built correctly indented,
70
+ # so nothing here interacts with a template's own dedent computation.
71
+ def with_pii_distribution(text, stack)
72
+ resources = pii_cloudfront_yaml(
73
+ fronted_logical_id: stack.web_handler_present ? stack.web_logical_id : stack.logical_id,
74
+ use_oac: !stack.web_handler_present && !stack.rust_web,
75
+ geo_restriction_type: stack.geo_restriction_type, geo_restriction_countries: stack.geo_restriction_countries
248
76
  )
249
- bastion_parameters = Shared.bastion_parameters(shared: shared, google_oauth_present: google_oauth_present)
250
- Shared.check_bastion_parameters!(bastion_parameters, stack_outputs)
251
-
252
- # A plain local, not two heredocs opened side by side inside the
253
- # template heredoc's own interpolation — a heredoc body starts on the
254
- # line after it opens, so an if/else here keeps each one unambiguous.
255
- web_google_oauth_env_yaml =
256
- if google_oauth_present
257
- <<~GOOGLE.each_line.with_index.map { |l, i| i.zero? ? l : " " + l }.join.rstrip
258
- # BY NAME, not `!Ref`/`!GetAtt` -- #{web_logical_id}GoogleOauth
259
- # is deliberately NOT declared as a CloudFormation resource
260
- # here (would need either GenerateSecretString, which can't
261
- # invent a real Google client_id/secret, or a value baked
262
- # into this git-TRACKED template as plaintext). `make
263
- # sync-google-oauth` owns this secret's whole lifecycle
264
- # instead, straight from #{domain}'s own gitignored
265
- # .env.local, outside CloudFormation entirely. GetSecretValue
266
- # accepts a bare name directly, no ARN needed -- same
267
- # generator-only caveat as DB_SECRET_ARN's own comment above.
268
- GOOGLE_OAUTH_SECRET_ID: #{stack_name}-web-google-oauth
269
- GOOGLE_REDIRECT_URI: !Sub "${WebRedirectBaseUrl}/auth/google/callback"
270
- GOOGLE
271
- else
272
- <<~NOOAUTH.each_line.with_index.map { |l, i| i.zero? ? l : " " + l }.join.rstrip
273
- # GOOGLE_CLIENT_ID/GOOGLE_CLIENT_SECRET/GOOGLE_REDIRECT_URI
274
- # deliberately NOT set here — #{domain}'s own .env.local has
275
- # no real GOOGLE_CLIENT_ID yet, matching Fly's own current
276
- # reality (its secrets list holds only
277
- # DATABASE_URL and SESSION_SECRET) that nobody has configured
278
- # OAuth for this app at all. Drop real credentials into
279
- # .env.local and regenerate to wire this up.
280
- NOOAUTH
281
- end
282
-
283
- template_yaml = <<~YAML
284
- # GENERATED by hecks deploy project #{domain} — re-run it to refresh
285
- # this file rather than hand-editing. Source: #{world_file}'s own
286
- # deployed_to("AwsLambda") block.
287
- #
288
- # SELF-CONTAINED, ON PURPOSE — this stack owns its own private VPC,
289
- # subnets, security groups, and RDS Postgres instance, not just the
290
- # Lambda. No externally-supplied VPC/database parameters, no
291
- # DatabaseUrl secret anyone has to type: RDS's own
292
- # ManageMasterUserPassword generates and stores the password in
293
- # Secrets Manager, and DATABASE_URL is composed from it via a
294
- # CloudFormation dynamic reference — resolved at deploy time, never
295
- # visible in this template, a parameter, or any tool's output.
296
- #
297
- # NO NAT GATEWAY for #{logical_id} — deliberately. Its own subnets
298
- # have no route to the internet at all, only to the RDS instance
299
- # inside the same VPC: dispatch a command, read/write the journal.
300
- # CloudWatch Logs delivery doesn't go through the function's own VPC
301
- # networking, so it needs no route either.
302
- #{google_oauth_present ? " #\n # #{web_logical_id} is the ONE exception -- real Google OAuth token\n # exchange (hecks's own GoogleAuthentication adapter posting to\n # https://oauth2.googleapis.com/token) is a genuine third-party HTTPS\n # call with no AWS PrivateLink/VPC Endpoint option (unlike\n # lambda:InvokeFunction, above), and #{web_logical_id} is ALSO\n # VPC-attached (Member's own permanent Postgres binding needs the\n # private RDS instance directly). A real, live 15-second Lambda\n # timeout caught this -- the token-exchange POST had nowhere to\n # route to and just hung. #{db_id}NatGateway below is the real,\n # ongoing-cost fix (one NAT Gateway, one public subnet) -- opt-in,\n # the SAME google_oauth_present signal that wires\n # GOOGLE_CLIENT_ID/SECRET, since nothing else in this stack needs\n # outbound internet at all." : " # Real outbound internet access from here (a future external API\n # call, say) would need a NAT Gateway added -- a real, ongoing cost\n # this template doesn't take on until something actually needs it."}
303
- AWSTemplateFormatVersion: '2010-09-09'
304
- Transform: AWS::Serverless-2016-10-31
305
- Description: >
306
- #{infra_name} — dispatched through hecks's rust/host, a
307
- wasmtime-sandboxed Rust binary with a rehydrate-and-replay Postgres
308
- journal (docs/decisions/0018), backed by its own private RDS
309
- Postgres instance. PackageType Zip, no container.
310
- #{
311
- # google_oauth_present and shared are independent and collected
312
- # into one array under a single `Parameters:` header, not an
313
- # if/elsif/else — both can be true at once (a Shared-mode
314
- # rust_web domain with real Google OAuth).
315
- param_blocks = []
316
- if google_oauth_present
317
- param_blocks << <<~OAUTHPARAMS.rstrip
318
- # Lambda Function URLs get a random, un-derivable hostname at
319
- # creation -- #{web_logical_id}'s own GOOGLE_REDIRECT_URI can't
320
- # reference `!GetAtt #{web_logical_id}Url.FunctionUrl` from inside
321
- # #{web_logical_id}'s OWN Environment (that's a real circular
322
- # dependency CloudFormation rejects: the value doesn't exist until
323
- # the function+URL do). Resolved OUTSIDE the stack instead --
324
- # `make deploy`'s own recipe looks up the CURRENTLY deployed
325
- # Function URL (empty on a true first deploy, before the URL
326
- # exists at all) and passes it as `--parameter-overrides`; every
327
- # deploy after the first self-heals this correctly.
328
- WebRedirectBaseUrl:
329
- Type: String
330
- Default: ""
331
- OAUTHPARAMS
332
- end
333
- if shared
334
- param_blocks << <<~SHAREDPARAMS.rstrip
335
- # THE STOREHOUSE — #{owner_domain_name}'s own live stack Outputs,
336
- # looked up at deploy time (Makefile's own `deploy:`/`mint-era`
337
- # targets, below) via the SAME `aws cloudformation describe-stacks`
338
- # pattern `mint-era` already used for a bastion, one stack over
339
- # instead of one sibling stack over. Never a CloudFormation
340
- # Export/ImportValue -- this codebase uses none anywhere (hecks
341
- # deploy project's own header on why: a live shell lookup keeps
342
- # this stack independently deployable/deletable, never coupled to
343
- # #{owner_domain_name}'s own stack through CloudFormation itself).
344
- OwningVpcId:
345
- Type: AWS::EC2::VPC::Id
346
- OwningSubnetAId:
347
- Type: AWS::EC2::Subnet::Id
348
- OwningSubnetBId:
349
- Type: AWS::EC2::Subnet::Id
350
- # #{owner_domain_name}'s own Lambda security group, NOT its
351
- # DB-facing one -- reused whole, not copied, the same shape
352
- # this codebase already trusts elsewhere (a WebFunction sharing
353
- # its own dispatch Lambda's security group rather than minting
354
- # a second one), one stack over instead of one resource over.
355
- # #{owner_domain_name}'s own DB security group already permits
356
- # ingress FROM this group (every member of it, regardless of
357
- # which stack minted the member -- security group references
358
- # match on GROUP MEMBERSHIP, not resource identity), so joining
359
- # it is what actually grants this Lambda access, not a new
360
- # ingress rule #{owner_domain_name}'s own stack would otherwise
361
- # need to add.
362
- OwningSecurityGroupId:
363
- Type: AWS::EC2::SecurityGroup::Id
364
- OwningDatabaseEndpoint:
365
- Type: String
366
- OwningDatabaseSecretArn:
367
- Type: String
368
- SHAREDPARAMS
369
- end
370
- if param_blocks.empty?
371
- ""
372
- else
373
- # Indented 2: the heredocs above dedent to column 0, but these
374
- # are Parameters: children, which YAML requires indented under it.
375
- "Parameters:\n" + param_blocks.join("\n").each_line.map { |l| " #{l}" }.join
376
- end
377
- }
378
- Resources:
379
- #{shared ? "" : Shared.vpc_and_database_yaml(
380
- db_id: db_id, db_name: db_name, infra_name: infra_name, aurora: aurora,
381
- google_oauth_present: google_oauth_present, compute_logical_id: logical_id,
382
- compute_description: "#{logical_id} - no inbound (Lambda receives no traffic via its VPC ENI), egress rule attached separately below"
383
- ).each_line.with_index.map { |l, i| (i.zero? ? "" : " ") + l }.join.rstrip}
384
-
385
- #{rust_web && google_oauth_present ? <<~RUSTSECRET.each_line.with_index.map { |l, i| (i.zero? ? "" : " ") + l }.join.rstrip : ""}
386
- # Auto-generated, never typed or seen -- rust/host's own
387
- # auth.rs signs both the session cookie and the OAuth `state`
388
- # token with this. Same ManageMasterUserPassword-style pattern
389
- # #{db_id} already uses for its own password.
390
- #{logical_id}SessionSecret:
391
- Type: AWS::SecretsManager::Secret
392
- Properties:
393
- GenerateSecretString:
394
- SecretStringTemplate: '{}'
395
- GenerateStringKey: session_secret
396
- PasswordLength: 64
397
- ExcludePunctuation: true
398
-
399
- RUSTSECRET
400
- #{logical_id}:
401
- Type: AWS::Serverless::Function#{aurora ? "\n # `{{resolve:secretsmanager:...}}` dynamic references do NOT\n # create an implicit CloudFormation dependency on the referenced\n # resource (documented AWS behavior) -- #{logical_id}'s own\n # Environment resolves one against #{db_id}Secret below. A plain\n # `DependsOn: #{db_id}Secret` alone (CREATE_COMPLETE before this\n # resource's own update starts) still hit a real, live, repeatable\n # \"Secrets Manager can't find the specified secret\n # (ResourceNotFoundException)\" -- CREATE_COMPLETE from\n # CloudFormation's own perspective doesn't guarantee the secret is\n # yet READABLE via a dynamic reference from a DIFFERENT resource's\n # own concurrent update (AWS-side eventual consistency, not a\n # CloudFormation ordering bug `DependsOn` alone can close).\n # Chaining through #{db_id}Cluster too -- which ALSO reads this\n # secret, and whose own RDS-side update takes real, substantial\n # wall-clock time to apply -- gives that propagation window time\n # to close before #{logical_id}'s own resolution is attempted.\n DependsOn: [#{db_id}Secret, #{db_id}Cluster]" : ""}
402
- Metadata:
403
- BuildMethod: makefile
404
- Properties:
405
- # PINNED, not SAM's own auto-suffixed default — a Ruby-side
406
- # LambdaClient needs to know this function's name AHEAD OF a
407
- # first deploy (there's no chicken-egg lookup step), the same
408
- # "configured for me, in the projection" standard every other
409
- # piece of generated deploy config already holds to. Matches
410
- # `stack_name` above exactly — one name, two places it's read
411
- # from, never out of sync since both derive from `infra_name`.
412
- FunctionName: #{stack_name}
413
- # EXPLICIT, not omitted -- `sam build WebFunction` and
414
- # `sam build --use-container WebFunction` run as TWO SEPARATE
415
- # single-resource invocations (deploy:'s own comment on why:
416
- # cargo-lambda cross-compiles on this host directly, pg's
417
- # native extension needs the container -- `--use-container` is
418
- # a global sam build flag, can't be scoped per-resource). Each
419
- # invocation regenerates .aws-sam/build/template.yaml from
420
- # THIS source template for every resource, not just the one it
421
- # builds -- a resource with no CodeUri here reverts to
422
- # CodeUri-less after the SECOND invocation, and `sam deploy`
423
- # then defaults a missing CodeUri to the template's own
424
- # containing directory (deploy/#{infra_name}/ itself),
425
- # zipping the whole project tree -- including WebFunction's
426
- # own source -- as this function's code. A real, live deploy
427
- # shipped exactly that broken artifact (confirmed via
428
- # `aws lambda get-function` + downloading the actual deployed
429
- # zip). Pinning CodeUri here survives that regeneration: it
430
- # still resolves to `.aws-sam/build/#{logical_id}/`, where
431
- # build-#{logical_id}'s own `cp ... $(ARTIFACTS_DIR)` already
432
- # placed the real bootstrap+wasm, regardless of which of the
433
- # two sam build calls ran most recently.
434
- # "." NOT "#{logical_id}" -- SAM's Makefile build workflow
435
- # requires the Makefile to live INSIDE CodeUri itself
436
- # (confirmed live: "Makefile not found at
437
- # .../WebFunction/Makefile" when CodeUri named a
438
- # directory of its own). This project's one Makefile lives at
439
- # the deploy directory's own root, alongside this template --
440
- # "." is where it actually is.
441
- CodeUri: .
442
- PackageType: Zip
443
- Runtime: provided.al2023
444
- Architectures: [arm64]
445
- Handler: bootstrap
446
- MemorySize: #{memory}
447
- Timeout: #{timeout}
448
- Environment:
449
- Variables:
450
- # NOT a `{{resolve:secretsmanager:...}}`-composed DATABASE_URL
451
- # anymore (the WebFunction handler below still uses that
452
- # mechanism, unchanged) -- that dynamic reference genuinely
453
- # IS resolved fresh at deploy time and never rendered into
454
- # this template, this tool's output, or the CloudFormation
455
- # console. But once it resolves INTO a Lambda's own
456
- # Environment.Variables entry, the resolved PLAINTEXT is
457
- # stored in the function's OWN configuration:
458
- # `lambda:GetFunctionConfiguration` returns it decrypted,
459
- # and the Lambda console's Configuration tab displays it, to
460
- # ANY principal holding read-only account access (AWS's own
461
- # managed ReadOnlyAccess policy included) -- caught
462
- # reviewing this template, not live; AWS's own guidance is
463
- # to fetch a secret from Secrets Manager at RUNTIME instead,
464
- # precisely to avoid this. DB_HOST/DB_NAME/DB_SECRET_ARN
465
- # below are not secrets themselves -- an endpoint address, a
466
- # database name, and a Secrets Manager ARN authenticate
467
- # nothing on their own -- so sitting here as plain
468
- # Environment.Variables values costs nothing; main.rs itself
469
- # fetches the actual password from Secrets Manager at cold
470
- # start, over the AWS SDK, and never hands it back to
471
- # CloudFormation or Lambda's own configuration at all.
472
- # #{logical_id}'s own least-privilege Policies grant (below,
473
- # sibling of VpcConfig) is the one runtime IAM permission
474
- # that fetch needs, scoped to this one secret ARN.
475
- #{if shared
476
- # Owner's live Endpoint/Secret, looked up into
477
- # OwningDatabaseEndpoint/OwningDatabaseSecretArn.
478
- "DB_HOST: !Ref OwningDatabaseEndpoint\n" \
479
- " DB_NAME: #{owner_db_name}\n" \
480
- " DB_SECRET_ARN: !Sub \"${OwningDatabaseSecretArn}\""
481
- else
482
- "DB_HOST: !GetAtt #{db_ref_id}.Endpoint.Address\n" \
483
- " DB_NAME: #{db_name}\n" \
484
- " DB_SECRET_ARN: !Sub \"${#{secret_sub}}\""
485
- end}
486
- # `domain_name`, NOT `infra_name` — this names the actual
487
- # .wasm FILE `hecks build_wasm` writes (rust/dist/#{domain_name}.wasm,
488
- # its own target_mod_name convention, a Rust-build-pipeline
489
- # concern unrelated to AWS resource identity) and what the
490
- # Makefile below actually bundles into the Lambda package
491
- # under. Confirmed the hard way: pinning this to infra_name
492
- # alongside the real AWS-identity fields left it looking for
493
- # a file `hecks build_wasm` never produces.
494
- HECKS_WASM_PATH: !Sub "/var/task/#{domain_name}.wasm"
495
- # STATIC, not a deploy-time parameter — HECKS_DOMAIN is this
496
- # domain's own declared name, known at generation time the
497
- # same way DBName/HECKS_WASM_PATH already are. HECKS_ERA is
498
- # "1" because that's what a FRESH database always gets:
499
- # PostgresEra::LineageManager::EraResolver#check! mints era 1
500
- # unconditionally the first time any Ruby process boots
501
- # against an empty `hecks_eras` (era_resolver.rb's own
502
- # `held.empty?` branch) — deterministic, not guessed. `make
503
- # mint-era` (this directory's own Makefile) is what actually
504
- # triggers that first boot; a LATER real schema evolution
505
- # (era 2+) is a separate, later re-generation, not this one.
506
- HECKS_DOMAIN: #{declared_domain_name}
507
- # UNCONDITIONAL, not gated on `rust_web` — `rust/host/src/main.rs`'s
508
- # own boot sequence reads `ir::ir().ok_or(...)?` for every domain
509
- # regardless of web mode (era-lineage bookkeeping needs the IR, not
510
- # just the optional in-process web UI), so a Shared-mode domain with
511
- # no web layer still needs this sidecar path to boot at all. Previously
512
- # gated on `rust_web`, which left every such domain's Lambda crashing
513
- # on cold start — checkout.rs's own header flagged this exact
514
- # contradiction, confirmed live against Banking's committed template.
515
- HECKS_IR_PATH: !Sub "/var/task/#{domain_name}.ir.json"
516
- HECKS_ERA: "1"#{hecks_schema ? %(\n HECKS_SCHEMA: #{hecks_schema}) : ""}#{rust_web && google_oauth_present ? <<~RUSTOAUTH.each_line.with_index.map { |l, i| i.zero? ? "\n " + l : " " + l }.join.rstrip : ""}
517
- # NOT `{{resolve:secretsmanager:...}}` composing
518
- # GOOGLE_CLIENT_ID/GOOGLE_CLIENT_SECRET/SESSION_SECRET
519
- # directly anymore -- the identical GetFunctionConfiguration
520
- # exposure DATABASE_URL's own comment above documents
521
- # applied here too. main.rs's own cold start fetches both
522
- # secrets itself and `set_var`s the real GOOGLE_CLIENT_ID/
523
- # GOOGLE_CLIENT_SECRET/SESSION_SECRET keys before anything
524
- # else runs -- auth.rs/web.rs still read those exact keys,
525
- # unchanged. GOOGLE_OAUTH_SECRET_ID is a bare NAME (`make
526
- # sync-google-oauth` owns #{stack_name}-web-google-oauth's
527
- # whole lifecycle outside CloudFormation, straight from
528
- # #{domain}'s own gitignored .env.local, never baked into
529
- # this generated, git-tracked template as plaintext) --
530
- # GetSecretValue accepts a name directly, no ARN needed.
531
- GOOGLE_OAUTH_SECRET_ID: #{stack_name}-web-google-oauth
532
- GOOGLE_REDIRECT_URI: !Sub "${WebRedirectBaseUrl}/auth/google/callback"
533
- SESSION_SECRET_ARN: !Sub "${#{logical_id}SessionSecret}"
534
- RUSTOAUTH
535
- # LEAST-PRIVILEGE -- #{logical_id}'s own execution role is
536
- # otherwise SAM's bare default (logging only); this is the one
537
- # runtime AWS permission the DB_SECRET_ARN handling above needs,
538
- # scoped to the single secret ARN this stack itself depends on,
539
- # never `Resource: "*"`.
540
- Policies:
541
- - Statement:
542
- - Effect: Allow
543
- Action: secretsmanager:GetSecretValue
544
- Resource: !Sub "${#{db_secret_ref}}"
545
- #{rust_web && google_oauth_present ? <<~OAUTHPOLICY.each_line.with_index.map { |l, i| i.zero? ? l : " " + l }.join.rstrip : ""}
546
- - Statement:
547
- - Effect: Allow
548
- Action: secretsmanager:GetSecretValue
549
- # NAME-BASED, not `!Ref`/`!GetAtt` -- this secret is
550
- # never a stack resource (RUSTOAUTH's own Environment
551
- # comment, above, has the full story), so there is no
552
- # exact ARN to point at. The trailing `-*` is AWS's
553
- # own documented pattern for granting a secret known
554
- # only by name: every real ARN Secrets Manager mints
555
- # is this name plus a random 6-character suffix,
556
- # which can't be predicted at template-render time.
557
- Resource: !Sub "arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:#{stack_name}-web-google-oauth-*"
558
- - Statement:
559
- - Effect: Allow
560
- Action: secretsmanager:GetSecretValue
561
- Resource: !Sub "${#{logical_id}SessionSecret}"
562
- OAUTHPOLICY
563
- # TMPL:cross_domain_lambda_policies
564
- VpcConfig:
565
- #{if shared
566
- "SubnetIds: [!Ref OwningSubnetAId, !Ref OwningSubnetBId]\n SecurityGroupIds: [!Ref OwningSecurityGroupId]"
567
- else
568
- "SubnetIds: [!Ref #{db_id}SubnetA, !Ref #{db_id}SubnetB]\n SecurityGroupIds: [!Ref #{logical_id}SecurityGroup]"
569
- end}
570
- # AWS_IAM by default — this dispatches real domain commands
571
- # (cap-table/governance-shaped data);
572
- # an unauthenticated public URL is the wrong default for that,
573
- # even though it's the simpler one to demo with. NONE only when
574
- # `web "Rust"` (rust/host/src/web.rs) makes this the public web
575
- # UI itself — safe even though the SAME Function URL also
576
- # carries the internal {"verb"}/{"read"} dispatch shapes,
577
- # because a Function-URL HTTP event can never be crafted into
578
- # that raw top-level shape (Function URLs always wrap the real
579
- # HTTP body inside requestContext.http/rawPath/event.body,
580
- # unconditionally); that raw shape is reachable only through
581
- # the AWS SDK's own IAM-authenticated Invoke API, which never
582
- # goes through a Function URL at all.
583
- FunctionUrlConfig:
584
- AuthType: #{rust_web ? "NONE" : "AWS_IAM"}
585
- #{web_handler_present ? <<~WEB.each_line.map { |l| " " + l }.join : ""}
586
- # THE WEB APP'S OWN LAMBDA — opt-in, see `web_handler_present` above.
587
- # VPC-attached for the SAME reason #{logical_id} is (Member's own
588
- # persistence needs the private RDS instance directly, permanently —
589
- # the hecksagon's own comment on why), sharing
590
- # #{logical_id}SecurityGroup rather than minting a second one, since
591
- # both need the identical egress-to-DB rule. That VPC attachment
592
- # ALSO cuts off the public internet by default (this stack's own "NO
593
- # NAT GATEWAY" design) — but this function has to reach
594
- # #{logical_id} itself via `lambda:InvokeFunction`, a public AWS API
595
- # call. #{web_logical_id}LambdaEndpoint below is the targeted fix: a
596
- # VPC Interface Endpoint for the Lambda service ONLY (not a NAT
597
- # Gateway) — a few dollars a month, not a general internet route.
598
- #{db_id}LambdaEndpointSecurityGroup:
599
- Type: AWS::EC2::SecurityGroup
600
- Properties:
601
- VpcId: !Ref #{db_id}Vpc
602
- # EC2's own GroupDescription character set excludes apostrophes
603
- # too, not just em-dashes -- "this stack's own" was a real,
604
- # live rejection ("Invalid security group description").
605
- GroupDescription: #{db_id}LambdaEndpoint - HTTPS from this stacks own Lambdas only
606
-
607
- #{db_id}LambdaEndpointIngress:
608
- Type: AWS::EC2::SecurityGroupIngress
609
- Properties:
610
- GroupId: !Ref #{db_id}LambdaEndpointSecurityGroup
611
- IpProtocol: tcp
612
- FromPort: 443
613
- ToPort: 443
614
- SourceSecurityGroupId: !Ref #{logical_id}SecurityGroup
615
-
616
- #{db_id}LambdaEndpoint:
617
- Type: AWS::EC2::VPCEndpoint
618
- Properties:
619
- VpcId: !Ref #{db_id}Vpc
620
- ServiceName: !Sub "com.amazonaws.${AWS::Region}.lambda"
621
- VpcEndpointType: Interface
622
- PrivateDnsEnabled: true
623
- SubnetIds: [!Ref #{db_id}SubnetA, !Ref #{db_id}SubnetB]
624
- SecurityGroupIds: [!Ref #{db_id}LambdaEndpointSecurityGroup]
625
-
626
- # Auto-generated, never typed or seen — the SAME
627
- # ManageMasterUserPassword pattern #{db_id} already uses for its own
628
- # password, applied to Sinatra's own session-signing secret instead.
629
- #{web_logical_id}SessionSecret:
630
- Type: AWS::SecretsManager::Secret
631
- Properties:
632
- GenerateSecretString:
633
- SecretStringTemplate: '{}'
634
- GenerateStringKey: session_secret
635
- PasswordLength: 64
636
- ExcludePunctuation: true
637
-
638
- #{webhook_secret_env ? <<~WEBHOOKSECRET.rstrip : ""}
639
- # Auto-generated, never typed or seen — the SAME
640
- # ManageMasterUserPassword/SessionSecret pattern above, applied to
641
- # `secret_env`'s own webhook secret instead (deploy_settings' own
642
- # `secret_env`, read far above). #{web_logical_id}'s own
643
- # `lambda_handler.rb` fetches this by ARN at cold start (the
644
- # `#{webhook_secret_env}_ARN` Environment variable below) and exposes
645
- # its plaintext under `ENV["#{webhook_secret_env}"]` itself — the
646
- # SAME two-step indirection DB_SECRET_ARN/SESSION_SECRET_ARN already
647
- # use, and for the identical reason (this Environment block's own
648
- # header comment on why a resolved plaintext must never land in
649
- # Lambda's own configuration).
650
- #{web_logical_id}WebhookSecret:
651
- Type: AWS::SecretsManager::Secret
652
- Properties:
653
- GenerateSecretString:
654
- SecretStringTemplate: '{}'
655
- GenerateStringKey: value
656
- PasswordLength: 64
657
- ExcludePunctuation: true
658
-
659
- WEBHOOKSECRET
660
- #{web_logical_id}:
661
- Type: AWS::Serverless::Function
662
- # NO Metadata.BuildMethod — "bundler" isn't a real SAM value (a
663
- # real, live UnsupportedBuilderException caught this); `Runtime:
664
- # ruby3.2` alone is enough for SAM to select its own built-in Ruby
665
- # bundler workflow automatically. #{logical_id}'s own `BuildMethod:
666
- # makefile` above is different: rust/host isn't a runtime SAM
667
- # knows how to build at all, so it needs an explicit custom
668
- # workflow — this function doesn't. STAYS `ruby3.2` deliberately,
669
- # even though `sam validate --lint` now flags it deprecated
670
- # (2026-03-31, creation disabled 2027-02-01, still comfortably
671
- # ahead of today) — `patch-pg-native`'s own build-ruby3.2 container
672
- # image and `lib/3.2/pg_ext.so` path glob (this Makefile, below)
673
- # are BOTH hardcoded to this exact runtime; bumping Runtime alone
674
- # without reworking that whole from-source pg build for a newer
675
- # image is a real, separate undertaking (untested container image
676
- # availability, a different vendor/bundle native-extension path)
677
- # out of scope here. Matches the live, currently-
678
- # deployed choice exactly, so both consumers of this one shared
679
- # WebFunction code path stay on the same, PROVEN runtime.
680
- Properties:
681
- FunctionName: #{stack_name}-web
682
- Runtime: ruby3.2
683
- Architectures: [arm64]
684
- # THREE parts (file.Class.method), not two — aws-lambda-ric's own
685
- # 2-part form calls a plain TOP-LEVEL function via __send__, not
686
- # a module method; #{web_handler_relpath}.rb's own comment on
687
- # why its module is named the way it is explains the rest — a
688
- # real, live "LambdaHandler is not a module" caught both halves
689
- # of this the hard way. `web_handler_relpath` (not a bare
690
- # "lambda_handler") whenever `dispatch "None"` moved CodeUri off
691
- # #{domain} itself — see that variable's own comment, above.
692
- Handler: #{web_handler_relpath}.#{webhook_handler_module}.lambda_handler
693
- CodeUri: #{web_code_uri}
694
- MemorySize: 512
695
- Timeout: 15
696
- Environment:
697
- Variables:
698
- DOMAIN_ROOT: /var/task
699
- # NOT `declared_domain_name` — `Adapters::Lambda`/
700
- # `RemoteDispatcher` both read this to compute WHICH
701
- # deployed Lambda function to invoke (`hecks-\#{...}`,
702
- # matching this stack's own name), a question `infra_name`
703
- # answers and a domain's own business identity does not.
704
- # Real, live, and confirmed the hard way: this was still
705
- # wired to `declared_domain_name` when a client app
706
- # first deployed, and WebFunction immediately threw
707
- # `ResourceNotFoundException: Function not found:
708
- # hecks-<domain>` on every read AND on the
709
- # Google OAuth callback itself — a real signed-in user hit
710
- # this within minutes of the deploy that introduced it.
711
- DOMAIN_NAME: #{infra_name}
712
- HECKS_LAMBDA_ROUTING: "true"
713
- HECKS_LAMBDA_REGION: #{region}#{hecks_schema ? %(\n HECKS_SCHEMA: #{hecks_schema}) : ""}
714
- # SAME setting #{domain}'s own fly.toml/Dockerfile already
715
- # sets for the Fly deployment this one replaces — Sinatra's
716
- # own `host_authorization` default is only PERMISSIVE in
717
- # production mode (an empty `permitted_hosts` list means
718
- # "allow any host" — checked directly in rack-protection's
719
- # own source); its DEVELOPMENT default is the restrictive
720
- # one (only localhost/.test), which is what Sinatra falls
721
- # back to when NEITHER RACK_ENV nor APP_ENV is set. A real,
722
- # live "Host not permitted" 403 against this function's own
723
- # unpredictable *.lambda-url.*.on.aws hostname caught the
724
- # gap — this function's Environment never set either var.
725
- RACK_ENV: production
726
- # The precompiled `pg` gem's aarch64-linux native extension
727
- # needs GLIBC 2.29+; Lambda's ruby3.2 MANAGED runtime is
728
- # Amazon Linux 2 (glibc 2.26) -- a real, live "Init<NameError>:
729
- # uninitialized constant PG::Error" caught this (rescuing
730
- # PG::Error itself failed to resolve, because pg's own
731
- # require died before reaching the file that defines it).
732
- # `make deploy`'s own `patch-pg-native` step (this directory's
733
- # Makefile) replaces the broken precompiled extension with one
734
- # compiled from source, in-container, against a real
735
- # SSL-enabled libpq -- and drops that libpq's .so beside it at
736
- # /var/task/lib, found by the dynamic loader with NO explicit
737
- # LD_LIBRARY_PATH override needed: the base ruby3.2 image's
738
- # own baked-in default (`docker inspect
739
- # public.ecr.aws/lambda/ruby:3.2-arm64`) is already
740
- # "/var/lang/lib:/lib64:/usr/lib64:/var/runtime:/var/runtime/lib:/var/task:/var/task/lib:/opt/lib"
741
- # -- setting LD_LIBRARY_PATH here as a Lambda env var
742
- # REPLACES that whole list rather than extending it (a real,
743
- # live gotcha: Lambda env vars always fully override a
744
- # same-named baked-in image env, never merge with it), which
745
- # is exactly what broke Ruby's OWN interpreter startup
746
- # ("libcrypt.so.1: cannot open shared object file") the one
747
- # time this was set explicitly, before libpq.so.5.16 itself
748
- # ever got a chance to matter.
749
- # Member's own permanent Postgres connection — the SAME RDS
750
- # instance #{logical_id} itself writes rust/host's flat
751
- # journal into, different database engine role (Ruby's own
752
- # era/lineage schema, not rust/host's), never the real
753
- # Fly-hosted Postgres this whole effort is retiring away from.
754
- #
755
- # GENERATOR-ONLY FIX, NOT YET SAFE TO DEPLOY — DB_HOST/DB_NAME/
756
- # DB_SECRET_ARN/SESSION_SECRET_ARN/GOOGLE_OAUTH_SECRET_ID below
757
- # replace the SAME `{{resolve:secretsmanager:...}}`-into-
758
- # Environment.Variables shape rust/host's own main.rs (this
759
- # template's own #{logical_id}) used to have -- readable in
760
- # plaintext by any read-only account principal via
761
- # lambda:GetFunctionConfiguration, the exact exposure
762
- # #{logical_id}'s own comment above the Policies section
763
- # documents. But THIS function's consumer (`lambda_handler.rb`)
764
- # is not a file this generator writes or this repo carries --
765
- # it lives in the deploying app's own domain directory (real,
766
- # live example: a Sinatra app), outside this
767
- # codebase entirely. Redeploying a domain with an existing
768
- # `lambda_handler.rb` BEFORE that handler is updated to fetch
769
- # these itself (`aws-sdk-secretsmanager`, not yet in this
770
- # project's own Gemfile) breaks it: it still expects
771
- # `ENV["DATABASE_URL"]`/`ENV["SESSION_SECRET"]`/
772
- # `ENV["GOOGLE_CLIENT_ID"]`/`ENV["GOOGLE_CLIENT_SECRET"]`
773
- # already resolved, and none of those keys exist below anymore.
774
- DB_HOST: !GetAtt #{db_ref_id}.Endpoint.Address
775
- DB_NAME: #{db_name}
776
- DB_SECRET_ARN: !Sub "${#{secret_sub}}"
777
- SESSION_SECRET_ARN: !Sub "${#{web_logical_id}SessionSecret}"#{webhook_secret_env ? %(\n #{webhook_secret_env}_ARN: !Sub "${#{web_logical_id}WebhookSecret}") : ""}
778
- #{web_google_oauth_env_yaml}
779
- VpcConfig:
780
- SubnetIds: [!Ref #{db_id}SubnetA, !Ref #{db_id}SubnetB]
781
- SecurityGroupIds: [!Ref #{logical_id}SecurityGroup]
782
- # LEAST-PRIVILEGE, scoped to #{logical_id}'s own ARN specifically
783
- # (SAM's own LambdaInvokePolicy template) — WebFunctionRole is
784
- # otherwise SAM's bare default execution role (logging only), and
785
- # had NO lambda:InvokeFunction permission at all until this: a
786
- # real, live AccessDeniedException caught it, the same request
787
- # that also caught the wrong-function-name bug just above
788
- # (WebFunctionRole is not authorized... on resource
789
- # hecks-task — this stack has never had ANY policy granting
790
- # that action, so the fix isn't complete without adding one).
791
- # THE SAME secretsmanager:GetSecretValue GRANTS #{logical_id}'s
792
- # own Policies section (above) needed, once this function's own
793
- # `lambda_handler.rb` fetches DB_SECRET_ARN/SESSION_SECRET_ARN/
794
- # GOOGLE_OAUTH_SECRET_ID itself instead of relying on a resolved
795
- # Environment.Variables value -- see this Environment block's own
796
- # comment for why that consumer-side change isn't in this repo.
797
- Policies:
798
- - LambdaInvokePolicy:
799
- FunctionName: !Ref #{logical_id}
800
- - Statement:
801
- - Effect: Allow
802
- Action: secretsmanager:GetSecretValue
803
- Resource: !Sub "${#{secret_sub}}"
804
- - Statement:
805
- - Effect: Allow
806
- Action: secretsmanager:GetSecretValue
807
- Resource: !Sub "${#{web_logical_id}SessionSecret}"#{webhook_secret_env ? %(\n - Statement:\n - Effect: Allow\n Action: secretsmanager:GetSecretValue\n Resource: !Sub "${#{web_logical_id}WebhookSecret}") : ""}
808
- #{google_oauth_present ? <<~WEBOAUTHPOLICY.each_line.with_index.map { |l, i| i.zero? ? l : " " + l }.join.rstrip : ""}
809
- - Statement:
810
- - Effect: Allow
811
- Action: secretsmanager:GetSecretValue
812
- Resource: !Sub "arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:#{stack_name}-web-google-oauth-*"
813
- WEBOAUTHPOLICY
814
- # NONE, not AWS_IAM — real users need to reach sign-in over
815
- # plain HTTPS, unlike #{logical_id}'s own AWS_IAM-protected URL
816
- # (this function is the ONLY thing meant to call that one).
817
- FunctionUrlConfig:
818
- AuthType: NONE
819
- WEB
820
-
821
- Outputs:
822
- FunctionUrl:
823
- Value: !GetAtt #{logical_id}Url.FunctionUrl#{web_handler_present ? "\n WebFunctionUrl:\n Value: !GetAtt #{web_logical_id}Url.FunctionUrl" : ""}
824
- # stack_outputs (hecks deploy project, above the heredocs) is the ONE
825
- # place these four names live — DatabaseEndpoint/DatabaseSecretArn
826
- # feed `make mint-era`'s own boot step directly; VpcId/
827
- # DbSecurityGroupId ALSO feed bastion.yaml's own Parameters (a
828
- # separate, sibling stack, created only for the few minutes a mint
829
- # takes and destroyed right after, attaching into the SAME private
830
- # network without this main template ever knowing a bastion
831
- # exists) and the Makefile's own --parameter-overrides. Rename a
832
- # key in that table and every consumer follows; reference one that
833
- # doesn't exist there and hecks deploy project refuses before
834
- # writing a single file, instead of a bastion.yaml whose Parameter
835
- # nothing could ever fill. (`PrivateSubnetId` used to sit here too
836
- # — dropped, nothing ever consumed it; bastion.yaml mints its own
837
- # subnet instead.)
838
- #{stack_outputs.map { |o| "#{o[:key]}:\n Value: #{o[:ref]}" }.join("\n ")}
839
- YAML
840
-
841
- # Spliced in after the heredoc renders, not interpolated inside it —
842
- # a `#{...}` marker at column 0 would drag the heredoc's own dedent
843
- # computation to zero and leave every other line un-stripped.
844
- template_yaml = template_yaml.sub(/^([ \t]*)# TMPL:cross_domain_lambda_policies\n/) { Shared.cross_domain_invoke_policy_yaml(cross_domain_lambda_targets, $1) }
845
-
846
- # Operates on the already-rendered string — `pii_cloudfront_yaml`
847
- # builds its own correctly-indented text, so nothing here interacts
848
- # with a heredoc's own dedent computation.
849
- if pii_detected
850
- fronted_logical_id = web_handler_present ? web_logical_id : logical_id
851
- use_oac = !web_handler_present && !rust_web
852
-
853
- pii_resources = pii_cloudfront_yaml(
854
- fronted_logical_id: fronted_logical_id, use_oac: use_oac,
855
- geo_restriction_type: geo_restriction_type, geo_restriction_countries: geo_restriction_countries
856
- )
857
- template_yaml = template_yaml.sub(/^Outputs:\n/) { "#{pii_resources}Outputs:\n PiiDistributionDomainName:\n Value: !GetAtt PiiDistribution.DomainName\n" }
858
- end
859
-
860
- # Surgical removal, post-render, not a nested heredoc — a conditional
861
- # nested heredoc broke embedded multi-line `#{shared ? ... : ...}`
862
- # strings inside the resource block, which assume exactly one
863
- # reindentation layer (confirmed regenerating examples/banking).
864
- if dispatch_none
865
- # Non-greedy through the resource's own FunctionUrlConfig/AuthType
866
- # closing pair, so WebFunction's own later "AuthType:" can't match.
867
- template_yaml = template_yaml.sub(
868
- /^ #{Regexp.escape(logical_id)}:\n.*?\n AuthType: (?:NONE|AWS_IAM)\n/m, ""
869
- )
870
- # Removes the now-dangling `!Ref` to the resource just deleted above
871
- # — SAM refuses an unresolved Ref at package time, not silently.
872
- template_yaml = template_yaml.sub(
873
- /^ - LambdaInvokePolicy:\n FunctionName: !Ref #{Regexp.escape(logical_id)}\n/, ""
874
- )
875
- # WebFunction's own URL becomes the stack's only "FunctionUrl".
876
- template_yaml = template_yaml.sub(
877
- /^ FunctionUrl:\n Value: !GetAtt #{Regexp.escape(logical_id)}Url\.FunctionUrl\n WebFunctionUrl:\n Value: !GetAtt #{Regexp.escape(web_logical_id)}Url\.FunctionUrl\n/,
878
- " FunctionUrl:\n Value: !GetAtt #{web_logical_id}Url.FunctionUrl\n"
879
- )
880
- end
881
-
882
-
883
-
884
- # A separate, sibling stack for the few minutes `make mint-era`
885
- # needs it, then deleted — SSM Session Manager only, never SSH, no
886
- # inbound rule at all. Skipped when `shared`: era-minting reuses the
887
- # owner's own standing infrastructure instead.
888
- bastion_yaml = shared ? nil : Shared.bastion_yaml(
889
- domain: domain, infra_name: infra_name, stack_name: stack_name, db_id: db_id,
890
- google_oauth_present: google_oauth_present, bastion_parameters: bastion_parameters
891
- )
892
-
893
- # A top-level variable, not inlined in the Makefile heredoc below —
894
- # Make recipe lines need a literal leading tab, and a nested
895
- # interpolation's re-indent pass would corrupt that detection.
896
- mint_era_recipe =
897
- if shared
898
- <<~SHAREDMINT.rstrip
899
- # NOT AUTOMATED YET for a Shared-mode domain — it has no RDS/VPC
900
- # of its own to stand a temporary bastion next to (bastion.yaml
901
- # itself isn't even generated here — hecks deploy project's own
902
- # comment on why). Minting era 1 for #{infra_name}'s own schema
903
- # needs a tunnel to #{owner_domain_name}'s ALREADY-EXISTING RDS
904
- # instance instead -- reuse #{owner_domain_name}'s own deploy
905
- # directory's `make mint-era` machinery (or an already-open
906
- # tunnel to it) to run this domain's own boot against
907
- # `postgres://...@<tunnel-host>:<port>/#{owner_db_name}` with
908
- # `schema: #{infra_name.inspect}` in LineageManager.check!'s own
909
- # settings -- the exact same call this target runs automatically
910
- # for a domain with its own dedicated instance.
911
- #
912
- # IF THIS DOMAIN VENDORS/ATTACHES ANOTHER BLUEBOOK (attaches,
913
- # attaches ... from: :vendor), `check!` alone leaves THAT chapter's own
914
- # aggregates with no snapshot table at all -- it only provisions the
915
- # ONE bluebook it's called with (era_resolver.rb's own `bluebook.
916
- # aggregates.each`), never the whole registry. Found live minting
917
- # a client site's own era: `Payments::Payment.Initiate` refused with
918
- # "relation \\"payment_head_snapshot_1\\" does not exist" the first
919
- # time this domain's own checkout route ran for real. The automated
920
- # (non-Shared) recipe below now does this correctly for every OTHER
921
- # loaded bluebook, using ONE `Lineage` keyed by THIS domain's own
922
- # name (never each chapter's own -- a vendored chapter's aggregates
923
- # share THIS domain's single `hecks_journal_#{infra_name}` and
924
- # snapshot tables, they do not get a separate journal of their own).
925
- # Reproduce the same shape manually here: after `LineageManager.
926
- # check!` returns, `db = PostgresEra.connect_for(bluebook.name,
927
- # settings)`, `lineage = Lineage.new(db, bluebook.name)`, then for
928
- # every OTHER loaded bluebook's own aggregates, `lineage.
929
- # ensure_first_head!(aggregate.storage_name)`.
930
- #
931
- # EXIT 0, NOT 1 -- `deploy:`'s own last line (below) always chains
932
- # `$(MAKE) mint-era` unconditionally, even for a Shared-mode domain,
933
- # so a nonzero exit here used to mean a fully successful `sam
934
- # deploy` still left `make deploy` exiting nonzero -- confirmed
935
- # live: CI and any scripted caller read every Shared-mode deploy as
936
- # failed, which trains operators to ignore a red `make deploy` on
937
- # exactly the domains where a real failure most needs to stand out.
938
- # This step never fails at its own job (it has no automated job to
939
- # fail at -- it only reports that a manual step remains), so it
940
- # reports success and leaves the manual-step reminder in the echo
941
- # text above, not in the exit code. A human running `make mint-era`
942
- # directly still SEES the same message; they just don't get a
943
- # misleading "command failed" on top of it either.
944
- \t@echo "mint-era isn't automated yet for a Shared-mode domain (database \\"Shared\\") -- see this target's own comment in the generated Makefile for the manual path through #{owner_domain_name}'s own tunnel. This is NOT a failure -- exiting 0 so a genuinely successful \\"make deploy\\" still reports success; era-minting for this domain remains a separate manual step."; \\
945
- \texit 0
946
- SHAREDMINT
947
- else
948
- <<~OWNMINT.rstrip
949
- \t@echo "Looking up $(STACK)'s VPC/security group..."
950
- # stack_outputs/bastion_parameters (hecks deploy project) are the ONE
951
- # place these OutputKey strings and parameter names live — this eval
952
- # chain and the --parameter-overrides line just below are both
953
- # generated from the SAME table template.yaml's Outputs and
954
- # bastion.yaml's Parameters already read from.
955
- \t#{stack_outputs.map { |o| %($(eval #{o[:var]} := $(shell aws cloudformation describe-stacks --stack-name $(STACK) --query "Stacks[0].Outputs[?OutputKey=='#{o[:key]}'].OutputValue" --output text))) }.join("\n\t")}
956
- \t@echo "Deploying the temporary bastion stack $(BASTION_STACK)..."
957
- \taws cloudformation deploy --template-file bastion.yaml --stack-name $(BASTION_STACK) \\
958
- \t\t--parameter-overrides #{bastion_parameters.map { |p| "#{p[:name]}=$(#{stack_outputs.find { |o| o[:key] == p[:from_output] }[:var]})" }.join(" ")} \\
959
- \t\t--capabilities CAPABILITY_IAM
960
- # ONE continuous shell invocation from here on (every line ends in
961
- # \\, joining it to the next) — INSTANCE_ID is a SHELL variable, not
962
- # a Make one: a Make-level $$(eval $$(shell ...)) expands at parse
963
- # time, BEFORE the `aws cloudformation deploy` line above ever runs,
964
- # which would make INSTANCE_ID permanently empty. It can only be
965
- # computed here, after the bastion stack genuinely exists.
966
- #
967
- # A COMMENT LINE MUST NEVER SIT BETWEEN TWO \\-CONTINUED RECIPE
968
- # LINES — confirmed live, the hard way: a `#`-prefixed line inserted
969
- # between `DB_PASS=...; \\` and the line that used it silently
970
- # corrupted Make's own assembly of this whole block into one shell
971
- # script, and `DB_PASS` came out empty on the far side despite
972
- # printing correctly one line earlier. Every explanatory comment for
973
- # this whole chain belongs HERE, before it starts, same as this one —
974
- # never spliced into the middle of it.
975
- #
976
- # BASTION TEARDOWN IS PART OF THIS SAME CHAIN, ALL THE WAY THROUGH —
977
- # a prior version of this recipe ended at `exit $$BOOT_STATUS` with
978
- # no trailing `\\`, which made Make treat the delete-stack lines
979
- # after it as a SEPARATE recipe line, reached only on a zero exit.
980
- # A failed boot (bad tunnel, bad `ruby -e` invocation, anything) left
981
- # the bastion stack standing forever — including a live 5432 ingress
982
- # rule punched into the PRODUCTION RDS security group. Teardown now
983
- # runs unconditionally, with BOOT_STATUS captured up front and
984
- # returned last, so `make mint-era`'s own exit code still reflects
985
- # whether era 1 actually got minted.
986
- #
987
- # `LineageManager.check!` directly below, NOT `Hecks.boot` — also
988
- # confirmed live (the very first real invoke: "relation
989
- # \\"hecks_eras\\" does not exist", despite this step itself
990
- # reporting success): `Hecks.boot` always dispatches persistence
991
- # through whatever the domain's OWN .world file declares
992
- # (`persisted_by("Heki")` for a domain that hasn't opted into
993
- # Postgres for local dev), by EXPLICIT design — loader.rb's own
994
- # comment: "persisted_by(\\"PostgresEra\\") is never inferred from
995
- # anything else." `EraCheck.check!` (what `Hecks.boot` runs) is
996
- # itself adapter-capability-gated on that same declaration and simply
997
- # no-ops for a non-lineage-capable adapter — it never touches
998
- # hecks_eras at all. The fix reuses the SAME loading pipeline
999
- # `Loader.boot` itself does (`Ports::Loading.bootstrap`/
1000
- # `load_library`/`load_project`/`load_domain`) to build a real,
1001
- # fully-loaded registry (bluebook + any attaches attachments),
1002
- # then calls straight into PostgresEra's own `LineageManager.check!`
1003
- # with THIS deploy's actual `DATABASE_URL` — bypassing the world
1004
- # file's own adapter choice entirely, the same targeted pattern
1005
- # spec/adapters/driven/postgres_era/lineage_spec.rb's own `check!` helper
1006
- # already uses.
1007
- #
1008
- # THE WHOLE TUNNEL IS RESTARTED PER ATTEMPT, not just the boot
1009
- # check retried through one long-lived tunnel — confirmed live,
1010
- # the hard way: an SSM port-forwarding session can start and then
1011
- # exit again within the first few seconds (visible in its own log
1012
- # as a session ID that both starts AND exits before the retries
1013
- # even finish), not merely "slow to become ready". Retrying a
1014
- # Postgres connection five times against a tunnel process that has
1015
- # already died is retrying against a corpse — it fails identically
1016
- # every time, for a different reason than the one the retry was
1017
- # built for. Each attempt below kills whatever tunnel the LAST
1018
- # attempt opened (a no-op the first time) and opens a fresh one,
1019
- # so a dead session gets a real replacement, not just more patience.
1020
- #
1021
- # DB_PASS_URLENC, DERIVED RIGHT AFTER DB_PASS BELOW, IS WHAT ACTUALLY
1022
- # GOES INTO THE `postgres://` URL FURTHER DOWN — not DB_PASS itself.
1023
- # RDS/Secrets-Manager-generated passwords are not guaranteed free of
1024
- # `%`, and libpq's own connection-URI parser treats a bare `%` as an
1025
- # invalid token (percent-encoding is the one escape libpq's URI form
1026
- # actually documents and decodes; a raw `%` is not) -- roughly 29% of
1027
- # 32-char generated passwords contain at least one `%`, so this was
1028
- # silently unreachable until it wasn't. `ERB::Util.url_encode`
1029
- # percent-encodes everything outside RFC 3986's unreserved set,
1030
- # exactly what a URI password component needs -- a strict superset
1031
- # of the ExcludeCharacters the Aurora path below already excludes
1032
- # (a no-op there), and the actual fix for the plain-RDS
1033
- # ManageMasterUserPassword path, whose generated character set this
1034
- # template has no control over at all. This comment sits HERE,
1035
- # before the chain starts, on purpose -- see the note further below
1036
- # on why a comment line can never be spliced between two
1037
- # `\`-continued recipe lines.
1038
- \t@INSTANCE_ID=$$(aws cloudformation describe-stacks --stack-name $(BASTION_STACK) --query "Stacks[0].Outputs[?OutputKey=='InstanceId'].OutputValue" --output text); \\
1039
- \techo "Waiting for $$INSTANCE_ID to register with SSM..."; \\
1040
- \tfor i in $$(seq 1 30); do \\
1041
- \t\tSTATUS=$$(aws ssm describe-instance-information --filters "Key=InstanceIds,Values=$$INSTANCE_ID" --query "InstanceInformationList[0].PingStatus" --output text 2>/dev/null); \\
1042
- \t\tif [ "$$STATUS" = "Online" ]; then break; fi; \\
1043
- \t\tsleep 5; \\
1044
- \tdone; \\
1045
- \tDB_PASS=$$(aws secretsmanager get-secret-value --secret-id $(DB_SECRET_ARN) --query SecretString --output text | ruby -rjson -e 'print JSON.parse(STDIN.read)["password"]'); \\
1046
- \tDB_PASS_URLENC=$$(ruby -rerb -e 'print ERB::Util.url_encode(ARGV[0])' "$$DB_PASS"); \\
1047
- \tBOOT_STATUS=1; \\
1048
- \tfor attempt in 1 2 3 4 5; do \\
1049
- \t\tkill $$TUNNEL_PID 2>/dev/null; \\
1050
- \t\techo "Opening an SSM tunnel to $(DB_HOST):5432 and minting era 1 (attempt $$attempt/5)..."; \\
1051
- \t\taws ssm start-session --target $$INSTANCE_ID \\
1052
- \t\t\t--document-name AWS-StartPortForwardingSessionToRemoteHost \\
1053
- \t\t\t--parameters "{\\"host\\":[\\"$(DB_HOST)\\"],\\"portNumber\\":[\\"5432\\"],\\"localPortNumber\\":[\\"15432\\"]}" \\
1054
- \t\t\t>/tmp/$(BASTION_STACK)-tunnel-$$attempt.log 2>&1 & \\
1055
- \t\tTUNNEL_PID=$$!; \\
1056
- \t\tfor i in $$(seq 1 15); do \\
1057
- \t\t\tnc -z localhost 15432 2>/dev/null && break; \\
1058
- \t\t\tsleep 1; \\
1059
- \t\tdone; \\
1060
- \t\tcd $(ROOT) && DATABASE_URL="postgres://postgres:$$DB_PASS_URLENC@localhost:15432/#{db_name}" ruby -Ilib -e 'require "hecks"; require "hecks/ports/persistence/plugins/era"; loading = Hecks::Ports::Loading.bootstrap; directory = loading.bluebook_directory(ARGV[0]); root = loading.shared_root(nil, directory); registry = Hecks::Runtime::Registry.new(root: File.dirname(directory)); Hecks.with_registry(registry) { loading.load_library; loading.load_project(root); loading.load_domain(directory) }; bluebook = registry.bluebooks[#{declared_domain_name.inspect}] or abort "no #{declared_domain_name} bluebook loaded"; current_text = Hecks::Runtime::EraCheck.source_text_for(bluebook, directory); Hecks::Adapters::PostgresEra::LineageManager.check!(registry: registry, bluebook: bluebook, current_text: current_text, settings: { database: ENV["DATABASE_URL"]#{hecks_schema ? ", schema: #{hecks_schema.inspect}" : ""} }); db = Hecks::Adapters::PostgresEra.connect_for(bluebook.name, { database: ENV["DATABASE_URL"]#{hecks_schema ? ", schema: #{hecks_schema.inspect}" : ""} }); lineage = Hecks::Adapters::PostgresEra::Lineage.new(db, bluebook.name); (registry.bluebooks.values - [bluebook]).each { |other| other.aggregates.each { |aggregate| lineage.ensure_first_head!(aggregate.storage_name) } }; db.close; puts "booted OK — era resolution ran"' $(DOMAIN) && { BOOT_STATUS=0; break; }; \\
1061
- \t\tBOOT_STATUS=$$?; \\
1062
- \t\techo "boot check attempt $$attempt/5 failed (exit $$BOOT_STATUS) -- restarting the tunnel and retrying in 3s..."; \\
1063
- \t\tsleep 3; \\
1064
- \tdone; \\
1065
- \tkill $$TUNNEL_PID 2>/dev/null; \\
1066
- \techo "Tearing down the temporary bastion stack..."; \\
1067
- \taws cloudformation delete-stack --stack-name $(BASTION_STACK); \\
1068
- \taws cloudformation wait stack-delete-complete --stack-name $(BASTION_STACK); \\
1069
- \techo "$(BASTION_STACK) deleted. Era 1 should now be held if BOOT_STATUS was 0 — verify with hecks console or a Postgres query against hecks_eras."; \\
1070
- \texit $$BOOT_STATUS
1071
- OWNMINT
1072
- end
1073
-
1074
- # Shares mint_era_recipe's bastion/tunnel/retry/teardown chain but
1075
- # runs `hecks query era.scaffold_translation` or
1076
- # `hecks query era.audit_translation` over it.
1077
- #
1078
- # Neither script actually reads DATABASE_URL/HECKS_SCHEMA (both call
1079
- # `registry.binding_settings`, a lookup against the literal `database
1080
- # "..."` in #{domain}'s `.world`) — without `db_env_blind:`, this
1081
- # recipe would open a real tunnel to production and silently
1082
- # scaffold/audit the local dev database instead, reporting success.
1083
- # `db_env_blind` refuses unless `ALLOW_LOCAL_DB=1` is set.
1084
- translation_recipe = lambda do |verb, script, extra_args = "", cwd: "$(ROOT)", run_prefix: "ruby -Ilib", db_env_blind: false,
1085
- domain_arg: "$(DOMAIN) "|
1086
- if shared
1087
- <<~SHAREDTRANSLATION.rstrip
1088
- \t@echo "#{verb} isn't automated yet for a Shared-mode domain (database \\"Shared\\") -- see mint-era's own comment in the generated Makefile for the manual path through #{owner_domain_name}'s own tunnel."; \\
1089
- \texit 1
1090
- SHAREDTRANSLATION
1091
- else
1092
- <<~OWNTRANSLATION.rstrip
1093
- #{db_env_blind ? <<~ENVBLINDGUARD.rstrip
1094
- \t@if [ -z "$$ALLOW_LOCAL_DB" ]; then \\
1095
- \t\techo "REFUSING: #{script} resolves its OWN database connection from #{domain}'s .world file, NOT from DATABASE_URL/HECKS_SCHEMA -- opening a real tunnel to #{stack_name}'s production RDS instance below would silently scaffold/audit the LOCAL dev database instead and report success (see this recipe's own comment, above, for the confirmed root cause). Set ALLOW_LOCAL_DB=1 to run #{verb} against #{domain}'s local .world-declared database on purpose (e.g. to exercise the scaffold/audit logic itself); otherwise run this for real against production via the manual tunnel path mint-era's own comment describes."; \\
1096
- \t\texit 1; \\
1097
- \tfi
1098
- ENVBLINDGUARD
1099
- : ""}
1100
- \t@echo "Looking up $(STACK)'s VPC/security group..."
1101
- \t#{stack_outputs.map { |o| %($(eval #{o[:var]} := $(shell aws cloudformation describe-stacks --stack-name $(STACK) --query "Stacks[0].Outputs[?OutputKey=='#{o[:key]}'].OutputValue" --output text))) }.join("\n\t")}
1102
- \t@echo "Deploying the temporary bastion stack $(BASTION_STACK)..."
1103
- \taws cloudformation deploy --template-file bastion.yaml --stack-name $(BASTION_STACK) \\
1104
- \t\t--parameter-overrides #{bastion_parameters.map { |p| "#{p[:name]}=$(#{stack_outputs.find { |o| o[:key] == p[:from_output] }[:var]})" }.join(" ")} \\
1105
- \t\t--capabilities CAPABILITY_IAM
1106
- # DB_PASS_URLENC -- same percent-encoding fix as mint_era_recipe's
1107
- # own comment above the equivalent line in that recipe (libpq's URI
1108
- # parser rejects a bare `%`, which a generated RDS password isn't
1109
- # guaranteed to be free of); this recipe's DATABASE_URL below uses
1110
- # the encoded form for the same reason, even though the script it
1111
- # feeds doesn't currently read it (see db_env_blind above).
1112
- \t@INSTANCE_ID=$$(aws cloudformation describe-stacks --stack-name $(BASTION_STACK) --query "Stacks[0].Outputs[?OutputKey=='InstanceId'].OutputValue" --output text); \\
1113
- \techo "Waiting for $$INSTANCE_ID to register with SSM..."; \\
1114
- \tfor i in $$(seq 1 30); do \\
1115
- \t\tSTATUS=$$(aws ssm describe-instance-information --filters "Key=InstanceIds,Values=$$INSTANCE_ID" --query "InstanceInformationList[0].PingStatus" --output text 2>/dev/null); \\
1116
- \t\tif [ "$$STATUS" = "Online" ]; then break; fi; \\
1117
- \t\tsleep 5; \\
1118
- \tdone; \\
1119
- \tDB_PASS=$$(aws secretsmanager get-secret-value --secret-id $(DB_SECRET_ARN) --query SecretString --output text | ruby -rjson -e 'print JSON.parse(STDIN.read)["password"]'); \\
1120
- \tDB_PASS_URLENC=$$(ruby -rerb -e 'print ERB::Util.url_encode(ARGV[0])' "$$DB_PASS"); \\
1121
- \tRUN_STATUS=1; \\
1122
- \tfor attempt in 1 2 3 4 5; do \\
1123
- \t\tkill $$TUNNEL_PID 2>/dev/null; \\
1124
- \t\techo "Opening an SSM tunnel to $(DB_HOST):5432 and running #{verb} (attempt $$attempt/5)..."; \\
1125
- \t\taws ssm start-session --target $$INSTANCE_ID \\
1126
- \t\t\t--document-name AWS-StartPortForwardingSessionToRemoteHost \\
1127
- \t\t\t--parameters "{\\"host\\":[\\"$(DB_HOST)\\"],\\"portNumber\\":[\\"5432\\"],\\"localPortNumber\\":[\\"15432\\"]}" \\
1128
- \t\t\t>/tmp/$(BASTION_STACK)-tunnel-$$attempt.log 2>&1 & \\
1129
- \t\tTUNNEL_PID=$$!; \\
1130
- \t\tfor i in $$(seq 1 15); do \\
1131
- \t\t\tnc -z localhost 15432 2>/dev/null && break; \\
1132
- \t\t\tsleep 1; \\
1133
- \t\tdone; \\
1134
- \t\tcd #{cwd} && DATABASE_URL="postgres://postgres:$$DB_PASS_URLENC@localhost:15432/#{db_name}" HECKS_SCHEMA="#{hecks_schema}" #{run_prefix} #{script} #{cwd == "$(ROOT)" ? domain_arg : ""}#{extra_args}&& { RUN_STATUS=0; break; }; \\
1135
- \t\tRUN_STATUS=$$?; \\
1136
- \t\techo "#{verb} attempt $$attempt/5 failed (exit $$RUN_STATUS) -- restarting the tunnel and retrying in 3s..."; \\
1137
- \t\tsleep 3; \\
1138
- \tdone; \\
1139
- \tkill $$TUNNEL_PID 2>/dev/null; \\
1140
- \techo "Tearing down the temporary bastion stack..."; \\
1141
- \taws cloudformation delete-stack --stack-name $(BASTION_STACK); \\
1142
- \taws cloudformation wait stack-delete-complete --stack-name $(BASTION_STACK); \\
1143
- \techo "$(BASTION_STACK) deleted."; \\
1144
- \texit $$RUN_STATUS
1145
- OWNTRANSLATION
1146
- end
77
+ text.sub(/^Outputs:\n/) do
78
+ "#{resources}Outputs:\n PiiDistributionDomainName:\n Value: !GetAtt PiiDistribution.DomainName\n"
1147
79
  end
1148
-
1149
- # `hecks ask` answers the scaffold as text, and refuses (exit 1) where the script aborted.
1150
- scaffold_translation_recipe = translation_recipe.call(
1151
- "scaffold-translation", "exe/hecks query era.scaffold_translation", db_env_blind: true,
1152
- run_prefix: "HECKS_ENVIRONMENT=memory ruby", domain_arg: "domain=$(DOMAIN) "
1153
- )
1154
- # A refused audit is a refused query: `hecks ask` exits 1, so the target fails too.
1155
- translation_audit_recipe = translation_recipe.call(
1156
- "translation-audit", "exe/hecks query era.audit_translation", db_env_blind: true,
1157
- run_prefix: "HECKS_ENVIRONMENT=memory ruby", domain_arg: "domain=$(DOMAIN) "
1158
- )
1159
-
1160
- # Same chain, running an app-owned one-time migration script instead
1161
- # of a hecks tool. `bin/migrate_console_settings` belongs to the domain's own
1162
- # application, not to hecks, so it stays a `bin/` path when hecks drops its own.
1163
- # Harmless when the domain has none: `bundle exec ruby` on a missing file just fails
1164
- # loudly.
1165
- migrate_console_settings_recipe = translation_recipe.call(
1166
- "migrate-console-settings", "bin/migrate_console_settings", "",
1167
- cwd: "$(DOMAIN)", run_prefix: "bundle exec ruby"
1168
- )
1169
-
1170
- # `OLD`/`NEW` are Make command-line variables (`make rename-schema
1171
- # OLD=old NEW=new`), reusable across whichever domain owns this
1172
- # stack's RDS instance. Idempotent by inspection (checks which schema
1173
- # exists first), not by catching a Postgres error.
1174
- rename_schema_recipe =
1175
- if shared
1176
- <<~SHAREDRENAME.rstrip
1177
- # NOT AUTOMATED for a Shared-mode domain, same reason mint-era
1178
- # isn't: there is no bastion.yaml here to stand up next to (this
1179
- # domain has no RDS/VPC of its own). This domain's own schema
1180
- # lives inside #{owner_domain_name}'s database — run this same
1181
- # target from #{owner_domain_name}'s own deploy directory instead.
1182
- \t@echo "rename-schema isn't automated for a Shared-mode domain (database \\"Shared\\") -- run it from #{owner_domain_name}'s own deploy directory instead, which owns the RDS instance this domain's schema actually lives on."; \\
1183
- \texit 1
1184
- SHAREDRENAME
1185
- else
1186
- <<~OWNRENAME.rstrip
1187
- # THE WHOLE TUNNEL IS RESTARTED PER ATTEMPT, not just probed once
1188
- # through a single long-lived session — confirmed live, the hard
1189
- # way: an SSM port-forwarding session can start and then exit
1190
- # again within seconds, not merely "slow to become ready". Probing
1191
- # the same dead tunnel five times fails identically every time,
1192
- # for a different reason than the retry was built for. Without
1193
- # this restart, a still-dead tunnel also doesn't just fail loudly
1194
- # — `EXISTS_OLD`/`EXISTS_NEW` would both come back empty, which the
1195
- # branch below reads as "neither schema exists" and reports as if
1196
- # that were the real answer, not a connectivity failure wearing
1197
- # its name — which is exactly why `TUNNEL_READY` gates that branch
1198
- # explicitly rather than trusting empty results at face value.
1199
- #
1200
- # OLD/NEW ARE ALLOWLISTED AS BARE IDENTIFIERS, not SQL-escaped, before
1201
- # either one reaches a psql command line: both get interpolated
1202
- # straight into the `nspname = '...'` lookups above and the
1203
- # `ALTER SCHEMA "..." RENAME TO "..."` below, and a schema name can't
1204
- # be bound as a parameter the way a value can — quoting/escaping an
1205
- # identifier is exactly the kind of thing that's easy to get subtly
1206
- # wrong, so instead of trying, this refuses anything that isn't
1207
- # `^[A-Za-z_][A-Za-z0-9_]*$` outright. `make` runs this recipe as an
1208
- # operator command, not user-facing web input, but it still executes
1209
- # against production RDS, so a typo'd or copy-pasted `OLD`/`NEW`
1210
- # containing quotes or `;` must fail loudly here, before either
1211
- # tunnel or query, rather than run as-is.
1212
- #
1213
- # `$$OLD`/`$$NEW` HERE, NOT `$(OLD)`/`$(NEW)`: this check itself has
1214
- # to read the value without Make ever splicing its raw text into the
1215
- # recipe line, or a value containing `;`/`"` would break out of THIS
1216
- # line's shell command and run before the pattern match ever sees it
1217
- # — confirmed live, the same way the tunnel-restart behavior above
1218
- # was. `$$OLD` instead reads the real shell environment variable
1219
- # `make` already exports for command-line-assigned vars, so the
1220
- # value is one opaque string to the shell no matter what characters
1221
- # it contains. Once a value's passed this gate it's provably just
1222
- # `[A-Za-z0-9_]`, so the existing `$(OLD)`/`$(NEW)` splices further
1223
- # below (already written before this fix, reused as-is) are safe.
1224
- \t@[ -n "$$OLD" ] && [ -n "$$NEW" ] || { echo "usage: make rename-schema OLD=<old-schema> NEW=<new-schema>"; exit 1; }
1225
- \t@echo "$$OLD" | grep -Eq '^[A-Za-z_][A-Za-z0-9_]*$$' || { echo "invalid OLD schema name -- schema names must match ^[A-Za-z_][A-Za-z0-9_]*$$, refusing to touch SQL"; exit 1; }
1226
- \t@echo "$$NEW" | grep -Eq '^[A-Za-z_][A-Za-z0-9_]*$$' || { echo "invalid NEW schema name -- schema names must match ^[A-Za-z_][A-Za-z0-9_]*$$, refusing to touch SQL"; exit 1; }
1227
- \t@echo "Looking up $(STACK)'s VPC/security group..."
1228
- \t#{stack_outputs.map { |o| %($(eval #{o[:var]} := $(shell aws cloudformation describe-stacks --stack-name $(STACK) --query "Stacks[0].Outputs[?OutputKey=='#{o[:key]}'].OutputValue" --output text))) }.join("\n\t")}
1229
- \t@echo "Deploying the temporary bastion stack $(BASTION_STACK)..."
1230
- \taws cloudformation deploy --template-file bastion.yaml --stack-name $(BASTION_STACK) \\
1231
- \t\t--parameter-overrides #{bastion_parameters.map { |p| "#{p[:name]}=$(#{stack_outputs.find { |o| o[:key] == p[:from_output] }[:var]})" }.join(" ")} \\
1232
- \t\t--capabilities CAPABILITY_IAM
1233
- \t@INSTANCE_ID=$$(aws cloudformation describe-stacks --stack-name $(BASTION_STACK) --query "Stacks[0].Outputs[?OutputKey=='InstanceId'].OutputValue" --output text); \\
1234
- \techo "Waiting for $$INSTANCE_ID to register with SSM..."; \\
1235
- \tfor i in $$(seq 1 30); do \\
1236
- \t\tSTATUS=$$(aws ssm describe-instance-information --filters "Key=InstanceIds,Values=$$INSTANCE_ID" --query "InstanceInformationList[0].PingStatus" --output text 2>/dev/null); \\
1237
- \t\tif [ "$$STATUS" = "Online" ]; then break; fi; \\
1238
- \t\tsleep 5; \\
1239
- \tdone; \\
1240
- \tDB_PASS=$$(aws secretsmanager get-secret-value --secret-id $(DB_SECRET_ARN) --query SecretString --output text | ruby -rjson -e 'print JSON.parse(STDIN.read)["password"]'); \\
1241
- \tTUNNEL_READY=1; \\
1242
- \tfor attempt in 1 2 3 4 5; do \\
1243
- \t\tkill $$TUNNEL_PID 2>/dev/null; \\
1244
- \t\techo "Opening an SSM tunnel to $(DB_HOST):5432 (attempt $$attempt/5)..."; \\
1245
- \t\taws ssm start-session --target $$INSTANCE_ID \\
1246
- \t\t\t--document-name AWS-StartPortForwardingSessionToRemoteHost \\
1247
- \t\t\t--parameters "{\\"host\\":[\\"$(DB_HOST)\\"],\\"portNumber\\":[\\"5432\\"],\\"localPortNumber\\":[\\"15432\\"]}" \\
1248
- \t\t\t>/tmp/$(BASTION_STACK)-tunnel-$$attempt.log 2>&1 & \\
1249
- \t\tTUNNEL_PID=$$!; \\
1250
- \t\tfor i in $$(seq 1 15); do \\
1251
- \t\t\tnc -z localhost 15432 2>/dev/null && break; \\
1252
- \t\t\tsleep 1; \\
1253
- \t\tdone; \\
1254
- \t\tPGPASSWORD=$$DB_PASS psql -h localhost -p 15432 -U postgres -d #{db_name} -tAc "SELECT 1" >/dev/null 2>&1 && { TUNNEL_READY=0; break; }; \\
1255
- \t\techo "tunnel not carrying real traffic yet (attempt $$attempt/5) -- restarting the tunnel and retrying in 3s..."; \\
1256
- \t\tsleep 3; \\
1257
- \tdone; \\
1258
- \tEXISTS_OLD=$$(PGPASSWORD=$$DB_PASS psql -h localhost -p 15432 -U postgres -d #{db_name} -tAc "SELECT 1 FROM pg_namespace WHERE nspname = '$(OLD)'"); \\
1259
- \tEXISTS_NEW=$$(PGPASSWORD=$$DB_PASS psql -h localhost -p 15432 -U postgres -d #{db_name} -tAc "SELECT 1 FROM pg_namespace WHERE nspname = '$(NEW)'"); \\
1260
- \tif [ "$$TUNNEL_READY" != "0" ]; then \\
1261
- \t\techo "tunnel never carried a real connection -- aborting without touching either schema"; \\
1262
- \t\tBOOT_STATUS=1; \\
1263
- \telif [ "$$EXISTS_NEW" = "1" ]; then \\
1264
- \t\techo "schema $(NEW) already exists -- already renamed, no-op"; \\
1265
- \t\tBOOT_STATUS=0; \\
1266
- \telif [ "$$EXISTS_OLD" = "1" ]; then \\
1267
- \t\tPGPASSWORD=$$DB_PASS psql -h localhost -p 15432 -U postgres -d #{db_name} -v ON_ERROR_STOP=1 -c "ALTER SCHEMA \\"$(OLD)\\" RENAME TO \\"$(NEW)\\""; \\
1268
- \t\tBOOT_STATUS=$$?; \\
1269
- \t\techo "renamed schema $(OLD) to $(NEW) (exit $$BOOT_STATUS)"; \\
1270
- \telse \\
1271
- \t\techo "neither schema $(OLD) nor $(NEW) exists on #{db_name} -- nothing to rename"; \\
1272
- \t\tBOOT_STATUS=1; \\
1273
- \tfi; \\
1274
- \tkill $$TUNNEL_PID 2>/dev/null; \\
1275
- \techo "Tearing down the temporary bastion stack..."; \\
1276
- \taws cloudformation delete-stack --stack-name $(BASTION_STACK); \\
1277
- \taws cloudformation wait stack-delete-complete --stack-name $(BASTION_STACK); \\
1278
- \techo "$(BASTION_STACK) deleted."; \\
1279
- \texit $$BOOT_STATUS
1280
- OWNRENAME
1281
- end
1282
-
1283
- # `google_oauth_present` and `shared` are independent facts, not
1284
- # alternatives — an elsif would silently drop Owning* overrides
1285
- # whenever OAuth is also present. `@` (Make's "don't echo" prefix)
1286
- # is only meaningful on the chain's true first line, so lines are
1287
- # built unprefixed and `@` is added once, right before joining.
1288
- owner_lookup_lines = shared ? [
1289
- %(OWNER_VPC_ID=$$(aws cloudformation describe-stacks --stack-name #{owner_stack_name} --query "Stacks[0].Outputs[?OutputKey=='VpcId'].OutputValue" --output text); \\),
1290
- %(OWNER_SUBNET_A_ID=$$(aws cloudformation describe-stacks --stack-name #{owner_stack_name} --query "Stacks[0].Outputs[?OutputKey=='PrivateSubnetAId'].OutputValue" --output text); \\),
1291
- %(OWNER_SUBNET_B_ID=$$(aws cloudformation describe-stacks --stack-name #{owner_stack_name} --query "Stacks[0].Outputs[?OutputKey=='PrivateSubnetBId'].OutputValue" --output text); \\),
1292
- %(OWNER_SG_ID=$$(aws cloudformation describe-stacks --stack-name #{owner_stack_name} --query "Stacks[0].Outputs[?OutputKey=='FunctionSecurityGroupId'].OutputValue" --output text); \\),
1293
- %(OWNER_DB_HOST=$$(aws cloudformation describe-stacks --stack-name #{owner_stack_name} --query "Stacks[0].Outputs[?OutputKey=='DatabaseEndpoint'].OutputValue" --output text); \\),
1294
- %(OWNER_DB_SECRET_ARN=$$(aws cloudformation describe-stacks --stack-name #{owner_stack_name} --query "Stacks[0].Outputs[?OutputKey=='DatabaseSecretArn'].OutputValue" --output text); \\),
1295
- ] : []
1296
- owning_overrides = "OwningVpcId=$$OWNER_VPC_ID OwningSubnetAId=$$OWNER_SUBNET_A_ID OwningSubnetBId=$$OWNER_SUBNET_B_ID OwningSecurityGroupId=$$OWNER_SG_ID OwningDatabaseEndpoint=$$OWNER_DB_HOST OwningDatabaseSecretArn=$$OWNER_DB_SECRET_ARN"
1297
-
1298
- sam_deploy_lines =
1299
- if google_oauth_present && shared
1300
- [
1301
- %(WEB_URL=$$(aws lambda get-function-url-config --function-name #{rust_web ? stack_name : "#{stack_name}-web"} --query FunctionUrl --output text 2>/dev/null | sed 's:/$$::'); \\),
1302
- "if [ -n \"$$WEB_URL\" ]; then \\",
1303
- %(\tsam deploy --parameter-overrides WebRedirectBaseUrl="$$WEB_URL" #{owning_overrides}; \\),
1304
- "else \\",
1305
- "\tsam deploy --parameter-overrides #{owning_overrides}; \\",
1306
- "fi",
1307
- ]
1308
- elsif google_oauth_present
1309
- [
1310
- %(WEB_URL=$$(aws lambda get-function-url-config --function-name #{rust_web ? stack_name : "#{stack_name}-web"} --query FunctionUrl --output text 2>/dev/null | sed 's:/$$::'); \\),
1311
- "if [ -n \"$$WEB_URL\" ]; then \\",
1312
- %(\tsam deploy --parameter-overrides WebRedirectBaseUrl="$$WEB_URL"; \\),
1313
- "else \\",
1314
- "\tsam deploy; \\",
1315
- "fi",
1316
- ]
1317
- elsif shared
1318
- ["sam deploy --parameter-overrides #{owning_overrides}"]
1319
- else
1320
- ["sam deploy"]
1321
- end
1322
-
1323
- deploy_shell_chain = owner_lookup_lines + sam_deploy_lines
1324
- deploy_shell_chain = ["@#{deploy_shell_chain.first}"] + deploy_shell_chain.drop(1) if deploy_shell_chain.first&.match?(/=\$\$\(/)
1325
-
1326
- # Google OAuth newly added to an existing stack would deadlock this
1327
- # bridge: PublicSubnetId/BastionSubnetId don't exist on the live stack
1328
- # until the upcoming `sam deploy` creates them, so the pre-check's own
1329
- # `mint-era` call would hand bastion.yaml an empty Subnet::Id and
1330
- # CloudFormation would refuse it before `sam deploy` ever runs. Skip
1331
- # the bridge in exactly that case; the unconditional `mint-era` call
1332
- # at the end of `deploy:` still covers it afterward.
1333
- predeploy_bridge_shell =
1334
- if google_oauth_present
1335
- <<~OAUTHBRIDGE.rstrip
1336
- @echo "Checking whether $(STACK) already exists, and if so whether it already has Google OAuth's PublicSubnetId/BastionSubnetId outputs, before deciding whether to bridge era history now or let this deploy create them first..."
1337
- @if aws cloudformation describe-stacks --stack-name $(STACK) >/dev/null 2>&1; then \\
1338
- \tOAUTH_OUTPUTS_READY=$$(aws cloudformation describe-stacks --stack-name $(STACK) --query "Stacks[0].Outputs[?OutputKey=='PublicSubnetId'].OutputValue" --output text 2>/dev/null); \\
1339
- \tif [ -z "$$OAUTH_OUTPUTS_READY" ]; then \\
1340
- \t\techo "Existing stack found, but Google OAuth is newly being added -- PublicSubnetId/BastionSubnetId don't exist on it yet (sam deploy, below, is what creates them); skipping the pre-deploy bridge this one time so THIS deploy can actually run. mint-era still runs once, after sam deploy, same as a genuine first deploy."; \\
1341
- \telse \\
1342
- \t\techo "Existing stack found -- bridging era history before this deploy flips $(STACK) over, not after."; \\
1343
- \t\t$(MAKE) mint-era || exit 1; \\
1344
- \tfi; \\
1345
- else \\
1346
- \techo "No existing stack -- first deploy, nothing to bridge yet; mint-era runs once, below, after the stack (and its RDS instance) exist."; \\
1347
- fi
1348
- OAUTHBRIDGE
1349
- else
1350
- <<~PLAINBRIDGE.rstrip
1351
- @echo "Checking whether $(STACK) already exists, to decide whether this deploy needs to bridge era history before it flips the stack over..."
1352
- @if aws cloudformation describe-stacks --stack-name $(STACK) >/dev/null 2>&1; then \\
1353
- \techo "Existing stack found -- bridging era history before this deploy flips $(STACK) over, not after."; \\
1354
- \t$(MAKE) mint-era || exit 1; \\
1355
- else \\
1356
- \techo "No existing stack -- first deploy, nothing to bridge yet; mint-era runs once, below, after the stack (and its RDS instance) exist."; \\
1357
- fi
1358
- PLAINBRIDGE
1359
- end
1360
-
1361
- deploy_recipe_lines = (
1362
- (google_oauth_present ? ["$(MAKE) sync-google-oauth"] : []) +
1363
- (shared ? [%(@echo "Looking up #{owner_stack_name}'s shared VpcId/PrivateSubnetAId/PrivateSubnetBId/FunctionSecurityGroupId/DatabaseEndpoint/DatabaseSecretArn outputs to pass as $(STACK)'s Owning* parameters...")] : []) +
1364
- deploy_shell_chain
1365
- ).map { |l| "\t#{l}" }.join("\n")
1366
-
1367
- makefile_content = <<~MAKE
1368
- # GENERATED by hecks deploy project #{domain} — re-run it to refresh
1369
- # this file rather than hand-editing.
1370
- #
1371
- # `sam build` invokes the build-<LogicalId> target below (SAM's own
1372
- # BuildMethod: makefile convention — see template.yaml's Metadata).
1373
- # `cargo lambda` (https://www.cargo-lambda.info) cross-compiles
1374
- # rust/host for arm64 Lambda without Docker or a hand-configured
1375
- # cross-linker — install once with `pip3 install cargo-lambda` or
1376
- # `brew install cargo-lambda`.
1377
-
1378
- HOST_DIR := #{File.join(root, "rust", "host")}
1379
- # `domain_name`, matching `hecks build_wasm`'s own target_mod_name
1380
- # (a Rust-build-pipeline naming convention, not AWS resource
1381
- # identity — see HECKS_WASM_PATH's own comment above).
1382
- WASM := #{File.join(root, "rust", "dist", "#{domain_name}.wasm")}
1383
-
1384
- build-#{logical_id}:
1385
- \t@command -v cargo-lambda >/dev/null 2>&1 || { \\
1386
- \t\techo "cargo-lambda isn't installed. Install it once with: pip3 install cargo-lambda"; \\
1387
- \t\texit 1; \\
1388
- \t}
1389
- # ALWAYS rebuilt, never `test -f $(WASM) || ...` — that guard only
1390
- # checked EXISTENCE, not staleness, so a wasm built once at the
1391
- # start of a work session silently kept getting deployed through
1392
- # every later kernel change (a real, live bug: this cost a stale
1393
- # deploy against a wasm that predated the kernel's own "mutations"
1394
- # field). `hecks build_wasm` recompiles fast enough that "always
1395
- # rebuild" is the safe default, matching rust/host's own bootstrap
1396
- # build just below, which was never guarded this way to begin with.
1397
- \tcd #{root} && HECKS_ENVIRONMENT=memory ruby exe/hecks build.build_wasm domain=#{domain} --wait
1398
- \t@rustup target list --installed 2>/dev/null | grep -qx aarch64-unknown-linux-gnu || rustup target add aarch64-unknown-linux-gnu
1399
- # `rustup run stable`, not a bare `cargo lambda` — same reasoning
1400
- # `hecks build_wasm`'s own Makefile-equivalent line holds itself to: a
1401
- # `cargo`/`rustc` earlier on PATH than rustup's own shims (Homebrew
1402
- # installs one; common on this kind of machine) is a DIFFERENT
1403
- # toolchain that never saw `rustup target add`, and fails with
1404
- # "can't find crate for \`core\`" for the cross target even though
1405
- # `rustup target list --installed` reports it present.
1406
- # macOS -> Linux arm64 cross-compiling by default routes through
1407
- # zig (cargo-lambda's own bundled cargo-zigbuild) — every zig
1408
- # release checked so far (0.15.2, 0.16.0) rejects
1409
- # `-Wl,--fix-cortex-a53-843419`, a flag rustc's own
1410
- # aarch64-unknown-linux-gnu target emits unconditionally: "error:
1411
- # unsupported linker arg: --fix-cortex-a53-843419", found live on
1412
- # THIS domain's own very first real deploy attempt (upstream:
1413
- # https://github.com/ziglang/zig/issues, no fix released yet as of
1414
- # this comment). Not fixable by a flag here — it's zig's own `zig
1415
- # cc` argument-translation layer refusing before rustc's link step
1416
- # ever runs. If this build fails with that error, cargo-lambda has
1417
- # a real, documented non-zig, non-Docker fallback: `-c cargo` (or
1418
- # `CARGO_LAMBDA_COMPILER=cargo`) plus a real GNU cross-toolchain —
1419
- # `brew tap messense/macos-cross-toolchains && brew install
1420
- # aarch64-unknown-linux-gnu` (this flag has worked in real
1421
- # binutils/LLD since 2018; only zig's own translation of it is the
1422
- # gap), then set `CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=
1423
- # aarch64-linux-gnu-gcc` with that toolchain's own bin/ on PATH.
1424
- # Confirmed working end to end (a real `sam deploy` against a real
1425
- # Lambda) the same day this comment was written.
1426
- \tcd $(HOST_DIR) && rustup run stable cargo lambda build --release --arm64
1427
- \tcp $(HOST_DIR)/target/lambda/bootstrap/bootstrap $(ARTIFACTS_DIR)/bootstrap
1428
- # UNCONDITIONAL, not gated on rust_web -- same reason HECKS_IR_PATH
1429
- # itself is unconditional above: main.rs's own boot sequence reads
1430
- # the IR for every domain regardless of web mode, so the sidecar
1431
- # file this env var points to has to actually be in the package
1432
- # too, or the env var alone just changes the crash from "not set"
1433
- # to "not found".
1434
- \tcp $(WASM) $(ARTIFACTS_DIR)/#{domain_name}.wasm
1435
- \tcp #{File.join(root, "rust", "dist", "#{domain_name}.ir.json")} $(ARTIFACTS_DIR)/#{domain_name}.ir.json
1436
-
1437
- # `sam build <resource>` WIPES .aws-sam/build/ entirely before
1438
- # building just the one resource named -- confirmed live: building
1439
- # #{web_logical_id} second left NO #{logical_id}/ directory behind at
1440
- # all, and reverted its OWN built CodeUri back to the raw, unbuilt
1441
- # source value ("../.." -- CodeUri: . resolved two directories up
1442
- # from .aws-sam/build/template.yaml). `sam deploy` then zips THAT
1443
- # directory verbatim as #{logical_id}'s own code -- a real, live
1444
- # deploy shipped #{web_logical_id}'s own source tree (Gemfile,
1445
- # lambda_handler.rb, even .env.local) as #{logical_id}'s Lambda
1446
- # package this way, caught by downloading the actually-deployed
1447
- # code and finding #{web_logical_id}'s file tree inside it.
1448
- # `deploy:`'s own two separate `sam build <resource>` calls can
1449
- # never both survive in .aws-sam/build/ at once — this target
1450
- # re-populates #{logical_id}'s build directory from the SAME
1451
- # already-built bootstrap+wasm build-#{logical_id} just produced
1452
- # (untouched by the second sam build call — only .aws-sam/build/
1453
- # itself gets wiped, never $(HOST_DIR)/target or $(WASM)) and
1454
- # repoints the built template's CodeUri back at it.
1455
- .PHONY: restore-#{logical_id}-build
1456
- restore-#{logical_id}-build:
1457
- \t@mkdir -p .aws-sam/build/#{logical_id}
1458
- \tcp $(HOST_DIR)/target/lambda/bootstrap/bootstrap .aws-sam/build/#{logical_id}/bootstrap
1459
- \tcp $(WASM) .aws-sam/build/#{logical_id}/#{domain_name}.wasm
1460
- \tcp #{File.join(root, "rust", "dist", "#{domain_name}.ir.json")} .aws-sam/build/#{logical_id}/#{domain_name}.ir.json
1461
- \truby -e 'lines = File.readlines(".aws-sam/build/template.yaml"); start = lines.index { |l| l.strip == "#{logical_id}:" } or raise "restore-#{logical_id}-build: #{logical_id} resource not found in built template"; idx = (start+1...lines.length).find { |i| lines[i] =~ /CodeUri:/ } or raise "restore-#{logical_id}-build: no CodeUri line found under #{logical_id}"; lines[idx] = lines[idx].sub(/CodeUri:.*/, "CodeUri: #{logical_id}"); File.write(".aws-sam/build/template.yaml", lines.join)'
1462
-
1463
- # `make verify-parity-#{logical_id}` — closes the exact gap the
1464
- # equivalence-gap plan's own Phase 8 named: parity was checked ONLY
1465
- # against CI's fixed test corpus, completely decoupled from what a
1466
- # real `sam deploy` actually ships — `hecks check_conformance` (the
1467
- # differential harness, docs/decisions/0010-ruby-is-the-reference-
1468
- # implementation.md) had never once been run against a specific
1469
- # compiled deploy artifact. Runs it here, for real, against
1470
- # $(WASM) -- the EXACT file `build-#{logical_id}` (above) just built
1471
- # and `deploy:` (below) is about to ship -- not a corpus-wide `cargo
1472
- # build`'s own separate binary. `hecks check_conformance --wait` exits non-zero
1473
- # on any mismatch, which this target lets propagate uncaught: a real
1474
- # divergence between THIS artifact and Ruby's own reading of the
1475
- # identical source blocks `make deploy` before `sam deploy` ever
1476
- # runs, the same way a failing build already would.
1477
- #
1478
- # `spec/corpus/#{domain_name}.json` is this domain's OWN pinned
1479
- # fuzzer-replay script (the same shape `hecks fuzz`/`hecks run` already
1480
- # use) -- if a domain doesn't have one yet, this warns LOUDLY and
1481
- # continues rather than either silently skipping (this project's own
1482
- # standing rule against a silent gap reading as full coverage) or
1483
- # blocking every deploy of a domain nobody has written one for yet.
1484
- #
1485
- # SECOND HALF, BELOW — `hecks fuzz_conformance`, ADR 0037's own fuzz
1486
- # bridge pointed at this exact $(WASM). A pinned script only proves
1487
- # "matches Ruby on the cases we thought to write down" (ADR 0039's own
1488
- # honest framing); the fuzz bridge is what actually FOUND Findings 1-6.
1489
- # Deliberately WARN-ONLY (`-@`, not `@`) for now: ADR 0037 already
1490
- # catalogues real, confirmed, not-yet-fixed divergences (Findings 3 and
1491
- # 5) that fire on ordinary generated sequences for more than one real
1492
- # domain today, so making this a hard blocker before those are closed
1493
- # would turn every affected domain's `make deploy` red for a reason
1494
- # this target can't yet point at a fix for. Flip `-@` to `@` once ADR
1495
- # 0037's open findings are closed and this stops firing in practice.
1496
- .PHONY: verify-parity-#{logical_id}
1497
- verify-parity-#{logical_id}:
1498
- \t@if [ -f #{root}/spec/corpus/#{domain_name}.json ]; then \\
1499
- \t\tcd #{root} && HECKS_ENVIRONMENT=memory ruby exe/hecks build.check_conformance domain=#{domain} script=spec/corpus/#{domain_name}.json artifact=$(WASM) --wait; \\
1500
- \telse \\
1501
- \t\techo "verify-parity-#{logical_id}: no spec/corpus/#{domain_name}.json -- SKIPPING the pre-deploy Ruby/Rust parity check, nothing to compare $(WASM) against. Write one (hecks fuzz/hecks run's own script shape) before this domain's next deploy."; \\
1502
- \tfi
1503
- \t@echo "verify-parity-#{logical_id}: fuzzing $(WASM) against generated sequences (ADR 0037's bridge, WARN-ONLY -- see this target's own comment)..."
1504
- \t-@cd #{root} && HECKS_ENVIRONMENT=memory ruby exe/hecks build.fuzz_conformance domain=#{domain} artifact=$(WASM) --wait
1505
-
1506
- # `make mint-era` — takes #{stack_name}'s RDS instance from freshly
1507
- # created to "era 1 minted, ready for HECKS_DOMAIN/HECKS_ERA to
1508
- # resolve" (template.yaml's own env vars already assume era 1 —
1509
- # this is what makes that true). Run ONCE per fresh database, before
1510
- # the first real `sam deploy` of the Lambda itself; safe to re-run
1511
- # (a second boot against an already-held era 1 is a no-op — see
1512
- # era_resolver.rb's own quiet-reboot branch). Stands up bastion.yaml
1513
- # as its own sibling stack, uses it for one Ruby boot over an SSM
1514
- # tunnel, then deletes it — nothing from this target is left running
1515
- # afterward.
1516
- ROOT := #{root}
1517
- DOMAIN := #{domain}
1518
- STACK := #{stack_name}
1519
- BASTION_STACK := #{stack_name}-bastion
1520
-
1521
- mint-era:
1522
- #{mint_era_recipe}
1523
-
1524
- # `make scaffold-translation` — run this when a deploy's own
1525
- # pre-flight boot check (or `make mint-era` directly) refuses with
1526
- # "the shape changed (era N) and no translation edge covers it".
1527
- # Diffs the held era against the current bluebook over the same
1528
- # bastion tunnel mint-era uses and WRITES a translations/*.bluebook
1529
- # edge file into this domain's own repo — confident rules inline,
1530
- # genuine ambiguities as `unresolved` lines a human has to resolve by
1531
- # hand (a rename, a drop, a compute — never guessed). Review the
1532
- # written file, then `make translation-audit`.
1533
- scaffold-translation:
1534
- #{scaffold_translation_recipe}
1535
-
1536
- # `make translation-audit` — verifies a written (or still-pending)
1537
- # translation edge over the same tunnel: every translated state
1538
- # against the new era's own types/invariants/lifecycle, the compiled
1539
- # SQL against the port's own reference transform, and a before/after
1540
- # sample of real records printed for human review. Re-run
1541
- # `make deploy` (or `make mint-era`) once this passes — that's what
1542
- # actually applies the edge; this only checks it.
1543
- translation-audit:
1544
- #{translation_audit_recipe}
1545
-
1546
- # `make migrate-console-settings` — runs this domain's own
1547
- # bin/migrate_console_settings (if it has one) over the same bastion
1548
- # tunnel every other Postgres-touching target here uses, against the
1549
- # real deployed database instead of local dev. A one-time data
1550
- # migration, not a repeatable ops procedure like mint-era/scaffold-
1551
- # translation — generated the same way regardless, per this
1552
- # project's own standing rule against hand-run ops steps.
1553
- migrate-console-settings:
1554
- #{migrate_console_settings_recipe}
1555
-
1556
- # `make rename-schema OLD=<old> NEW=<new>` — a native `ALTER SCHEMA
1557
- # ... RENAME TO ...` over the same bastion tunnel mint-era uses,
1558
- # for when this domain's own declared identity changes
1559
- # (`formerly_known_as`) and its storage schema should follow. Purely
1560
- # a Postgres namespace rename: every table/row/sequence inside it is
1561
- # untouched, so this is safe to run against a live, in-use database
1562
- # — the only requirement is that nothing else names the schema by
1563
- # its OLD name at the moment this runs (this domain's own deployed
1564
- # Lambda's HECKS_SCHEMA env var, most directly — run `make deploy`
1565
- # right before or right after this, not with a long gap either way).
1566
- rename-schema:
1567
- #{rename_schema_recipe}
1568
-
1569
- #{pg_version ? <<~PGNATIVE : ""}
1570
- # #{web_logical_id}'s own `pg` gem needs a native extension SAM's
1571
- # default Ruby builder can't produce correctly: the precompiled
1572
- # aarch64-linux binary needs GLIBC 2.29+, but Lambda's ruby3.2
1573
- # MANAGED runtime is Amazon Linux 2 (glibc 2.26) — a real, live
1574
- # "Init<NameError>: uninitialized constant PG::Error" caught this
1575
- # (rescuing PG::Error itself failed to resolve, because pg's own
1576
- # `require` died before ever reaching the file that defines it).
1577
- # Built once, from source, in the SAME container `sam build
1578
- # --use-container` itself uses (its glibc, not the host's, is what
1579
- # has to match the deployed runtime) — then cached under
1580
- # PG_NATIVE_DIR so every later `make deploy` skips straight to the
1581
- # fast path. Delete that directory to force a clean rebuild.
1582
- #
1583
- # OpenSSL is built `no-shared` and linked STATICALLY into libpq, so
1584
- # the libpq.so this produces carries no runtime dependency on
1585
- # whatever OpenSSL (if any) happens to already be on the Lambda
1586
- # execution image — sidesteps the AL2 package conflict between
1587
- # `openssl-libs` and the build image's own pre-installed
1588
- # `openssl-snapsafe-libs` entirely, rather than fighting it.
1589
- PG_NATIVE_DIR := #{root}/tmp/pg-native-arm64-ruby3.2
1590
-
1591
- $(PG_NATIVE_DIR)/pg_ext.so:
1592
- \t@mkdir -p $(PG_NATIVE_DIR)
1593
- \t@echo "Building #{web_logical_id}'s pg native extension from source (cached under $(PG_NATIVE_DIR) after this — a few minutes, one time only)..."
1594
- \tdocker run --rm --platform linux/arm64 -v $(PG_NATIVE_DIR):/out \\
1595
- \t\t--entrypoint /bin/bash public.ecr.aws/sam/build-ruby3.2:latest-arm64 -c ' \\
1596
- \t\t\tset -e; \\
1597
- \t\t\tyum install -y perl-IPC-Cmd >/dev/null; \\
1598
- \t\t\tcd /tmp; \\
1599
- \t\t\tcurl -sL https://www.openssl.org/source/openssl-3.0.15.tar.gz | tar xz; \\
1600
- \t\t\tcd openssl-3.0.15; \\
1601
- \t\t\t./Configure linux-aarch64 no-shared no-tests --prefix=/tmp/openssl-install >/dev/null; \\
1602
- \t\t\tmake -j$$(nproc) >/dev/null; \\
1603
- \t\t\tmake install_sw >/dev/null; \\
1604
- \t\t\tcd /tmp; \\
1605
- \t\t\tcurl -sL https://ftp.postgresql.org/pub/source/v16.4/postgresql-16.4.tar.gz | tar xz; \\
1606
- \t\t\tcd postgresql-16.4; \\
1607
- \t\t\tCPPFLAGS="-I/tmp/openssl-install/include" LDFLAGS="-L/tmp/openssl-install/lib" \\
1608
- \t\t\t\t./configure --without-readline --without-zlib --without-icu --with-ssl=openssl --prefix=/tmp/pg-install >/dev/null; \\
1609
- \t\t\tmake -C src/include install >/dev/null; \\
1610
- \t\t\tmake -C src/interfaces/libpq -j$$(nproc) install >/dev/null; \\
1611
- \t\t\tcd /tmp; \\
1612
- \t\t\tgem fetch pg -v #{pg_version} --platform ruby >/dev/null; \\
1613
- \t\t\tgem unpack pg-#{pg_version}.gem --target=/tmp/gemsrc >/dev/null; \\
1614
- \t\t\tcd /tmp/gemsrc/pg-#{pg_version}/ext; \\
1615
- \t\t\truby -I.. -I../lib extconf.rb --with-pg-include=/tmp/pg-install/include --with-pg-lib=/tmp/pg-install/lib >/dev/null; \\
1616
- \t\t\tmake >/dev/null; \\
1617
- \t\t\tcp pg_ext.so /out/pg_ext.so; \\
1618
- \t\t\tcp /tmp/pg-install/lib/libpq.so.5.16 /out/libpq.so.5.16; \\
1619
- \t\t'
1620
-
1621
- # `find`, not a hardcoded gem-version path — survives #{domain}'s
1622
- # own pg version bumping in its Gemfile.lock without this Makefile
1623
- # needing regeneration to match. libpq.so.5.16 lands beside the
1624
- # rest of #{web_logical_id}'s code at /var/task/lib — the ruby3.2
1625
- # base image's OWN baked-in LD_LIBRARY_PATH default already searches
1626
- # that exact path, so no rpath patching AND no explicit
1627
- # LD_LIBRARY_PATH override belongs in template.yaml (setting one
1628
- # there REPLACES the image's full default list instead of extending
1629
- # it — a real, live "libcrypt.so.1: cannot open shared object file"
1630
- # caught this — #{web_logical_id}'s own Environment no longer sets
1631
- # it at all, on purpose).
1632
- .PHONY: patch-pg-native
1633
- patch-pg-native: $(PG_NATIVE_DIR)/pg_ext.so
1634
- \t@PG_EXT=$$(find .aws-sam/build/#{web_logical_id}/vendor/bundle -path "*/pg-*/lib/3.2/pg_ext.so" | head -1); \\
1635
- \t\ttest -n "$$PG_EXT" || { echo "patch-pg-native: no pg_ext.so found under .aws-sam/build/#{web_logical_id} — run sam build --use-container #{web_logical_id} first"; exit 1; }; \\
1636
- \t\tcp $(PG_NATIVE_DIR)/pg_ext.so "$$PG_EXT"
1637
- \t@mkdir -p .aws-sam/build/#{web_logical_id}/lib
1638
- \tcp $(PG_NATIVE_DIR)/libpq.so.5.16 .aws-sam/build/#{web_logical_id}/lib/libpq.so.5.16
1639
- \tln -sf libpq.so.5.16 .aws-sam/build/#{web_logical_id}/lib/libpq.so.5
1640
-
1641
- PGNATIVE
1642
- #{google_oauth_present ? <<~SYNCOAUTH : ""}
1643
- # Owns #{stack_name}-web-google-oauth's WHOLE lifecycle (create on
1644
- # first run, update every run after — idempotent, cheap, no reason
1645
- # to cache the way patch-pg-native's own multi-minute build does) —
1646
- # deliberately OUTSIDE CloudFormation, straight from #{domain}'s own
1647
- # gitignored .env.local, so the real client_id/secret never lands in
1648
- # this generated, git-TRACKED template.yaml as plaintext. Read by
1649
- # name (`{{resolve:secretsmanager:#{stack_name}-web-google-oauth:...}}`,
1650
- # template.yaml's own Environment — #{rust_web ? logical_id : web_logical_id}'s
1651
- # own), not by `!Ref` — see that comment for why this secret isn't a
1652
- # stack resource at all. NOT nested inside patch-pg-native's own
1653
- # pg_version gate above -- a real, live "No rule to make target
1654
- # `sync-google-oauth'" caught that this target used to only exist
1655
- # when a Ruby WebFunction (with its own pg gem) was also present,
1656
- # even though Google OAuth itself has nothing to do with pg at all.
1657
- .PHONY: sync-google-oauth
1658
- sync-google-oauth:
1659
- \t@CLIENT_ID=$$(grep '^GOOGLE_CLIENT_ID=' #{domain}/.env.local | cut -d= -f2-); \\
1660
- \t\tCLIENT_SECRET=$$(grep '^GOOGLE_CLIENT_SECRET=' #{domain}/.env.local | cut -d= -f2-); \\
1661
- \t\ttest -n "$$CLIENT_ID" -a -n "$$CLIENT_SECRET" || { echo "sync-google-oauth: #{domain}/.env.local is missing GOOGLE_CLIENT_ID/GOOGLE_CLIENT_SECRET"; exit 1; }; \\
1662
- \t\tSECRET_JSON=$$(ruby -rjson -e 'puts JSON.generate({client_id: ARGV[0], client_secret: ARGV[1]})' "$$CLIENT_ID" "$$CLIENT_SECRET"); \\
1663
- \t\taws secretsmanager put-secret-value --secret-id #{stack_name}-web-google-oauth --secret-string "$$SECRET_JSON" >/dev/null 2>&1 || \\
1664
- \t\taws secretsmanager create-secret --name #{stack_name}-web-google-oauth --secret-string "$$SECRET_JSON" >/dev/null
1665
-
1666
- SYNCOAUTH
1667
- # `make deploy` — THE one command. Chains `sam build`, `sam deploy`
1668
- # (creates/updates the VPC+RDS+Lambda stack), then `mint-era` (safe
1669
- # to run every time — a second boot against an already-held era 1
1670
- # is a no-op, era_resolver.rb's own quiet-reboot branch) so a fresh
1671
- # database always ends this command with era 1 actually held, not a
1672
- # separate step someone has to remember. No hand-run AWS CLI
1673
- # sequence anywhere in this path.
1674
- #{google_oauth_present ? <<~OAUTHNOTE : ""}
1675
- # deploy's own last step below looks up #{web_logical_id}'s
1676
- # CURRENTLY deployed Function URL live (not tracked anywhere
1677
- # generated) and passes it as WebRedirectBaseUrl -- see that
1678
- # Parameter's own comment, above, for why GOOGLE_REDIRECT_URI can't
1679
- # just be a CloudFormation intrinsic. Empty on a genuine first
1680
- # deploy (before the Url exists at all); every deploy after that
1681
- # finds the real, STABLE hostname (confirmed unchanged across every
1682
- # redeploy this session) and self-heals GOOGLE_REDIRECT_URI to match.
1683
- OAUTHNOTE
1684
- #{shared ? <<~SHAREDNOTE : ""}
1685
- # deploy's own last step below looks up #{owner_domain_name}'s live
1686
- # stack Outputs (never tracked anywhere generated -- same reasoning
1687
- # as WebRedirectBaseUrl's own live lookup above, and the SAME
1688
- # `aws cloudformation describe-stacks` pattern mint-era's own eval
1689
- # chain, below, already proves works against a sibling stack) and
1690
- # passes them as this template's own Owning* Parameters.
1691
- SHAREDNOTE
1692
- .PHONY: deploy mint-era
1693
- deploy:
1694
- #{if dispatch_none
1695
- "\t# ONE build only -- dispatch \"None\" means there is no\n\t# #{logical_id} of any kind (no cargo-lambda, no rust artifact, no\n\t# verify-parity: nothing rust/host-shaped for this domain at all),\n\t# so unlike every other WebFunction-carrying domain there is no\n\t# second build to protect from `sam build <resource>`'s own\n\t# WIPES-.aws-sam/build/-first behavior (build-#{logical_id}'s own\n\t# sibling comment on why that matters for domains that DO have one)\n\t# and nothing to restore afterward.\n\tsam build --use-container #{web_logical_id}\n\t$(MAKE) patch-pg-native"
1696
- elsif pg_version
1697
- "\t# TWO SEPARATE builds, not one `--use-container` run -- that flag is\n\t# global to `sam build`, but #{logical_id} deliberately builds on\n\t# THIS machine's own toolchain (cargo-lambda already cross-compiles\n\t# to arm64 without a container -- build-#{logical_id}'s own comment\n\t# on why), which a generic provided.al2023 container doesn't have\n\t# at all (a real, live \"Make Failed\" caught this). Only #{web_logical_id}\n\t# needs the container, to cross-compile pg's native extension for\n\t# Amazon Linux -- `sam build <resource>` WIPES .aws-sam/build/ before\n\t# building just the one named, so #{logical_id}'s own build gets\n\t# restored below (restore-#{logical_id}-build's own comment on why).\n\tsam build #{logical_id}\n\tsam build --use-container #{web_logical_id}\n\t$(MAKE) restore-#{logical_id}-build\n\t$(MAKE) patch-pg-native\n\t$(MAKE) verify-parity-#{logical_id}"
1698
- elsif web_handler_present
1699
- "\t# TWO SEPARATE builds -- #{web_logical_id} has no native extension to\n\t# cross-compile (no `pg` in #{domain}/Gemfile.lock), so a plain `sam\n\t# build` targets it fine; #{logical_id} still gets its own `sam build\n\t# <resource>` first, then restored below -- `sam build <resource>`\n\t# WIPES .aws-sam/build/ before building just the one named, so the\n\t# second call here would otherwise erase #{logical_id}'s own build\n\t# (restore-#{logical_id}-build's own comment on why).\n\tsam build #{logical_id}\n\tsam build #{web_logical_id}\n\t$(MAKE) restore-#{logical_id}-build\n\t$(MAKE) verify-parity-#{logical_id}"
1700
- else
1701
- "\tsam build\n\t$(MAKE) verify-parity-#{logical_id}"
1702
- end}
1703
- #{shared ? "" : <<~PREDEPLOYBRIDGE.each_line.map { |l| "\t" + l }.join.rstrip
1704
- # THE TRANSACTION-SAFETY GAP, closed for the case that actually
1705
- # matters: `sam deploy` below is what flips this Lambda's own
1706
- # HECKS_DOMAIN/HECKS_SCHEMA env vars live — the INSTANT it
1707
- # completes, real traffic can hit a domain/schema combination
1708
- # whose era history hasn't been bridged yet if THIS domain was
1709
- # just renamed (formerly_known_as) or its schema just moved.
1710
- # `mint-era` already bridges that (era_resolver.rb's own
1711
- # rename_domain! path, idempotent either way) — it only ran
1712
- # AFTER `sam deploy` before this existed, which is exactly the
1713
- # order that left a real live window open the one time this was
1714
- # actually run in anger (an SSM tunnel flaked mid-bridge, after
1715
- # the Lambda had already flipped over).
1716
- #
1717
- # STILL RUNS AFTER TOO (unchanged, below) — this pre-check
1718
- # cannot replace that: a domain's VERY FIRST deploy has no
1719
- # stack, hence no RDS, hence nothing to tunnel to yet, so
1720
- # mint-era's real first run has to stay where it always was,
1721
- # once `sam deploy` has just created that RDS instance. This
1722
- # only ADDS a second, EARLIER run for a domain that already has
1723
- # a live stack (a rename or an ordinary redeploy) — cheap and
1724
- # safe either way, since mint-era's own quiet-reboot path is a
1725
- # no-op the moment nothing has actually changed.
1726
- #
1727
- # See predeploy_bridge_shell's own comment (hecks deploy project,
1728
- # above, right before deploy_recipe_lines) for why this branches
1729
- # on google_oauth_present at all -- the short version: adding
1730
- # Google OAuth to an EXISTING stack would otherwise deadlock this
1731
- # exact pre-check against outputs the upcoming sam deploy (not
1732
- # yet run) hasn't created.
1733
- #{predeploy_bridge_shell}
1734
- PREDEPLOYBRIDGE
1735
- }
1736
- #{deploy_recipe_lines}
1737
- \t$(MAKE) mint-era
1738
- MAKE
1739
-
1740
- # Everything `sam deploy --guided` would ask interactively is already
1741
- # known from the domain's own `deployed_to("AwsLambda")` block.
1742
- samconfig_toml = <<~TOML
1743
- # GENERATED by hecks deploy project #{domain} — re-run it to refresh
1744
- # this file rather than hand-editing.
1745
- version = 0.1
1746
-
1747
- [default.deploy.parameters]
1748
- stack_name = "#{stack_name}"
1749
- region = "#{region}"
1750
- resolve_s3 = true
1751
- s3_prefix = "#{stack_name}"
1752
- capabilities = "CAPABILITY_IAM"
1753
- confirm_changeset = false
1754
- disable_rollback = false
1755
- TOML
1756
-
1757
- files = { "template.yaml" => template_yaml }
1758
- files["bastion.yaml"] = bastion_yaml if bastion_yaml
1759
- files["Makefile"] = makefile_content
1760
- files["samconfig.toml"] = samconfig_toml
1761
- files
1762
80
  end
81
+ private_class_method :with_pii_distribution
82
+
83
+ # Surgical removal, post-render, not a nested conditional heredoc: a conditional nested
84
+ # heredoc broke embedded multi-line `#{shared ? ... : ...}` strings inside the resource
85
+ # block, which assume exactly one reindentation layer.
86
+ def without_dispatch_function(text, stack)
87
+ function = Regexp.escape(stack.logical_id)
88
+ # Non-greedy through the resource's own FunctionUrlConfig/AuthType closing pair, so
89
+ # WebFunction's own later "AuthType:" can't match.
90
+ text = text.sub(/^ #{function}:\n.*?\n AuthType: (?:NONE|AWS_IAM)\n/m, "")
91
+ # Removes the now-dangling `!Ref` to the resource just deleted above: SAM refuses an
92
+ # unresolved Ref at package time, not silently.
93
+ text = text.sub(/^ - LambdaInvokePolicy:\n FunctionName: !Ref #{function}\n/, "")
94
+ # WebFunction's own URL becomes the stack's only "FunctionUrl".
95
+ text.sub(web_url_output(stack), " FunctionUrl:\n Value: !GetAtt #{stack.web_logical_id}Url.FunctionUrl\n")
96
+ end
97
+ private_class_method :without_dispatch_function
1763
98
 
1764
- # Builds the CloudFront/WAFv2/logging resources a pii-marked domain is
1765
- # fronted by. `use_oac` signs requests only for the AWS_IAM case; the
1766
- # already-public WebFunction/rust_web Function URL stays as reachable
1767
- # as before. Managed cache/origin-request policy ids are AWS's own
1768
- # permanent `Managed-CachingDisabled`/`Managed-AllViewer` ids.
1769
- def pii_cloudfront_yaml(fronted_logical_id:, use_oac:, geo_restriction_type:, geo_restriction_countries:)
1770
- reindent = ->(text) { text.each_line.map { |line| line.strip.empty? ? line : " #{line}" }.join }
1771
-
1772
- # Not pre-reindented (unlike the return value as a whole, below) — each
1773
- # is spliced back into the still-being-dedented `RESOURCES` heredoc via
1774
- # `#{...}`, which the outer `reindent.call` already shifts by 2
1775
- # spaces once; reindenting here too would double it.
1776
- origin_access_control = use_oac ? <<~OAC : ""
1777
- PiiOriginAccessControl:
1778
- Type: AWS::CloudFront::OriginAccessControl
1779
- Properties:
1780
- OriginAccessControlConfig:
1781
- Name: !Sub "${AWS::StackName}-pii-oac"
1782
- OriginAccessControlOriginType: lambda
1783
- SigningBehavior: always
1784
- SigningProtocol: sigv4
1785
- OAC
1786
-
1787
- invoke_permission = <<~PERMISSION
1788
- PiiLambdaInvokePermission:
1789
- Type: AWS::Lambda::Permission
1790
- Properties:
1791
- Action: lambda:InvokeFunctionUrl
1792
- FunctionName: !Ref #{fronted_logical_id}
1793
- Principal: cloudfront.amazonaws.com
1794
- SourceArn: !Sub "arn:aws:cloudfront::${AWS::AccountId}:distribution/${PiiDistribution}"
1795
- FunctionUrlAuthType: #{use_oac ? "AWS_IAM" : "NONE"}
1796
- PERMISSION
1797
-
1798
- countries_yaml = geo_restriction_countries.map { |code| " - #{code}" }.join("\n")
1799
-
1800
- reindent.call(<<~RESOURCES)
1801
- PiiAccessLogsBucket:
1802
- Type: AWS::S3::Bucket
1803
- Properties:
1804
- # `BucketOwnerPreferred`, not the newer `BucketOwnerEnforced`
1805
- # default — CloudFront's own classic access-log delivery
1806
- # (`Logging:`, on PiiDistribution below) still authorizes itself
1807
- # via a canned ACL (`AccessControlTranslation` between accounts is
1808
- # a distinct, newer mechanism this bucket has no other account to
1809
- # need), which an ACLs-disabled bucket refuses outright.
1810
- OwnershipControls:
1811
- Rules:
1812
- - ObjectOwnership: BucketOwnerPreferred
1813
- AccessControl: LogDeliveryWrite
1814
- LifecycleConfiguration:
1815
- Rules:
1816
- - Id: ExpirePiiAccessLogs
1817
- Status: Enabled
1818
- ExpirationInDays: 365
1819
-
1820
- PiiResponseHeadersPolicy:
1821
- Type: AWS::CloudFront::ResponseHeadersPolicy
1822
- Properties:
1823
- ResponseHeadersPolicyConfig:
1824
- Name: !Sub "${AWS::StackName}-pii-headers"
1825
- SecurityHeadersConfig:
1826
- StrictTransportSecurity:
1827
- AccessControlMaxAgeSec: 63072000
1828
- IncludeSubdomains: true
1829
- Override: true
1830
- ContentTypeOptions:
1831
- Override: true
1832
- FrameOptions:
1833
- FrameOption: DENY
1834
- Override: true
1835
- ReferrerPolicy:
1836
- ReferrerPolicy: same-origin
1837
- Override: true
1838
- XSSProtection:
1839
- ModeBlock: true
1840
- Protection: true
1841
- Override: true
1842
-
1843
- PiiWebAcl:
1844
- Type: AWS::WAFv2::WebACL
1845
- Properties:
1846
- Name: !Sub "${AWS::StackName}-pii-waf"
1847
- Scope: CLOUDFRONT
1848
- DefaultAction:
1849
- Allow: {}
1850
- VisibilityConfig:
1851
- SampledRequestsEnabled: true
1852
- CloudWatchMetricsEnabled: true
1853
- MetricName: !Sub "${AWS::StackName}PiiWebAcl"
1854
- Rules:
1855
- - Name: AWSManagedRulesCommonRuleSet
1856
- Priority: 0
1857
- OverrideAction:
1858
- None: {}
1859
- Statement:
1860
- ManagedRuleGroupStatement:
1861
- VendorName: AWS
1862
- Name: AWSManagedRulesCommonRuleSet
1863
- VisibilityConfig:
1864
- SampledRequestsEnabled: true
1865
- CloudWatchMetricsEnabled: true
1866
- MetricName: !Sub "${AWS::StackName}PiiCommonRuleSet"
1867
- - Name: AWSManagedRulesKnownBadInputsRuleSet
1868
- Priority: 1
1869
- OverrideAction:
1870
- None: {}
1871
- Statement:
1872
- ManagedRuleGroupStatement:
1873
- VendorName: AWS
1874
- Name: AWSManagedRulesKnownBadInputsRuleSet
1875
- VisibilityConfig:
1876
- SampledRequestsEnabled: true
1877
- CloudWatchMetricsEnabled: true
1878
- MetricName: !Sub "${AWS::StackName}PiiKnownBadInputs"
1879
-
1880
- #{origin_access_control}
1881
- PiiDistribution:
1882
- Type: AWS::CloudFront::Distribution
1883
- Properties:
1884
- DistributionConfig:
1885
- Enabled: true
1886
- HttpVersion: http2
1887
- WebACLId: !GetAtt PiiWebAcl.Arn
1888
- Restrictions:
1889
- GeoRestriction:
1890
- RestrictionType: #{geo_restriction_type}
1891
- Locations:#{geo_restriction_type == "none" ? " []" : "\n" + countries_yaml}
1892
- Logging:
1893
- Bucket: !GetAtt PiiAccessLogsBucket.RegionalDomainName
1894
- IncludeCookies: false
1895
- Prefix: cloudfront/
1896
- Origins:
1897
- - Id: PiiOrigin
1898
- # `!Select [2, !Split ["/", ...]]` — the standard AWS-documented
1899
- # way to pull the bare hostname out of a Function URL's own
1900
- # `https://<id>.lambda-url.<region>.on.aws/` shape for use as a
1901
- # CustomOriginConfig DomainName, which admits no scheme or path.
1902
- DomainName: !Select [2, !Split ["/", !GetAtt #{fronted_logical_id}Url.FunctionUrl]]
1903
- CustomOriginConfig:
1904
- OriginProtocolPolicy: https-only
1905
- OriginSSLProtocols: [TLSv1.2]
1906
- #{use_oac ? " OriginAccessControlId: !Ref PiiOriginAccessControl" : ""}
1907
- DefaultCacheBehavior:
1908
- TargetOriginId: PiiOrigin
1909
- ViewerProtocolPolicy: redirect-to-https
1910
- AllowedMethods: [GET, HEAD, OPTIONS, PUT, PATCH, POST, DELETE]
1911
- CachedMethods: [GET, HEAD]
1912
- CachePolicyId: 4135ea2d-6df8-44a3-9df3-4b5a84be39ad
1913
- OriginRequestPolicyId: 216adef6-5c7f-47e4-b989-5492eafa07d3
1914
- ResponseHeadersPolicyId: !Ref PiiResponseHeadersPolicy
1915
-
1916
- #{invoke_permission}
1917
- RESOURCES
99
+ def web_url_output(stack)
100
+ function = Regexp.escape(stack.logical_id)
101
+ web = Regexp.escape(stack.web_logical_id)
102
+ Regexp.new(["^ FunctionUrl:\\n Value: !GetAtt #{function}Url\\.FunctionUrl\\n",
103
+ " WebFunctionUrl:\\n Value: !GetAtt #{web}Url\\.FunctionUrl\\n"].join)
1918
104
  end
105
+ private_class_method :web_url_output
1919
106
 
107
+ def bastion_yaml(stack)
108
+ Shared.bastion_yaml(stack.network, domain: stack.domain, stack_name: stack.stack_name,
109
+ bastion_parameters: stack.bastion_parameters)
110
+ end
111
+ private_class_method :bastion_yaml
1920
112
  end
1921
113
  end
1922
114
  end