hecks 3.14.0 → 4.0.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 (419) hide show
  1. checksums.yaml +4 -4
  2. data/examples/pizzas/adapters/fake_processor.rb +34 -0
  3. data/examples/pizzas/adapters/payment_signature.rb +66 -0
  4. data/examples/pizzas/adapters/payment_webhook.rb +87 -0
  5. data/examples/pizzas/bluebook/pizzas.behaviors +33 -0
  6. data/examples/pizzas/bluebook/pizzas.hecksagon +5 -3
  7. data/examples/pizzas/config.ru +22 -0
  8. data/lib/hecks/adapters/driven/prism.rb +0 -2
  9. data/lib/hecks/adapters/driven.rb +0 -2
  10. data/lib/hecks/adapters/driving/json.rb +1 -7
  11. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +2 -2
  12. data/lib/hecks/bluebook/dsl/derivation_words.rb +58 -0
  13. data/lib/hecks/bluebook/dsl/translation_builder.rb +6 -37
  14. data/lib/hecks/bluebook/expression/ast_json.rb +0 -9
  15. data/lib/hecks/bluebook/hecksagon.rb +1 -1
  16. data/lib/hecks/bluebook/meta_validator/translation_judge/rules.rb +7 -10
  17. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +4 -8
  18. data/lib/hecks/bluebook/meta_validator.rb +0 -11
  19. data/lib/hecks/bluebook/translation.rb +18 -10
  20. data/lib/hecks/bluebook/translation_expression/evaluator.rb +63 -0
  21. data/lib/hecks/bluebook/translation_expression/parser.rb +127 -0
  22. data/lib/hecks/bluebook/translation_expression.rb +39 -0
  23. data/lib/hecks/bluebook/translation_plan.rb +70 -0
  24. data/lib/hecks/bluebook.rb +2 -0
  25. data/lib/hecks/chapters.rb +3 -3
  26. data/lib/hecks/composer/adapters/claude_composer.adapter +3 -0
  27. data/lib/hecks/composer/adapters/claude_composer.rb +83 -0
  28. data/lib/hecks/composer/adapters/fake_agent.adapter +3 -0
  29. data/lib/hecks/composer/adapters/fake_agent.rb +55 -0
  30. data/lib/hecks/composer/adapters/jev.adapter +3 -0
  31. data/lib/hecks/composer/adapters/jev.rb +66 -0
  32. data/lib/hecks/composer/agent.rb +113 -0
  33. data/lib/hecks/composer/assistant.rb +110 -0
  34. data/lib/hecks/composer/behavior_run.rb +53 -0
  35. data/lib/hecks/composer/bluebook/composer.bluebook +178 -0
  36. data/lib/hecks/composer/bluebook/composer.hecksagon +16 -0
  37. data/lib/hecks/composer/board.rb +65 -0
  38. data/lib/hecks/composer/board_edit/command_text.rb +39 -0
  39. data/lib/hecks/composer/board_edit/source.rb +84 -0
  40. data/lib/hecks/composer/board_edit.rb +144 -0
  41. data/lib/hecks/composer/check.rb +89 -0
  42. data/lib/hecks/composer/domains.rb +81 -0
  43. data/lib/hecks/composer/jobs.rb +134 -0
  44. data/lib/hecks/composer/layout.rb +37 -0
  45. data/lib/hecks/composer/live.rb +150 -0
  46. data/lib/hecks/composer/outline.rb +48 -0
  47. data/lib/hecks/composer/page.html +989 -0
  48. data/lib/hecks/composer/panels.rb +142 -0
  49. data/lib/hecks/composer/parse.rb +83 -0
  50. data/lib/hecks/composer/plain.rb +28 -0
  51. data/lib/hecks/composer/ports/composer_agent.port +7 -0
  52. data/lib/hecks/composer/session.rb +141 -0
  53. data/lib/hecks/composer/toolbox.rb +166 -0
  54. data/lib/hecks/composer/web.rb +156 -0
  55. data/lib/hecks/composer/workbench.rb +51 -0
  56. data/lib/hecks/composer/workspace.rb +81 -0
  57. data/lib/hecks/composer.rb +17 -0
  58. data/lib/hecks/corpus/adapters.rb +22 -0
  59. data/lib/hecks/corpus/rust.rb +1 -1
  60. data/lib/hecks/corpus/worlds.rb +19 -0
  61. data/lib/hecks/corpus.rb +5 -0
  62. data/lib/hecks/forms/app/commands.rb +2 -3
  63. data/lib/hecks/forms/app/queries.rb +2 -3
  64. data/lib/hecks/forms/app/records.rb +17 -5
  65. data/lib/hecks/forms/command_form_renderer.rb +42 -16
  66. data/lib/hecks/forms/field_controls.rb +2 -2
  67. data/lib/hecks/forms/field_renderer.rb +1 -1
  68. data/lib/hecks/forms/field_shape.rb +36 -17
  69. data/lib/hecks/forms/index_renderer.rb +2 -1
  70. data/lib/hecks/forms/page.rb +1 -1
  71. data/lib/hecks/forms/params.rb +0 -13
  72. data/lib/hecks/forms/query_form_renderer.rb +7 -6
  73. data/lib/hecks/forms/record_renderer.rb +11 -7
  74. data/lib/hecks/forms/rule_words.rb +100 -0
  75. data/lib/hecks/forms/value_object_shape.rb +2 -1
  76. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +0 -3
  77. data/lib/hecks/fuzzing/isolated_boot/worlds.rb +3 -6
  78. data/lib/hecks/fuzzing/replay/filters.rb +2 -6
  79. data/lib/hecks/gate/stages.yml +25 -0
  80. data/lib/hecks/grammar/evolve/seeds.rb +0 -6
  81. data/lib/hecks/grammar/evolve.rb +0 -3
  82. data/lib/hecks/grammar/translation.bluebook +1 -1
  83. data/lib/hecks/hecks/adapters/codebase/composing.rb +56 -0
  84. data/lib/hecks/hecks/adapters/codebase/source_tree.rb +3 -1
  85. data/lib/hecks/hecks/adapters/journal_store/changes.rb +0 -26
  86. data/lib/hecks/hecks/adapters/journal_store/examination.rb +2 -23
  87. data/lib/hecks/hecks/adapters/journal_store/readings.rb +3 -11
  88. data/lib/hecks/hecks/adapters/journal_store.rb +0 -11
  89. data/lib/hecks/hecks/adapters/rehearsal/inquiry.rb +4 -11
  90. data/lib/hecks/hecks/adapters/rehearsal/memory_store.rb +0 -9
  91. data/lib/hecks/hecks/adapters/rehearsal/postgres_reading.rb +0 -4
  92. data/lib/hecks/hecks/codebase.behaviors +6 -0
  93. data/lib/hecks/hecks/codebase.bluebook +21 -0
  94. data/lib/hecks/hecks/composer.behaviors +68 -0
  95. data/lib/hecks/hecks/context_map.hecksagon +1 -1
  96. data/lib/hecks/hecks/custodian.behaviors +0 -6
  97. data/lib/hecks/hecks/custodian.bluebook +3 -80
  98. data/lib/hecks/hecks/hecks.hecksagon +2 -2
  99. data/lib/hecks/hecks/hecks.world +1 -1
  100. data/lib/hecks/hecks/rehearsal.bluebook +7 -13
  101. data/lib/hecks/kernel_capabilities/templates.rb +4 -4
  102. data/lib/hecks/kernel_capabilities.rb +1 -1
  103. data/lib/hecks/language/bluebook/vocabulary.bluebook +2 -3
  104. data/lib/hecks/language/translation/translation_aggregate.bluebook +21 -24
  105. data/lib/hecks/ports/access_control.rb +2 -2
  106. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +3 -41
  107. data/lib/hecks/ports/persistence/plugins/era/lineage/coverage.rb +7 -7
  108. data/lib/hecks/ports/persistence/plugins/era/lineage/rewrite.rb +25 -2
  109. data/lib/hecks/ports/persistence/plugins/era/lineage/rules.rb +4 -4
  110. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +17 -25
  111. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/base_sql.rb +0 -12
  112. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/domain_rename.rb +1 -1
  113. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +2 -22
  114. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +0 -1
  115. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +9 -3
  116. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +3 -29
  117. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/refusals.rb +2 -26
  118. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +6 -33
  119. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +3 -3
  120. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +2 -4
  121. data/lib/hecks/ports/persistence/plugins/era/translation/expression_sql.rb +52 -0
  122. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +23 -29
  123. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +1 -1
  124. data/lib/hecks/ports/persistence/plugins/era/translation.rb +0 -1
  125. data/lib/hecks/ports/persistence/plugins/era.rb +3 -6
  126. data/lib/hecks/ports.rb +0 -1
  127. data/lib/hecks/projection_files.rb +3 -1
  128. data/lib/hecks/projections/deploy/templates/lambda/nat_comment_oauth.tmpl +1 -1
  129. data/lib/hecks/projections/glossary/entries.rb +10 -4
  130. data/lib/hecks/projections/glossary/groups.rb +42 -0
  131. data/lib/hecks/projections/glossary/html/page.rb +7 -2
  132. data/lib/hecks/projections/glossary/html/parser.rb +1 -1
  133. data/lib/hecks/projections/glossary/html/sections.rb +31 -11
  134. data/lib/hecks/projections/glossary/html.rb +1 -2
  135. data/lib/hecks/projections/glossary/markdown.rb +31 -28
  136. data/lib/hecks/projections/glossary/mermaid.rb +3 -0
  137. data/lib/hecks/projections/glossary/page.css +143 -153
  138. data/lib/hecks/projections/glossary/page.js +63 -3
  139. data/lib/hecks/projections/glossary/sentences/actions.rb +88 -0
  140. data/lib/hecks/projections/glossary/sentences/paths.rb +77 -0
  141. data/lib/hecks/projections/glossary/sentences/queries.rb +90 -0
  142. data/lib/hecks/projections/glossary/sentences/value_objects.rb +32 -5
  143. data/lib/hecks/projections/glossary/sentences.rb +13 -3
  144. data/lib/hecks/projections/glossary/slugs.rb +4 -1
  145. data/lib/hecks/projections/glossary.rb +2 -1
  146. data/lib/hecks/projections/plain_english.rb +97 -0
  147. data/lib/hecks/projections/rust_vocabulary/arguments.rb +2 -2
  148. data/lib/hecks/projections/rust_vocabulary/enum_body.rb +20 -25
  149. data/lib/hecks/projections/rust_vocabulary/tests.rb +2 -2
  150. data/lib/hecks/projections/rust_vocabulary.rb +14 -6
  151. data/lib/hecks/projections/site/cms_editor/color.rb +0 -6
  152. data/lib/hecks/projections/statements/actions.rb +99 -0
  153. data/lib/hecks/projections/statements/relationships.rb +86 -0
  154. data/lib/hecks/projections/statements.rb +15 -24
  155. data/lib/hecks/projector/exporter/adapters.rb +112 -0
  156. data/lib/hecks/projector/exporter/translations.rb +18 -35
  157. data/lib/hecks/projector/exporter/worlds.rb +103 -0
  158. data/lib/hecks/projector/exporter.rb +5 -1
  159. data/lib/hecks/quality_control/adapters/agent.rb +6 -7
  160. data/lib/hecks/quality_control/adapters/agent_profile.rb +16 -114
  161. data/lib/hecks/quality_control/cli/qa_mine_combinations.rb +1 -1
  162. data/lib/hecks/runtime/loader/boot_steps.rb +0 -20
  163. data/lib/hecks/runtime/outbox.rb +2 -9
  164. data/lib/hecks/runtime/port_operation_interpreter.rb +1 -9
  165. data/lib/hecks/runtime/remote_dispatcher.rb +2 -2
  166. data/lib/hecks/runtime/value.rb +13 -0
  167. data/lib/hecks/rust_build/project_rust/target_ir.rb +23 -4
  168. data/lib/hecks/rust_build/wasm.rb +0 -12
  169. data/lib/hecks/rust_build/write_if_changed.rb +12 -12
  170. data/lib/hecks/rust_format.rb +77 -0
  171. data/lib/hecks/tools/fuzz_sweep/findings.rb +3 -9
  172. data/lib/hecks/tools/fuzz_sweep.rb +1 -3
  173. data/lib/hecks/tools/translation_audit/verdicts.rb +3 -28
  174. data/lib/hecks/tools/translation_audit.rb +8 -10
  175. data/lib/hecks/tools/translation_scaffold/reporting.rb +1 -1
  176. data/lib/hecks/version.rb +1 -1
  177. data/lib/hecks/vocabulary.rb +2 -1
  178. data/lib/hecks.rb +3 -0
  179. data/prebuilt/{a883a7e568667aa5ac904c82daff356bdf9e996252a36af6def4a4a43efb967e.marshal → 02c09c73b272c1fb5a9ae6d87bb5e5e529208e00f6b4b597dc2c0fd70a9c0964.marshal} +0 -0
  180. data/prebuilt/{1a6f596be124228be93eba1d86dbc9d2933190fa389b284331241cb9f27115a0.marshal → bb84a835cf6a15136bad5ca03aa5bf93a5b92f5f72444618d3ef2412b987fb32.marshal} +0 -0
  181. data/prebuilt/{verdicts-872993b1aa26d73fe48f28251374b8585fed145d02aab42be0f00de386b6ff80.json → verdicts-f1f10da14c8e8f01b4c9b7e3e2d74be8cd2bf4676364ed17fa16975d2a076d75.json} +1 -1
  182. data/rust/Cargo.toml +40 -0
  183. data/rust/build/Cargo.toml +36 -0
  184. data/rust/build/clippy.toml +4 -0
  185. data/rust/build/src/adapters_pass.rs +196 -0
  186. data/rust/build/src/binding_pass.rs +54 -13
  187. data/rust/build/src/build_artifact.rs +23 -5
  188. data/rust/build/src/cargo_sync.rs +69 -26
  189. data/rust/build/src/lineage_pass.rs +101 -87
  190. data/rust/build/src/main.rs +11 -2
  191. data/rust/build/src/pipeline.rs +80 -19
  192. data/rust/build/src/reserved_names.rs +6 -50
  193. data/rust/build/src/resolve.rs +82 -35
  194. data/rust/build/src/rustfmt.rs +100 -0
  195. data/rust/build/src/seams_pass.rs +85 -16
  196. data/rust/build/src/subprocess.rs +30 -6
  197. data/rust/build/src/tmp.rs +5 -2
  198. data/rust/build/src/translation_pass.rs +72 -22
  199. data/rust/build/src/world_pass.rs +109 -0
  200. data/rust/clippy.toml +4 -0
  201. data/rust/codegen/Cargo.toml +36 -0
  202. data/rust/codegen/clippy.toml +4 -0
  203. data/rust/codegen/src/asks.rs +40 -14
  204. data/rust/codegen/src/attr.rs +2 -2
  205. data/rust/codegen/src/bridging.rs +676 -117
  206. data/rust/codegen/src/commands.rs +979 -373
  207. data/rust/codegen/src/constraints.rs +88 -25
  208. data/rust/codegen/src/dependency_planning.rs +128 -34
  209. data/rust/codegen/src/domain_generator.rs +300 -170
  210. data/rust/codegen/src/exemplar.rs +87 -32
  211. data/rust/codegen/src/expr_emitter.rs +54 -6
  212. data/rust/codegen/src/fielded.rs +193 -41
  213. data/rust/codegen/src/hecks_naming.rs +124 -16
  214. data/rust/codegen/src/hecks_naming_tests.rs +81 -0
  215. data/rust/codegen/src/json.rs +80 -44
  216. data/rust/codegen/src/json_codec.rs +585 -149
  217. data/rust/codegen/src/literal.rs +104 -138
  218. data/rust/codegen/src/main.rs +145 -52
  219. data/rust/codegen/src/manifest.rs +76 -14
  220. data/rust/codegen/src/mutations.rs +1212 -239
  221. data/rust/codegen/src/naming.rs +111 -60
  222. data/rust/codegen/src/optional_pass.rs +90 -28
  223. data/rust/codegen/src/ports.rs +5 -4
  224. data/rust/codegen/src/prelude.rs +139 -49
  225. data/rust/codegen/src/queries.rs +601 -226
  226. data/rust/codegen/src/reactions.rs +268 -74
  227. data/rust/codegen/src/read_models.rs +606 -205
  228. data/rust/codegen/src/reference_specs.rs +16 -3
  229. data/rust/codegen/src/registry.rs +128 -79
  230. data/rust/codegen/src/reserved_names.rs +6 -50
  231. data/rust/codegen/src/shared.rs +71 -22
  232. data/rust/codegen/src/sidecars.rs +15 -5
  233. data/rust/codegen/src/skip_reason.rs +8 -2
  234. data/rust/codegen/src/types.rs +306 -108
  235. data/rust/host/Cargo.lock +1 -0
  236. data/rust/host/Cargo.toml +40 -4
  237. data/rust/host/HECKS_RELEASE +1 -1
  238. data/rust/host/clippy.toml +4 -0
  239. data/rust/host/src/api.rs +698 -218
  240. data/rust/host/src/auth.rs +491 -214
  241. data/rust/host/src/b64.rs +91 -0
  242. data/rust/host/src/bin/expr_harness.rs +6 -2
  243. data/rust/host/src/bin/lineage_harness.rs +47 -11
  244. data/rust/host/src/bin/mint_harness.rs +70 -37
  245. data/rust/host/src/bin/validate_harness.rs +16 -8
  246. data/rust/host/src/boot.rs +144 -68
  247. data/rust/host/src/boundary_fuzz/api.rs +130 -32
  248. data/rust/host/src/boundary_fuzz/auth.rs +108 -23
  249. data/rust/host/src/boundary_fuzz/expr_json.rs +85 -11
  250. data/rust/host/src/boundary_fuzz/ir.rs +164 -12
  251. data/rust/host/src/boundary_fuzz/server.rs +283 -36
  252. data/rust/host/src/boundary_fuzz/web.rs +56 -8
  253. data/rust/host/src/checkout.rs +78 -32
  254. data/rust/host/src/clock.rs +8 -0
  255. data/rust/host/src/commerce_ir.rs +333 -95
  256. data/rust/host/src/crypto.rs +33 -0
  257. data/rust/host/src/dispatch.rs +990 -331
  258. data/rust/host/src/expr_json.rs +1021 -273
  259. data/rust/host/src/extension.rs +24 -4
  260. data/rust/host/src/field_hints.rs +11 -7
  261. data/rust/host/src/fuzz_support.rs +85 -13
  262. data/rust/host/src/host.rs +100 -0
  263. data/rust/host/src/http.rs +22 -0
  264. data/rust/host/src/ir.rs +326 -16
  265. data/rust/host/src/journal.rs +277 -176
  266. data/rust/host/src/lambda_client.rs +219 -53
  267. data/rust/host/src/lib.rs +16 -4
  268. data/rust/host/src/log.rs +62 -11
  269. data/rust/host/src/mint.rs +1153 -326
  270. data/rust/host/src/needs.rs +185 -44
  271. data/rust/host/src/payments/keystore.rs +197 -45
  272. data/rust/host/src/payments/roles.rs +39 -7
  273. data/rust/host/src/payments/tests.rs +1427 -282
  274. data/rust/host/src/payments.rs +391 -112
  275. data/rust/host/src/platform_signin/attack_tests.rs +5 -5
  276. data/rust/host/src/platform_signin/config.rs +11 -11
  277. data/rust/host/src/platform_signin/cookie.rs +2 -2
  278. data/rust/host/src/platform_signin/extension.rs +2 -1
  279. data/rust/host/src/platform_signin/handler.rs +9 -8
  280. data/rust/host/src/platform_signin/jti.rs +4 -2
  281. data/rust/host/src/platform_signin/jwks.rs +2 -1
  282. data/rust/host/src/platform_signin/jws.rs +2 -37
  283. data/rust/host/src/platform_signin/known_answer_tests.rs +5 -1
  284. data/rust/host/src/platform_signin/mode_tests.rs +5 -3
  285. data/rust/host/src/platform_signin/pages.rs +20 -20
  286. data/rust/host/src/platform_signin/session.rs +8 -1
  287. data/rust/host/src/platform_signin/settings.rs +6 -6
  288. data/rust/host/src/platform_signin/strict_json.rs +12 -9
  289. data/rust/host/src/platform_signin/verify.rs +41 -41
  290. data/rust/host/src/platform_signin/wiring_support.rs +13 -18
  291. data/rust/host/src/presentation.rs +207 -63
  292. data/rust/host/src/presentation_write.rs +502 -187
  293. data/rust/host/src/query_step.rs +69 -18
  294. data/rust/host/src/rate_limit.rs +541 -142
  295. data/rust/host/src/reference_transform.rs +77 -25
  296. data/rust/host/src/reference_validate.rs +443 -104
  297. data/rust/host/src/resend.rs +190 -55
  298. data/rust/host/src/secrets.rs +116 -21
  299. data/rust/host/src/server.rs +483 -104
  300. data/rust/host/src/storage_shape.rs +88 -21
  301. data/rust/host/src/test_pg.rs +17 -7
  302. data/rust/host/src/ui_schema.rs +495 -207
  303. data/rust/host/src/wasm_runner.rs +21 -6
  304. data/rust/host/src/web/commerce.rs +101 -23
  305. data/rust/host/src/web/newsletter/cooldown.rs +10 -3
  306. data/rust/host/src/web/newsletter/template.rs +26 -7
  307. data/rust/host/src/web/newsletter/tests.rs +112 -23
  308. data/rust/host/src/web/newsletter.rs +209 -59
  309. data/rust/host/src/web/newsletter_send.rs +251 -79
  310. data/rust/host/src/web/registration_receipt/tests.rs +16 -4
  311. data/rust/host/src/web/registration_receipt.rs +100 -24
  312. data/rust/host/src/web/registrations.rs +1384 -332
  313. data/rust/host/src/web/resend_webhook.rs +138 -80
  314. data/rust/host/src/web.rs +2416 -508
  315. data/rust/lsp/Cargo.toml +36 -0
  316. data/rust/lsp/clippy.toml +4 -0
  317. data/rust/lsp/src/json.rs +29 -22
  318. data/rust/lsp/src/main.rs +81 -31
  319. data/rust/lsp/src/outline.rs +23 -7
  320. data/rust/lsp/src/rpc.rs +17 -4
  321. data/rust/parser/Cargo.toml +36 -0
  322. data/rust/parser/clippy.toml +4 -0
  323. data/rust/parser/src/build/closed_sets.rs +6 -1
  324. data/rust/parser/src/build/identity.rs +4 -3
  325. data/rust/parser/src/build/mod.rs +1 -1
  326. data/rust/parser/src/build/named_pattern.rs +4 -1
  327. data/rust/parser/src/build/naming.rs +2 -80
  328. data/rust/parser/src/build/pattern_subset.rs +6 -8
  329. data/rust/parser/src/build/query_derive.rs +3 -1
  330. data/rust/parser/src/build/query_inference.rs +14 -5
  331. data/rust/parser/src/build/read_model.rs +4 -1
  332. data/rust/parser/src/build/references.rs +4 -3
  333. data/rust/parser/src/canonical.rs +34 -15
  334. data/rust/parser/src/emit.rs +187 -105
  335. data/rust/parser/src/expr/ast_json.rs +134 -42
  336. data/rust/parser/src/expr/evaluator.rs +27 -9
  337. data/rust/parser/src/expr/mod.rs +54 -13
  338. data/rust/parser/src/expr/resolver.rs +299 -92
  339. data/rust/parser/src/ir.rs +35 -3
  340. data/rust/parser/src/keywords.rs +7168 -516
  341. data/rust/parser/src/lex.rs +11 -25
  342. data/rust/parser/src/lib.rs +1 -0
  343. data/rust/parser/src/main.rs +65 -6
  344. data/rust/parser/src/parse/adapter.rs +157 -0
  345. data/rust/parser/src/parse/aggregate.rs +19 -10
  346. data/rust/parser/src/parse/chapter.rs +16 -17
  347. data/rust/parser/src/parse/command.rs +35 -16
  348. data/rust/parser/src/parse/domain_port.rs +4 -7
  349. data/rust/parser/src/parse/entity.rs +21 -11
  350. data/rust/parser/src/parse/hecksagon.rs +10 -4
  351. data/rust/parser/src/parse/lifecycle.rs +9 -5
  352. data/rust/parser/src/parse/mod.rs +22 -91
  353. data/rust/parser/src/parse/needs.rs +21 -12
  354. data/rust/parser/src/parse/policy.rs +2 -2
  355. data/rust/parser/src/parse/process_manager.rs +9 -11
  356. data/rust/parser/src/parse/query.rs +8 -4
  357. data/rust/parser/src/parse/read_model.rs +57 -16
  358. data/rust/parser/src/parse/value_object.rs +7 -2
  359. data/rust/parser/src/parse/world.rs +322 -0
  360. data/rust/parser/src/ruby_value.rs +7 -6
  361. data/rust/src/exemplar/json.rs +4 -4
  362. data/rust/src/exemplar/mutations.rs +18 -0
  363. data/rust/src/exemplar/read_models.rs +1 -10
  364. data/rust/src/kernel/attribute_shapes/composite.rs +54 -14
  365. data/rust/src/kernel/attribute_shapes/list.rs +2 -2
  366. data/rust/src/kernel/attribute_shapes/mod.rs +8 -8
  367. data/rust/src/kernel/attribute_shapes/scalar.rs +2 -2
  368. data/rust/src/kernel/cli.rs +229 -112
  369. data/rust/src/kernel/dispatch.rs +323 -115
  370. data/rust/src/kernel/dispatch_guard_tests.rs +327 -65
  371. data/rust/src/kernel/expr.rs +203 -86
  372. data/rust/src/kernel/expression_operators/arithmetic.rs +138 -25
  373. data/rust/src/kernel/expression_operators/comparison.rs +87 -2
  374. data/rust/src/kernel/expression_operators/enumeration.rs +106 -22
  375. data/rust/src/kernel/expression_operators/logical.rs +9 -3
  376. data/rust/src/kernel/expression_operators/membership.rs +3 -1
  377. data/rust/src/kernel/expression_operators/mod.rs +34 -21
  378. data/rust/src/kernel/expression_operators/pattern_match.rs +46 -11
  379. data/rust/src/kernel/expression_operators/positional.rs +84 -30
  380. data/rust/src/kernel/expression_operators/presence.rs +52 -13
  381. data/rust/src/kernel/expression_operators/sign_test.rs +3 -1
  382. data/rust/src/kernel/expression_operators/sized.rs +24 -8
  383. data/rust/src/kernel/expression_operators/text.rs +120 -27
  384. data/rust/src/kernel/expression_operators/to_string.rs +10 -4
  385. data/rust/src/kernel/json.rs +230 -148
  386. data/rust/src/kernel/mod.rs +46 -33
  387. data/rust/src/kernel/named_query.rs +63 -15
  388. data/rust/src/kernel/needs.rs +112 -25
  389. data/rust/src/kernel/orchestrate.rs +980 -581
  390. data/rust/src/kernel/orchestrate_guard_tests.rs +538 -140
  391. data/rust/src/kernel/pattern.rs +107 -34
  392. data/rust/src/kernel/query_comparators.rs +118 -41
  393. data/rust/src/kernel/query_ordering.rs +13 -5
  394. data/rust/src/kernel/read_model.rs +479 -181
  395. data/rust/src/kernel/reference_lookup.rs +122 -26
  396. data/rust/src/kernel/repository.rs +293 -66
  397. data/rust/src/kernel/routing.rs +58 -42
  398. data/rust/src/kernel/ruby_float.rs +139 -0
  399. data/rust/src/kernel/vocab/aggregate_dispatch_order.rs +57 -57
  400. data/rust/src/kernel/vocab/entity_dispatch_order.rs +54 -54
  401. data/rust/src/kernel/vocab/field_hint.rs +19 -25
  402. data/rust/src/kernel/vocab/mod.rs +6 -6
  403. data/rust/src/kernel/vocab/query_comparator.rs +23 -24
  404. data/rust/src/kernel/vocab/refusal_template.rs +501 -297
  405. data/rust/src/lib.rs +50 -0
  406. data/rust/src/main.rs +4 -1
  407. data/rust/web/Cargo.toml +36 -0
  408. data/rust/web/clippy.toml +4 -0
  409. metadata +82 -15
  410. data/examples/pizzas/bluebook/hecksagon/mock_stripe_payment_adapter.rb +0 -36
  411. data/lib/hecks/adapters/driven/google_authentication.adapter +0 -3
  412. data/lib/hecks/adapters/driven/google_authentication.rb +0 -99
  413. data/lib/hecks/adapters/driven/mock_stripe_adapter.adapter +0 -3
  414. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +0 -32
  415. data/lib/hecks/ports/authentication.port +0 -6
  416. data/lib/hecks/ports/authentication.rb +0 -67
  417. data/lib/hecks/ports/persistence/plugins/era/translation/approval_file.rb +0 -255
  418. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +0 -22
  419. data/rust/host/src/approval.rs +0 -652
@@ -2,6 +2,7 @@
2
2
  //! `dispatch_body` with the Lambda custom-runtime path, so neither repeats the other's logic.
3
3
 
4
4
  use crate::dispatch;
5
+ use crate::host::Host;
5
6
  use crate::journal::LineageConfig;
6
7
  use crate::lambda_client::LambdaInvoker;
7
8
  use crate::log;
@@ -17,34 +18,43 @@ use axum::Router;
17
18
  use lambda_runtime::Error;
18
19
  use serde_json::Value;
19
20
  use std::net::SocketAddr;
20
- use std::path::{Path, PathBuf};
21
+ use std::path::PathBuf;
21
22
  use std::sync::Arc;
22
23
  use tokio::sync::Mutex;
23
24
  use tokio_postgres::Client;
24
25
 
25
26
  /// The per-request dispatch decision, shared between the Lambda custom-runtime path and the
26
27
  /// axum fallback route below. `body` is already-parsed JSON in one of its recognized shapes.
27
- pub async fn dispatch_body(
28
- body: Value,
29
- client: &Mutex<Client>,
30
- wasm_path: &Path,
31
- config: &LineageConfig,
32
- invoker: &dyn LambdaInvoker,
33
- ) -> Result<Value, Error> {
34
- if let Some(response) = web::render(&body, client, wasm_path, config, invoker).await {
28
+ pub async fn dispatch_body(body: Value, host: Host<'_>) -> Result<Value, Error> {
29
+ let Host {
30
+ client,
31
+ wasm_path,
32
+ config,
33
+ ..
34
+ } = host;
35
+ if let Some(response) = web::render(&body, host).await {
35
36
  return Ok(response);
36
37
  }
37
38
 
38
- if body.get("read").and_then(|v| v.as_bool()) == Some(true) {
39
+ if body.get("read").and_then(serde_json::Value::as_bool) == Some(true) {
39
40
  // `{e:#}`: anyhow's alternate Display walks `source()` to the real message,
40
41
  // where a bare `tokio_postgres` error's Display is just "db error".
41
- let result = dispatch::read(client, wasm_path).await.map_err(|e| format!("{e:#}"))?;
42
+ let result = dispatch::read(client, wasm_path)
43
+ .await
44
+ .map_err(|e| format!("{e:#}"))?;
42
45
  return Ok(result);
43
46
  }
44
47
 
45
48
  // A query step reads current state and writes nothing to the journal.
46
49
  if body.get("query").is_some() {
47
- return Ok(crate::query_step::answer(&body, crate::ir::ir(), &config.domain, client, wasm_path).await?);
50
+ return Ok(crate::query_step::answer(
51
+ &body,
52
+ crate::ir::ir(),
53
+ &config.domain,
54
+ client,
55
+ wasm_path,
56
+ )
57
+ .await?);
48
58
  }
49
59
 
50
60
  let verb = body
@@ -52,30 +62,45 @@ pub async fn dispatch_body(
52
62
  .and_then(|v| v.as_str())
53
63
  .ok_or("event missing \"verb\"")?
54
64
  .to_string();
55
- let role = body.get("role").and_then(|v| v.as_str()).map(|s| s.to_string());
65
+ let role = body
66
+ .get("role")
67
+ .and_then(|v| v.as_str())
68
+ .map(std::string::ToString::to_string);
56
69
  // Who the caller is, for Governance. Honored only where the internal protocol is: from this
57
70
  // host's own peers (`trusts_internal_dispatch`), never from a public caller.
58
- let actor_id = body.get("actor_id").and_then(|v| v.as_str()).map(|s| s.to_string());
59
- let caller = dispatch::Caller { role: role.as_deref(), actor_id: actor_id.as_deref() };
71
+ let actor_id = body
72
+ .get("actor_id")
73
+ .and_then(|v| v.as_str())
74
+ .map(std::string::ToString::to_string);
75
+ let caller = dispatch::Caller {
76
+ role: role.as_deref(),
77
+ actor_id: actor_id.as_deref(),
78
+ };
60
79
 
61
80
  let outcome = if let Some(to) = body.get("to").cloned() {
62
81
  if body.get("args").is_some() {
63
82
  return Err("cannot combine to/with with legacy args".into());
64
83
  }
65
- let facts = body.get("with").cloned().unwrap_or_else(|| serde_json::json!({}));
66
- dispatch::handle_routed_as(client, wasm_path, &verb, to, facts, caller, config, invoker)
84
+ let facts = body
85
+ .get("with")
86
+ .cloned()
87
+ .unwrap_or_else(|| serde_json::json!({}));
88
+ dispatch::handle_routed_as(host, &verb, to, facts, caller)
67
89
  .await
68
90
  .map_err(|e| format!("{e:#}"))?
69
91
  } else if let Some(facts) = body.get("with").cloned() {
70
92
  if body.get("args").is_some() {
71
93
  return Err("cannot combine \"with\" with legacy args".into());
72
94
  }
73
- dispatch::handle_facts_as(client, wasm_path, &verb, facts, caller, config, invoker)
95
+ dispatch::handle_facts_as(host, &verb, facts, caller)
74
96
  .await
75
97
  .map_err(|e| format!("{e:#}"))?
76
98
  } else {
77
- let args = body.get("args").cloned().unwrap_or_else(|| serde_json::json!({}));
78
- dispatch::handle_as(client, wasm_path, &verb, args, caller, config, invoker)
99
+ let args = body
100
+ .get("args")
101
+ .cloned()
102
+ .unwrap_or_else(|| serde_json::json!({}));
103
+ dispatch::handle_as(host, &verb, args, caller)
79
104
  .await
80
105
  .map_err(|e| format!("{e:#}"))?
81
106
  };
@@ -93,10 +118,24 @@ pub struct ServerState {
93
118
  pub limits: Arc<RateLimits>,
94
119
  }
95
120
 
121
+ impl ServerState {
122
+ /// The shared pieces a dispatch needs, borrowed for one call.
123
+ pub fn host(&self) -> Host<'_> {
124
+ Host::shared(
125
+ &self.client,
126
+ &self.wasm_path,
127
+ &self.lineage_config,
128
+ &self.invoker,
129
+ )
130
+ }
131
+ }
132
+
96
133
  /// The document `GET /version` serves: era, IR hash, and build — all fixed for the life of
97
134
  /// the process, so this is built once at boot. Carries no secrets.
98
135
  pub fn version_body(era: &str, ir_hash: &str, build_env: Option<&str>) -> Value {
99
- let build = build_env.filter(|v| !v.is_empty()).unwrap_or(env!("CARGO_PKG_VERSION"));
136
+ let build = build_env
137
+ .filter(|v| !v.is_empty())
138
+ .unwrap_or(env!("CARGO_PKG_VERSION"));
100
139
  serde_json::json!({ "era": era, "ir_hash": ir_hash, "build": build })
101
140
  }
102
141
 
@@ -104,13 +143,19 @@ pub fn version_body(era: &str, ir_hash: &str, build_env: Option<&str>) -> Value
104
143
  /// request answers a clone of the boot-time document, never touching auth or dispatch.
105
144
  pub fn version_router(body: Value) -> Router {
106
145
  let body = Arc::new(body);
107
- Router::new().route("/version", get(move || std::future::ready(axum::Json(body.as_ref().clone()))))
146
+ Router::new().route(
147
+ "/version",
148
+ get(move || std::future::ready(axum::Json(body.as_ref().clone()))),
149
+ )
108
150
  }
109
151
 
110
152
  /// Compiles the domain's wasm module, then binds `$PORT` (falls back to 8080) and serves
111
153
  /// forever. Nothing listens until the compile finishes, so the health check never lies.
112
154
  pub async fn serve(state: ServerState, version: Value) -> Result<(), Error> {
113
- let port: u16 = std::env::var("PORT").ok().and_then(|v| v.parse().ok()).unwrap_or(8080);
155
+ let port: u16 = std::env::var("PORT")
156
+ .ok()
157
+ .and_then(|v| v.parse().ok())
158
+ .unwrap_or(8080);
114
159
 
115
160
  warm_wasm(&state.wasm_path).await;
116
161
 
@@ -122,7 +167,11 @@ pub async fn serve(state: ServerState, version: Value) -> Result<(), Error> {
122
167
 
123
168
  /// The router and serve loop over an already-bound `listener`, split out so a test can
124
169
  /// run the real service on an ephemeral port.
125
- async fn serve_on(listener: tokio::net::TcpListener, state: ServerState, version: Value) -> Result<(), Error> {
170
+ async fn serve_on(
171
+ listener: tokio::net::TcpListener,
172
+ state: ServerState,
173
+ version: Value,
174
+ ) -> Result<(), Error> {
126
175
  let app = Router::new()
127
176
  .route("/", get(health))
128
177
  .fallback(any(dispatch_route))
@@ -130,7 +179,11 @@ async fn serve_on(listener: tokio::net::TcpListener, state: ServerState, version
130
179
  .merge(version_router(version));
131
180
 
132
181
  // The connect info is the TCP peer, which `rate_limit` needs to tell a visitor from a proxy.
133
- axum::serve(listener, app.into_make_service_with_connect_info::<SocketAddr>()).await?;
182
+ axum::serve(
183
+ listener,
184
+ app.into_make_service_with_connect_info::<SocketAddr>(),
185
+ )
186
+ .await?;
134
187
  Ok(())
135
188
  }
136
189
 
@@ -143,8 +196,14 @@ async fn warm_wasm(wasm_path: &Arc<PathBuf>) {
143
196
  Ok(Ok(())) => {
144
197
  phase.end();
145
198
  }
146
- Ok(Err(e)) => log::error("wasm_warm_failed", serde_json::json!({ "error": format!("{e:#}") })),
147
- Err(e) => log::error("wasm_warm_failed", serde_json::json!({ "error": format!("{e}") })),
199
+ Ok(Err(e)) => log::error(
200
+ "wasm_warm_failed",
201
+ serde_json::json!({ "error": format!("{e:#}") }),
202
+ ),
203
+ Err(e) => log::error(
204
+ "wasm_warm_failed",
205
+ serde_json::json!({ "error": format!("{e}") }),
206
+ ),
148
207
  }
149
208
  }
150
209
 
@@ -179,30 +238,55 @@ async fn dispatch_route(
179
238
  response
180
239
  }
181
240
 
182
- async fn route_request(state: ServerState, peer: SocketAddr, method: Method, uri: Uri, headers: HeaderMap, body: Bytes) -> Response {
241
+ async fn route_request(
242
+ state: ServerState,
243
+ peer: SocketAddr,
244
+ method: Method,
245
+ uri: Uri,
246
+ headers: HeaderMap,
247
+ body: Bytes,
248
+ ) -> Response {
183
249
  let envelope = match admit(&state.limits, peer, &method, &uri, &headers, &body) {
184
250
  Ok(envelope) => envelope,
185
251
  Err(refusal) => return *refusal,
186
252
  };
187
253
 
188
- match dispatch_body(envelope, &state.client, &state.wasm_path, &state.lineage_config, state.invoker.as_ref()).await {
254
+ match dispatch_body(envelope, state.host()).await {
189
255
  Ok(value) => value_to_response(value),
190
256
  Err(e) => {
191
- log::error("dispatch_failed", serde_json::json!({ "error": format!("{e}") }));
192
- (StatusCode::INTERNAL_SERVER_ERROR, axum::Json(serde_json::json!({ "error": format!("{e}") }))).into_response()
257
+ log::error(
258
+ "dispatch_failed",
259
+ serde_json::json!({ "error": format!("{e}") }),
260
+ );
261
+ (
262
+ StatusCode::INTERNAL_SERVER_ERROR,
263
+ axum::Json(serde_json::json!({ "error": format!("{e}") })),
264
+ )
265
+ .into_response()
193
266
  }
194
267
  }
195
268
  }
196
269
 
197
270
  /// Turns a raw HTTP request into `dispatch_body`'s envelope, or into the response that ends
198
271
  /// it first: a `400` for non-JSON, a `429` for a caller over a public route's rate limit.
199
- fn admit(limits: &RateLimits, peer: SocketAddr, method: &Method, uri: &Uri, headers: &HeaderMap, body: &Bytes) -> Result<Value, Box<Response>> {
272
+ fn admit(
273
+ limits: &RateLimits,
274
+ peer: SocketAddr,
275
+ method: &Method,
276
+ uri: &Uri,
277
+ headers: &HeaderMap,
278
+ body: &Bytes,
279
+ ) -> Result<Value, Box<Response>> {
200
280
  let parsed: Value = if body.is_empty() {
201
281
  serde_json::json!({})
202
282
  } else {
203
283
  match serde_json::from_slice(body) {
204
284
  Ok(value) => value,
205
- Err(e) => return Err(Box::new((StatusCode::BAD_REQUEST, format!("invalid JSON body: {e}")).into_response())),
285
+ Err(e) => {
286
+ return Err(Box::new(
287
+ (StatusCode::BAD_REQUEST, format!("invalid JSON body: {e}")).into_response(),
288
+ ))
289
+ }
206
290
  }
207
291
  };
208
292
 
@@ -213,14 +297,22 @@ fn admit(limits: &RateLimits, peer: SocketAddr, method: &Method, uri: &Uri, head
213
297
  // they're honored only from a peer on this host (the task's own sidecar); everyone
214
298
  // else — the load balancer, i.e. the public internet — gets an ordinary request
215
299
  // envelope for the path it actually hit.
216
- let envelope = if trusts_internal_dispatch(peer) && is_internal_dispatch_shape(&parsed) {
300
+ // A proxy on this host (an AwsBox's Caddy, which a tunnel also goes through) makes every
301
+ // public request arrive from loopback too; the forwarding headers it always adds are what
302
+ // tell its traffic apart from a sidecar's.
303
+ let envelope = if trusts_internal_dispatch(peer)
304
+ && !came_through_a_proxy(headers)
305
+ && is_internal_dispatch_shape(&parsed)
306
+ {
217
307
  parsed
218
308
  } else {
219
309
  synthesize_function_url_envelope(method, uri, headers, body)
220
310
  };
221
311
 
222
312
  if let Some((route_method, route_path)) = envelope_route(&envelope) {
223
- if let Verdict::Limited { retry_after_secs } = limits.check(route_method, route_path, headers, Some(peer.ip())) {
313
+ if let Verdict::Limited { retry_after_secs } =
314
+ limits.check(route_method, route_path, headers, Some(peer.ip()))
315
+ {
224
316
  return Err(Box::new(rate_limit::too_many_requests(retry_after_secs)));
225
317
  }
226
318
  }
@@ -230,7 +322,11 @@ fn admit(limits: &RateLimits, peer: SocketAddr, method: &Method, uri: &Uri, head
230
322
  /// The method and path a Function-URL-shaped envelope names, or `None` for
231
323
  /// the verb and read shapes, which carry no route.
232
324
  fn envelope_route(envelope: &Value) -> Option<(&str, &str)> {
233
- let method = envelope.get("requestContext")?.get("http")?.get("method")?.as_str()?;
325
+ let method = envelope
326
+ .get("requestContext")?
327
+ .get("http")?
328
+ .get("method")?
329
+ .as_str()?;
234
330
  let path = envelope.get("rawPath")?.as_str()?;
235
331
  Some((method, path))
236
332
  }
@@ -240,10 +336,29 @@ fn envelope_route(envelope: &Value) -> Option<(&str, &str)> {
240
336
  fn trusts_internal_dispatch(peer: SocketAddr) -> bool {
241
337
  match peer.ip() {
242
338
  std::net::IpAddr::V4(addr) => addr.is_loopback(),
243
- std::net::IpAddr::V6(addr) => addr.is_loopback() || addr.to_ipv4_mapped().is_some_and(|mapped| mapped.is_loopback()),
339
+ std::net::IpAddr::V6(addr) => {
340
+ addr.is_loopback()
341
+ || addr
342
+ .to_ipv4_mapped()
343
+ .is_some_and(|mapped| mapped.is_loopback())
344
+ }
244
345
  }
245
346
  }
246
347
 
348
+ /// Whether a proxy forwarded this request: Caddy's `reverse_proxy` always sets the
349
+ /// `X-Forwarded-*` headers, and a client cannot make it drop them.
350
+ fn came_through_a_proxy(headers: &HeaderMap) -> bool {
351
+ [
352
+ "x-forwarded-for",
353
+ "x-forwarded-host",
354
+ "x-forwarded-proto",
355
+ "forwarded",
356
+ "via",
357
+ ]
358
+ .iter()
359
+ .any(|name| headers.contains_key(*name))
360
+ }
361
+
247
362
  /// Whether `value` already matches one of `dispatch_body`'s recognized shapes; anything
248
363
  /// else (including a bare `{}`) is real REST traffic needing `synthesize_function_url_envelope`.
249
364
  fn is_internal_dispatch_shape(value: &Value) -> bool {
@@ -255,11 +370,19 @@ fn is_internal_dispatch_shape(value: &Value) -> bool {
255
370
 
256
371
  /// Rebuilds the envelope shape a real Lambda Function URL invocation already produces
257
372
  /// automatically, from the raw axum request parts.
258
- fn synthesize_function_url_envelope(method: &Method, uri: &Uri, headers: &HeaderMap, body: &Bytes) -> Value {
373
+ fn synthesize_function_url_envelope(
374
+ method: &Method,
375
+ uri: &Uri,
376
+ headers: &HeaderMap,
377
+ body: &Bytes,
378
+ ) -> Value {
259
379
  let mut header_map = serde_json::Map::new();
260
- for (name, value) in headers.iter() {
380
+ for (name, value) in headers {
261
381
  if let Ok(value) = value.to_str() {
262
- header_map.insert(name.as_str().to_ascii_lowercase(), Value::String(value.to_string()));
382
+ header_map.insert(
383
+ name.as_str().to_ascii_lowercase(),
384
+ Value::String(value.to_string()),
385
+ );
263
386
  }
264
387
  }
265
388
 
@@ -276,17 +399,25 @@ fn synthesize_function_url_envelope(method: &Method, uri: &Uri, headers: &Header
276
399
  /// Translates `dispatch_body`'s JSON result into a real HTTP response: `web::render`'s
277
400
  /// Function-URL envelope when `statusCode` is present, else the raw outcome as `200` JSON.
278
401
  fn value_to_response(value: Value) -> Response {
279
- let Some(status_code) = value.get("statusCode").and_then(|v| v.as_u64()) else {
402
+ let Some(status_code) = value.get("statusCode").and_then(serde_json::Value::as_u64) else {
280
403
  return (StatusCode::OK, axum::Json(value)).into_response();
281
404
  };
282
405
  // `try_from`, not `as u16`: 65736 would wrap to 200 and turn a handler's garbage into a success.
283
- let status = u16::try_from(status_code).ok().and_then(|code| StatusCode::from_u16(code).ok()).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
406
+ let status = u16::try_from(status_code)
407
+ .ok()
408
+ .and_then(|code| StatusCode::from_u16(code).ok())
409
+ .unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
284
410
 
285
411
  let mut headers = HeaderMap::new();
286
412
  if let Some(header_obj) = value.get("headers").and_then(|h| h.as_object()) {
287
413
  for (name, header_value) in header_obj {
288
- let Some(header_value) = header_value.as_str() else { continue };
289
- if let (Ok(name), Ok(header_value)) = (HeaderName::from_bytes(name.as_bytes()), HeaderValue::from_str(header_value)) {
414
+ let Some(header_value) = header_value.as_str() else {
415
+ continue;
416
+ };
417
+ if let (Ok(name), Ok(header_value)) = (
418
+ HeaderName::from_bytes(name.as_bytes()),
419
+ HeaderValue::from_str(header_value),
420
+ ) {
290
421
  headers.insert(name, header_value);
291
422
  }
292
423
  }
@@ -304,7 +435,11 @@ fn value_to_response(value: Value) -> Response {
304
435
  }
305
436
 
306
437
  let raw_body = value.get("body").and_then(|v| v.as_str()).unwrap_or("");
307
- let body_bytes = if value.get("isBase64Encoded").and_then(|v| v.as_bool()) == Some(true) {
438
+ let body_bytes = if value
439
+ .get("isBase64Encoded")
440
+ .and_then(serde_json::Value::as_bool)
441
+ == Some(true)
442
+ {
308
443
  web::base64_decode(raw_body)
309
444
  } else {
310
445
  raw_body.as_bytes().to_vec()
@@ -319,29 +454,39 @@ mod tests {
319
454
 
320
455
  #[test]
321
456
  fn a_function_url_shaped_body_is_recognized_as_internal_dispatch() {
322
- assert!(is_internal_dispatch_shape(&serde_json::json!({"requestContext": {"http": {"method": "GET"}}})));
457
+ assert!(is_internal_dispatch_shape(
458
+ &serde_json::json!({"requestContext": {"http": {"method": "GET"}}})
459
+ ));
323
460
  }
324
461
 
325
462
  #[test]
326
463
  fn a_bare_read_body_is_recognized_as_internal_dispatch() {
327
- assert!(is_internal_dispatch_shape(&serde_json::json!({"read": true})));
464
+ assert!(is_internal_dispatch_shape(
465
+ &serde_json::json!({"read": true})
466
+ ));
328
467
  }
329
468
 
330
469
  #[test]
331
470
  fn a_declared_query_body_is_recognized_as_internal_dispatch() {
332
471
  // `dispatch_body` answers `{"query": ...}`, and the generated editor's list and picture
333
472
  // pickers ask it; without this a loopback caller was sent to the login page.
334
- assert!(is_internal_dispatch_shape(&serde_json::json!({"query": "Pictures::MediaItem.Pictures", "args": {}})));
473
+ assert!(is_internal_dispatch_shape(
474
+ &serde_json::json!({"query": "Pictures::MediaItem.Pictures", "args": {}})
475
+ ));
335
476
  }
336
477
 
337
478
  #[test]
338
479
  fn a_verb_command_body_is_recognized_as_internal_dispatch() {
339
- assert!(is_internal_dispatch_shape(&serde_json::json!({"verb": "Register", "args": {}})));
480
+ assert!(is_internal_dispatch_shape(
481
+ &serde_json::json!({"verb": "Register", "args": {}})
482
+ ));
340
483
  }
341
484
 
342
485
  #[test]
343
486
  fn a_plain_rest_payload_is_not_internal_dispatch() {
344
- assert!(!is_internal_dispatch_shape(&serde_json::json!({"email": "a@example.com"})));
487
+ assert!(!is_internal_dispatch_shape(
488
+ &serde_json::json!({"email": "a@example.com"})
489
+ ));
345
490
  }
346
491
 
347
492
  #[test]
@@ -355,21 +500,43 @@ mod tests {
355
500
 
356
501
  #[test]
357
502
  fn only_a_peer_on_this_host_is_trusted_with_the_internal_protocol() {
358
- for trusted in ["127.0.0.1:5000", "127.4.5.6:5000", "[::1]:5000", "[::ffff:127.0.0.1]:5000"] {
359
- assert!(trusts_internal_dispatch(peer(trusted)), "{trusted} is this host");
503
+ for trusted in [
504
+ "127.0.0.1:5000",
505
+ "127.4.5.6:5000",
506
+ "[::1]:5000",
507
+ "[::ffff:127.0.0.1]:5000",
508
+ ] {
509
+ assert!(
510
+ trusts_internal_dispatch(peer(trusted)),
511
+ "{trusted} is this host"
512
+ );
360
513
  }
361
- for outside in ["10.0.0.5:5000", "172.31.4.9:5000", "203.0.113.7:5000", "[2001:db8::1]:5000", "[::ffff:10.0.0.5]:5000"] {
362
- assert!(!trusts_internal_dispatch(peer(outside)), "{outside} is not this host");
514
+ for outside in [
515
+ "10.0.0.5:5000",
516
+ "172.31.4.9:5000",
517
+ "203.0.113.7:5000",
518
+ "[2001:db8::1]:5000",
519
+ "[::ffff:10.0.0.5]:5000",
520
+ ] {
521
+ assert!(
522
+ !trusts_internal_dispatch(peer(outside)),
523
+ "{outside} is not this host"
524
+ );
363
525
  }
364
526
  }
365
527
 
366
528
  fn admitted(from: &str, path: &str, body: &str) -> Value {
367
529
  let uri: Uri = path.parse().unwrap();
368
530
  let body = Bytes::from(body.to_string());
369
- match admit(&limits_from(&[]), peer(from), &Method::POST, &uri, &HeaderMap::new(), &body) {
370
- Ok(envelope) => envelope,
371
- Err(_) => panic!("{from} POST {path} was refused before dispatch"),
372
- }
531
+ admit(
532
+ &limits_from(&[]),
533
+ peer(from),
534
+ &Method::POST,
535
+ &uri,
536
+ &HeaderMap::new(),
537
+ &body,
538
+ )
539
+ .unwrap_or_else(|_| panic!("{from} POST {path} was refused before dispatch"))
373
540
  }
374
541
 
375
542
  // The gap this closes: a public POST to any path the load balancer forwards carried
@@ -377,11 +544,17 @@ mod tests {
377
544
  // caller's own word for its role and no session check.
378
545
  #[test]
379
546
  fn a_verb_or_read_body_from_outside_this_host_is_an_ordinary_request_not_a_command() {
380
- for body in [r#"{"verb":"Approve","role":"admin"}"#, r#"{"read":true}"#, r#"{"query":"Pictures::MediaItem.Pictures","args":{}}"#] {
547
+ for body in [
548
+ r#"{"verb":"Approve","role":"admin"}"#,
549
+ r#"{"read":true}"#,
550
+ r#"{"query":"Pictures::MediaItem.Pictures","args":{}}"#,
551
+ ] {
381
552
  let envelope = admitted("10.0.0.5:5000", "/webhooks/x", body);
382
553
 
383
554
  assert!(
384
- envelope.get("verb").is_none() && envelope.get("read").is_none() && envelope.get("query").is_none(),
555
+ envelope.get("verb").is_none()
556
+ && envelope.get("read").is_none()
557
+ && envelope.get("query").is_none(),
385
558
  "{body} must not reach dispatch as a command"
386
559
  );
387
560
  assert_eq!(envelope["requestContext"]["http"]["method"], "POST");
@@ -397,18 +570,58 @@ mod tests {
397
570
 
398
571
  assert_eq!(envelope["rawPath"], "/webhooks/x");
399
572
  assert_eq!(envelope["requestContext"]["http"]["method"], "POST");
400
- assert!(envelope["headers"].get("cookie").is_none(), "headers come from the real request, not the body");
573
+ assert!(
574
+ envelope["headers"].get("cookie").is_none(),
575
+ "headers come from the real request, not the body"
576
+ );
401
577
  }
402
578
 
403
579
  #[test]
404
580
  fn a_verb_or_read_body_from_this_host_still_reaches_dispatch_unchanged() {
405
- for body in [r#"{"verb":"Register","to":"r1","with":{}}"#, r#"{"read":true}"#, r#"{"query":"Banking::Customer.Suspended","args":{}}"#] {
581
+ for body in [
582
+ r#"{"verb":"Register","to":"r1","with":{}}"#,
583
+ r#"{"read":true}"#,
584
+ r#"{"query":"Banking::Customer.Suspended","args":{}}"#,
585
+ ] {
406
586
  let envelope = admitted("127.0.0.1:5000", "/anything", body);
407
587
 
408
588
  assert_eq!(envelope, serde_json::from_str::<Value>(body).unwrap());
409
589
  }
410
590
  }
411
591
 
592
+ // The gap this closes: on an AwsBox, Caddy proxies the public internet to 127.0.0.1, so
593
+ // a public `{"read": true}` or `{"verb": ..., "role": "Admin"}` arrived from loopback and
594
+ // reached the kernel as the internal protocol.
595
+ #[test]
596
+ fn a_verb_or_read_body_a_proxy_on_this_host_forwarded_is_an_ordinary_request() {
597
+ for header in [
598
+ "x-forwarded-for",
599
+ "x-forwarded-host",
600
+ "x-forwarded-proto",
601
+ "forwarded",
602
+ "via",
603
+ ] {
604
+ let mut headers = HeaderMap::new();
605
+ headers.insert(header, "203.0.113.7".parse().unwrap());
606
+ let body = Bytes::from(r#"{"verb":"Approve","role":"Admin"}"#);
607
+ let envelope = admit(
608
+ &limits_from(&[]),
609
+ peer("127.0.0.1:5000"),
610
+ &Method::POST,
611
+ &"/anything".parse().unwrap(),
612
+ &headers,
613
+ &body,
614
+ )
615
+ .unwrap_or_else(|_| panic!("a request carrying {header} was refused"));
616
+
617
+ assert!(
618
+ envelope.get("verb").is_none(),
619
+ "a body forwarded with {header} must not reach dispatch as a command"
620
+ );
621
+ assert_eq!(envelope["rawPath"], "/anything");
622
+ }
623
+ }
624
+
412
625
  // Pins a real REST client's plain POST, with no Function-URL wrapping: it must
413
626
  // synthesize the same shape `web::render`/`checkout_route` read, not error on a
414
627
  // missing "verb".
@@ -417,8 +630,14 @@ mod tests {
417
630
  let method = Method::POST;
418
631
  let uri: Uri = "/registrations?utm_source=test".parse().unwrap();
419
632
  let mut headers = HeaderMap::new();
420
- headers.insert(HeaderName::from_static("stripe-signature"), HeaderValue::from_static("t=1,v1=abc"));
421
- headers.insert(HeaderName::from_static("content-type"), HeaderValue::from_static("application/json"));
633
+ headers.insert(
634
+ HeaderName::from_static("stripe-signature"),
635
+ HeaderValue::from_static("t=1,v1=abc"),
636
+ );
637
+ headers.insert(
638
+ HeaderName::from_static("content-type"),
639
+ HeaderValue::from_static("application/json"),
640
+ );
422
641
  let body = Bytes::from_static(br#"{"event_id":"ci-verification-test"}"#);
423
642
 
424
643
  let envelope = synthesize_function_url_envelope(&method, &uri, &headers, &body);
@@ -430,7 +649,10 @@ mod tests {
430
649
  assert_eq!(envelope["headers"]["content-type"], "application/json");
431
650
  assert_eq!(envelope["body"], r#"{"event_id":"ci-verification-test"}"#);
432
651
  assert_eq!(envelope["isBase64Encoded"], false);
433
- assert!(is_internal_dispatch_shape(&envelope), "the synthesized envelope must itself be recognized on a second pass");
652
+ assert!(
653
+ is_internal_dispatch_shape(&envelope),
654
+ "the synthesized envelope must itself be recognized on a second pass"
655
+ );
434
656
  }
435
657
 
436
658
  #[test]
@@ -453,7 +675,9 @@ mod tests {
453
675
  // Serves `version_router` on an ephemeral port, the way the host serves
454
676
  // it, so a request travels the real HTTP stack.
455
677
  async fn serve_version(body: Value) -> std::net::SocketAddr {
456
- let listener = tokio::net::TcpListener::bind(("127.0.0.1", 0)).await.unwrap();
678
+ let listener = tokio::net::TcpListener::bind(("127.0.0.1", 0))
679
+ .await
680
+ .unwrap();
457
681
  let addr = listener.local_addr().unwrap();
458
682
  tokio::spawn(async move { axum::serve(listener, version_router(body)).await });
459
683
  addr
@@ -462,31 +686,56 @@ mod tests {
462
686
  #[tokio::test]
463
687
  async fn version_answers_200_json_with_no_session() {
464
688
  let addr = serve_version(sample_version()).await;
465
- let response = reqwest::get(format!("http://{addr}/version")).await.unwrap();
689
+ let response = reqwest::get(format!("http://{addr}/version"))
690
+ .await
691
+ .unwrap();
466
692
  assert_eq!(response.status(), 200);
467
- let content_type = response.headers()["content-type"].to_str().unwrap().to_string();
468
- assert!(content_type.starts_with("application/json"), "got {content_type}");
693
+ let content_type = response.headers()["content-type"]
694
+ .to_str()
695
+ .unwrap()
696
+ .to_string();
697
+ assert!(
698
+ content_type.starts_with("application/json"),
699
+ "got {content_type}"
700
+ );
469
701
  let body: Value = response.json().await.unwrap();
470
- assert_eq!(body, serde_json::json!({ "era": "199b08", "ir_hash": "a".repeat(64), "build": "build-42" }));
702
+ assert_eq!(
703
+ body,
704
+ serde_json::json!({ "era": "199b08", "ir_hash": "a".repeat(64), "build": "build-42" })
705
+ );
471
706
  }
472
707
 
473
708
  #[tokio::test]
474
709
  async fn version_era_is_a_non_empty_string() {
475
710
  let addr = serve_version(sample_version()).await;
476
- let body: Value = reqwest::get(format!("http://{addr}/version")).await.unwrap().json().await.unwrap();
711
+ let body: Value = reqwest::get(format!("http://{addr}/version"))
712
+ .await
713
+ .unwrap()
714
+ .json()
715
+ .await
716
+ .unwrap();
477
717
  assert!(body["era"].as_str().is_some_and(|era| !era.is_empty()));
478
718
  }
479
719
 
480
720
  #[tokio::test]
481
721
  async fn version_is_get_only() {
482
722
  let addr = serve_version(sample_version()).await;
483
- let response = reqwest::Client::new().post(format!("http://{addr}/version")).send().await.unwrap();
723
+ let response = reqwest::Client::new()
724
+ .post(format!("http://{addr}/version"))
725
+ .send()
726
+ .await
727
+ .unwrap();
484
728
  assert_eq!(response.status(), 405);
485
729
  }
486
730
 
487
731
  fn limits_from(pairs: &[(&str, &str)]) -> Arc<RateLimits> {
488
- let map: std::collections::HashMap<String, String> = pairs.iter().map(|(k, v)| (k.to_string(), v.to_string())).collect();
489
- Arc::new(RateLimits::new(crate::rate_limit::Config::from_lookup(|name| map.get(name).cloned()).0))
732
+ let map: std::collections::HashMap<String, String> = pairs
733
+ .iter()
734
+ .map(|(k, v)| (k.to_string(), v.to_string()))
735
+ .collect();
736
+ Arc::new(RateLimits::new(
737
+ crate::rate_limit::Config::from_lookup(|name| map.get(name).cloned()).0,
738
+ ))
490
739
  }
491
740
 
492
741
  async fn gate_only(
@@ -506,15 +755,30 @@ mod tests {
506
755
  // A disposable server on an ephemeral port: the real `admit` gate reading the real connection
507
756
  // peer, with an acknowledgement standing in for dispatch, which needs Postgres and wasm.
508
757
  async fn gated_server(limits: Arc<RateLimits>) -> SocketAddr {
509
- let listener = tokio::net::TcpListener::bind(("127.0.0.1", 0)).await.unwrap();
758
+ let listener = tokio::net::TcpListener::bind(("127.0.0.1", 0))
759
+ .await
760
+ .unwrap();
510
761
  let addr = listener.local_addr().unwrap();
511
762
  let app = Router::new().fallback(any(gate_only)).with_state(limits);
512
- tokio::spawn(async move { axum::serve(listener, app.into_make_service_with_connect_info::<SocketAddr>()).await });
763
+ tokio::spawn(async move {
764
+ axum::serve(
765
+ listener,
766
+ app.into_make_service_with_connect_info::<SocketAddr>(),
767
+ )
768
+ .await
769
+ });
513
770
  addr
514
771
  }
515
772
 
516
- async fn post(addr: SocketAddr, path: &str, forwarded_for: Option<&str>, body: &str) -> reqwest::Response {
517
- let mut request = reqwest::Client::new().post(format!("http://{addr}{path}")).body(body.to_string());
773
+ async fn post(
774
+ addr: SocketAddr,
775
+ path: &str,
776
+ forwarded_for: Option<&str>,
777
+ body: &str,
778
+ ) -> reqwest::Response {
779
+ let mut request = reqwest::Client::new()
780
+ .post(format!("http://{addr}{path}"))
781
+ .body(body.to_string());
518
782
  if let Some(forwarded_for) = forwarded_for {
519
783
  request = request.header("x-forwarded-for", forwarded_for);
520
784
  }
@@ -527,37 +791,84 @@ mod tests {
527
791
  async fn a_burst_on_a_public_write_route_gets_429_with_retry_after_and_a_json_error() {
528
792
  let addr = gated_server(limits_from(&[("HECKS_RATE_LIMIT_SUBSCRIBE", "3")])).await;
529
793
  for attempt in 1..=3 {
530
- assert_eq!(post(addr, SUBSCRIBE, None, r#"{"email":"a@example.com"}"#).await.status(), 200, "request {attempt}");
794
+ assert_eq!(
795
+ post(addr, SUBSCRIBE, None, r#"{"email":"a@example.com"}"#)
796
+ .await
797
+ .status(),
798
+ 200,
799
+ "request {attempt}"
800
+ );
531
801
  }
532
802
 
533
803
  let refused = post(addr, SUBSCRIBE, None, r#"{"email":"a@example.com"}"#).await;
534
804
  assert_eq!(refused.status(), 429);
535
- let retry_after: u64 = refused.headers()["retry-after"].to_str().unwrap().parse().unwrap();
805
+ let retry_after: u64 = refused.headers()["retry-after"]
806
+ .to_str()
807
+ .unwrap()
808
+ .parse()
809
+ .unwrap();
536
810
  assert!((1..=3600).contains(&retry_after), "got {retry_after}");
537
- assert!(refused.headers()["content-type"].to_str().unwrap().starts_with("application/json"));
811
+ assert!(refused.headers()["content-type"]
812
+ .to_str()
813
+ .unwrap()
814
+ .starts_with("application/json"));
538
815
  let body: Value = refused.json().await.unwrap();
539
- assert_eq!(body, serde_json::json!({ "error": "too many requests, please try again later" }));
816
+ assert_eq!(
817
+ body,
818
+ serde_json::json!({ "error": "too many requests, please try again later" })
819
+ );
540
820
  }
541
821
 
542
822
  #[tokio::test]
543
823
  async fn registrations_are_limited_apart_from_subscribes() {
544
- let addr = gated_server(limits_from(&[("HECKS_RATE_LIMIT_SUBSCRIBE", "1"), ("HECKS_RATE_LIMIT_REGISTER", "2")])).await;
824
+ let addr = gated_server(limits_from(&[
825
+ ("HECKS_RATE_LIMIT_SUBSCRIBE", "1"),
826
+ ("HECKS_RATE_LIMIT_REGISTER", "2"),
827
+ ]))
828
+ .await;
545
829
  assert_eq!(post(addr, SUBSCRIBE, None, "{}").await.status(), 200);
546
830
  assert_eq!(post(addr, SUBSCRIBE, None, "{}").await.status(), 429);
547
831
 
548
- assert_eq!(post(addr, "/registrations", None, "{}").await.status(), 200, "subscribe being spent leaves register alone");
832
+ assert_eq!(
833
+ post(addr, "/registrations", None, "{}").await.status(),
834
+ 200,
835
+ "subscribe being spent leaves register alone"
836
+ );
549
837
  assert_eq!(post(addr, "/registrations", None, "{}").await.status(), 200);
550
838
  assert_eq!(post(addr, "/registrations", None, "{}").await.status(), 429);
551
839
  }
552
840
 
553
841
  #[tokio::test]
554
842
  async fn reads_and_other_routes_are_never_limited() {
555
- let addr = gated_server(limits_from(&[("HECKS_RATE_LIMIT_SUBSCRIBE", "1"), ("HECKS_RATE_LIMIT_REGISTER", "1")])).await;
843
+ let addr = gated_server(limits_from(&[
844
+ ("HECKS_RATE_LIMIT_SUBSCRIBE", "1"),
845
+ ("HECKS_RATE_LIMIT_REGISTER", "1"),
846
+ ]))
847
+ .await;
556
848
  let client = reqwest::Client::new();
557
849
  for _ in 0..5 {
558
- assert_eq!(client.get(format!("http://{addr}{SUBSCRIBE}")).send().await.unwrap().status(), 200);
559
- assert_eq!(client.get(format!("http://{addr}/registrations/REG-1")).send().await.unwrap().status(), 200);
560
- assert_eq!(post(addr, "/webhooks/stripe", None, "{}").await.status(), 200);
850
+ assert_eq!(
851
+ client
852
+ .get(format!("http://{addr}{SUBSCRIBE}"))
853
+ .send()
854
+ .await
855
+ .unwrap()
856
+ .status(),
857
+ 200
858
+ );
859
+ assert_eq!(
860
+ client
861
+ .get(format!("http://{addr}/registrations/REG-1"))
862
+ .send()
863
+ .await
864
+ .unwrap()
865
+ .status(),
866
+ 200
867
+ );
868
+ assert_eq!(
869
+ post(addr, "/webhooks/stripe", None, "{}").await.status(),
870
+ 200
871
+ );
561
872
  assert_eq!(post(addr, "/members", None, "{}").await.status(), 200);
562
873
  }
563
874
  }
@@ -567,18 +878,46 @@ mod tests {
567
878
  // The test client is the peer at 127.0.0.1 and nothing trusts it, so every header is
568
879
  // typed by the caller and the count stays with the connection.
569
880
  let addr = gated_server(limits_from(&[("HECKS_RATE_LIMIT_SUBSCRIBE", "1")])).await;
570
- assert_eq!(post(addr, SUBSCRIBE, Some("1.1.1.1"), "{}").await.status(), 200);
881
+ assert_eq!(
882
+ post(addr, SUBSCRIBE, Some("1.1.1.1"), "{}").await.status(),
883
+ 200
884
+ );
571
885
  for spoof in ["2.2.2.2", "3.3.3.3", "4.4.4.4, 5.5.5.5"] {
572
- assert_eq!(post(addr, SUBSCRIBE, Some(spoof), "{}").await.status(), 429, "{spoof}");
886
+ assert_eq!(
887
+ post(addr, SUBSCRIBE, Some(spoof), "{}").await.status(),
888
+ 429,
889
+ "{spoof}"
890
+ );
573
891
  }
574
892
  }
575
893
 
576
894
  #[tokio::test]
577
895
  async fn behind_a_trusted_proxy_each_visitor_has_their_own_allowance() {
578
- let addr = gated_server(limits_from(&[("HECKS_RATE_LIMIT_SUBSCRIBE", "1"), ("HECKS_TRUSTED_PROXIES", "127.0.0.1")])).await;
579
- assert_eq!(post(addr, SUBSCRIBE, Some("1.1.1.1, 203.0.113.7"), "{}").await.status(), 200);
580
- assert_eq!(post(addr, SUBSCRIBE, Some("2.2.2.2, 203.0.113.7"), "{}").await.status(), 429, "same visitor, new typed entry");
581
- assert_eq!(post(addr, SUBSCRIBE, Some("2.2.2.2, 198.51.100.4"), "{}").await.status(), 200, "a different visitor");
896
+ let addr = gated_server(limits_from(&[
897
+ ("HECKS_RATE_LIMIT_SUBSCRIBE", "1"),
898
+ ("HECKS_TRUSTED_PROXIES", "127.0.0.1"),
899
+ ]))
900
+ .await;
901
+ assert_eq!(
902
+ post(addr, SUBSCRIBE, Some("1.1.1.1, 203.0.113.7"), "{}")
903
+ .await
904
+ .status(),
905
+ 200
906
+ );
907
+ assert_eq!(
908
+ post(addr, SUBSCRIBE, Some("2.2.2.2, 203.0.113.7"), "{}")
909
+ .await
910
+ .status(),
911
+ 429,
912
+ "same visitor, new typed entry"
913
+ );
914
+ assert_eq!(
915
+ post(addr, SUBSCRIBE, Some("2.2.2.2, 198.51.100.4"), "{}")
916
+ .await
917
+ .status(),
918
+ 200,
919
+ "a different visitor"
920
+ );
582
921
  }
583
922
 
584
923
  #[tokio::test]
@@ -587,7 +926,11 @@ mod tests {
587
926
  let envelope = r#"{"requestContext":{"http":{"method":"POST"}},"rawPath":"/registrations","body":"{}"}"#;
588
927
  assert_eq!(post(addr, "/anything", None, envelope).await.status(), 200);
589
928
  assert_eq!(post(addr, "/anything", None, envelope).await.status(), 429);
590
- assert_eq!(post(addr, "/registrations", None, "{}").await.status(), 429, "the same budget as the real path");
929
+ assert_eq!(
930
+ post(addr, "/registrations", None, "{}").await.status(),
931
+ 429,
932
+ "the same budget as the real path"
933
+ );
591
934
  }
592
935
 
593
936
  #[tokio::test]
@@ -601,7 +944,11 @@ mod tests {
601
944
 
602
945
  #[tokio::test]
603
946
  async fn turning_the_limits_off_lets_every_request_through() {
604
- let addr = gated_server(limits_from(&[("HECKS_RATE_LIMIT", "off"), ("HECKS_RATE_LIMIT_SUBSCRIBE", "1")])).await;
947
+ let addr = gated_server(limits_from(&[
948
+ ("HECKS_RATE_LIMIT", "off"),
949
+ ("HECKS_RATE_LIMIT_SUBSCRIBE", "1"),
950
+ ]))
951
+ .await;
605
952
  for _ in 0..10 {
606
953
  assert_eq!(post(addr, SUBSCRIBE, None, "{}").await.status(), 200);
607
954
  }
@@ -611,16 +958,28 @@ mod tests {
611
958
  // checkout fixture's wasm, so the first request is a real compile.
612
959
  async fn fixture_host(database: &str, copy_name: &str) -> (SocketAddr, Arc<PathBuf>) {
613
960
  let client = crate::dispatch::tests::scratch_db(database).await;
614
- crate::dispatch::tests::provision_lineage(&*client.lock().await, "CheckoutFixture", 1, &["Event"]).await;
961
+ crate::dispatch::tests::provision_lineage(
962
+ &*client.lock().await,
963
+ "CheckoutFixture",
964
+ 1,
965
+ &["Event"],
966
+ )
967
+ .await;
615
968
  let wasm_path = Arc::new(crate::wasm_runner::tests::cold_copy_of_fixture(copy_name));
616
969
  let state = ServerState {
617
970
  client: Arc::new(client),
618
971
  wasm_path: Arc::clone(&wasm_path),
619
- lineage_config: Arc::new(LineageConfig { domain: "CheckoutFixture".to_string(), era: Some(1), mirrored: None }),
972
+ lineage_config: Arc::new(LineageConfig {
973
+ domain: "CheckoutFixture".to_string(),
974
+ era: Some(1),
975
+ mirrored: None,
976
+ }),
620
977
  invoker: Arc::new(crate::lambda_client::NeverInvoker),
621
978
  limits: limits_from(&[]),
622
979
  };
623
- let listener = tokio::net::TcpListener::bind(("127.0.0.1", 0)).await.unwrap();
980
+ let listener = tokio::net::TcpListener::bind(("127.0.0.1", 0))
981
+ .await
982
+ .unwrap();
624
983
  let addr = listener.local_addr().unwrap();
625
984
  tokio::spawn(async move { serve_on(listener, state, sample_version()).await });
626
985
  (addr, wasm_path)
@@ -631,7 +990,12 @@ mod tests {
631
990
  let requests: Vec<_> = (0..burst)
632
991
  .map(|_| {
633
992
  tokio::spawn(async move {
634
- let response = reqwest::Client::new().post(format!("http://{addr}/dispatch")).body(r#"{"read": true}"#).send().await.unwrap();
993
+ let response = reqwest::Client::new()
994
+ .post(format!("http://{addr}/dispatch"))
995
+ .body(r#"{"read": true}"#)
996
+ .send()
997
+ .await
998
+ .unwrap();
635
999
  let status = StatusCode::from_u16(response.status().as_u16()).unwrap();
636
1000
  (status, response.json::<Value>().await.unwrap())
637
1001
  })
@@ -639,7 +1003,12 @@ mod tests {
639
1003
  .collect();
640
1004
  let mut answers = Vec::new();
641
1005
  for request in requests {
642
- answers.push(tokio::time::timeout(std::time::Duration::from_secs(120), request).await.expect("a read was never answered").unwrap());
1006
+ answers.push(
1007
+ tokio::time::timeout(std::time::Duration::from_secs(120), request)
1008
+ .await
1009
+ .expect("a read was never answered")
1010
+ .unwrap(),
1011
+ );
643
1012
  }
644
1013
  answers
645
1014
  }
@@ -665,7 +1034,11 @@ mod tests {
665
1034
  let answers = read_burst(addr, 1).await;
666
1035
  assert_eq!(answers[0].0, StatusCode::OK, "{}", answers[0].1);
667
1036
  assert_eq!(crate::wasm_runner::compile_count(&wasm_path), 1);
668
- assert!(started.elapsed() < std::time::Duration::from_secs(1), "a warmed read took {:?}", started.elapsed());
1037
+ assert!(
1038
+ started.elapsed() < std::time::Duration::from_secs(1),
1039
+ "a warmed read took {:?}",
1040
+ started.elapsed()
1041
+ );
669
1042
  }
670
1043
 
671
1044
  #[test]
@@ -675,8 +1048,14 @@ mod tests {
675
1048
 
676
1049
  #[test]
677
1050
  fn build_falls_back_to_the_crate_version_when_unset_or_empty() {
678
- assert_eq!(version_body("e", "h", None)["build"], env!("CARGO_PKG_VERSION"));
679
- assert_eq!(version_body("e", "h", Some(""))["build"], env!("CARGO_PKG_VERSION"));
1051
+ assert_eq!(
1052
+ version_body("e", "h", None)["build"],
1053
+ env!("CARGO_PKG_VERSION")
1054
+ );
1055
+ assert_eq!(
1056
+ version_body("e", "h", Some(""))["build"],
1057
+ env!("CARGO_PKG_VERSION")
1058
+ );
680
1059
  }
681
1060
  }
682
1061