hecks 3.14.0 → 5.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 (427) 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/exe/hecks +1 -1
  9. data/lib/hecks/adapters/driven/prism.rb +0 -2
  10. data/lib/hecks/adapters/driven.rb +0 -2
  11. data/lib/hecks/adapters/driving/json.rb +9 -16
  12. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +2 -2
  13. data/lib/hecks/bluebook/dsl/derivation_words.rb +58 -0
  14. data/lib/hecks/bluebook/dsl/translation_builder.rb +6 -37
  15. data/lib/hecks/bluebook/expression/ast_json.rb +0 -9
  16. data/lib/hecks/bluebook/hecksagon.rb +1 -1
  17. data/lib/hecks/bluebook/meta_validator/translation_judge/rules.rb +7 -10
  18. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +4 -8
  19. data/lib/hecks/bluebook/meta_validator.rb +0 -11
  20. data/lib/hecks/bluebook/translation.rb +18 -10
  21. data/lib/hecks/bluebook/translation_expression/evaluator.rb +63 -0
  22. data/lib/hecks/bluebook/translation_expression/parser.rb +127 -0
  23. data/lib/hecks/bluebook/translation_expression.rb +39 -0
  24. data/lib/hecks/bluebook/translation_plan.rb +70 -0
  25. data/lib/hecks/bluebook.rb +2 -0
  26. data/lib/hecks/chapters.rb +3 -3
  27. data/lib/hecks/cli/project_cli/launcher_source.rb +1 -1
  28. data/lib/hecks/composer/adapters/claude_composer.adapter +3 -0
  29. data/lib/hecks/composer/adapters/claude_composer.rb +83 -0
  30. data/lib/hecks/composer/adapters/fake_agent.adapter +3 -0
  31. data/lib/hecks/composer/adapters/fake_agent.rb +55 -0
  32. data/lib/hecks/composer/adapters/jev.adapter +3 -0
  33. data/lib/hecks/composer/adapters/jev.rb +66 -0
  34. data/lib/hecks/composer/agent.rb +113 -0
  35. data/lib/hecks/composer/assistant.rb +110 -0
  36. data/lib/hecks/composer/behavior_run.rb +53 -0
  37. data/lib/hecks/composer/bluebook/composer.bluebook +178 -0
  38. data/lib/hecks/composer/bluebook/composer.hecksagon +16 -0
  39. data/lib/hecks/composer/board.rb +65 -0
  40. data/lib/hecks/composer/board_edit/command_text.rb +39 -0
  41. data/lib/hecks/composer/board_edit/source.rb +84 -0
  42. data/lib/hecks/composer/board_edit.rb +144 -0
  43. data/lib/hecks/composer/check.rb +89 -0
  44. data/lib/hecks/composer/domains.rb +81 -0
  45. data/lib/hecks/composer/jobs.rb +134 -0
  46. data/lib/hecks/composer/layout.rb +37 -0
  47. data/lib/hecks/composer/live.rb +150 -0
  48. data/lib/hecks/composer/outline.rb +48 -0
  49. data/lib/hecks/composer/page.html +989 -0
  50. data/lib/hecks/composer/panels.rb +142 -0
  51. data/lib/hecks/composer/parse.rb +83 -0
  52. data/lib/hecks/composer/plain.rb +28 -0
  53. data/lib/hecks/composer/ports/composer_agent.port +7 -0
  54. data/lib/hecks/composer/session.rb +141 -0
  55. data/lib/hecks/composer/toolbox.rb +166 -0
  56. data/lib/hecks/composer/web.rb +156 -0
  57. data/lib/hecks/composer/workbench.rb +51 -0
  58. data/lib/hecks/composer/workspace.rb +81 -0
  59. data/lib/hecks/composer.rb +17 -0
  60. data/lib/hecks/corpus/adapters.rb +22 -0
  61. data/lib/hecks/corpus/rust.rb +1 -1
  62. data/lib/hecks/corpus/worlds.rb +19 -0
  63. data/lib/hecks/corpus.rb +5 -0
  64. data/lib/hecks/forms/app/commands.rb +2 -3
  65. data/lib/hecks/forms/app/queries.rb +2 -3
  66. data/lib/hecks/forms/app/records.rb +17 -5
  67. data/lib/hecks/forms/app/responses.rb +3 -1
  68. data/lib/hecks/forms/command_form_renderer.rb +42 -16
  69. data/lib/hecks/forms/field_controls.rb +2 -2
  70. data/lib/hecks/forms/field_renderer.rb +1 -1
  71. data/lib/hecks/forms/field_shape.rb +36 -17
  72. data/lib/hecks/forms/index_renderer.rb +2 -1
  73. data/lib/hecks/forms/page.rb +1 -1
  74. data/lib/hecks/forms/params.rb +0 -13
  75. data/lib/hecks/forms/query_form_renderer.rb +7 -6
  76. data/lib/hecks/forms/record_renderer.rb +11 -7
  77. data/lib/hecks/forms/rule_words.rb +100 -0
  78. data/lib/hecks/forms/value_object_shape.rb +2 -1
  79. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +0 -3
  80. data/lib/hecks/fuzzing/isolated_boot/worlds.rb +3 -6
  81. data/lib/hecks/fuzzing/replay/filters.rb +2 -6
  82. data/lib/hecks/gate/stages.yml +25 -0
  83. data/lib/hecks/grammar/evolve/seeds.rb +0 -6
  84. data/lib/hecks/grammar/evolve.rb +0 -3
  85. data/lib/hecks/grammar/translation.bluebook +1 -1
  86. data/lib/hecks/hecks/adapters/codebase/composing.rb +56 -0
  87. data/lib/hecks/hecks/adapters/codebase/source_tree.rb +3 -1
  88. data/lib/hecks/hecks/adapters/first_admin.rb +3 -3
  89. data/lib/hecks/hecks/adapters/journal_store/changes.rb +0 -26
  90. data/lib/hecks/hecks/adapters/journal_store/examination.rb +2 -23
  91. data/lib/hecks/hecks/adapters/journal_store/readings.rb +3 -11
  92. data/lib/hecks/hecks/adapters/journal_store.rb +0 -11
  93. data/lib/hecks/hecks/adapters/rehearsal/inquiry.rb +4 -11
  94. data/lib/hecks/hecks/adapters/rehearsal/memory_store.rb +0 -9
  95. data/lib/hecks/hecks/adapters/rehearsal/postgres_reading.rb +0 -4
  96. data/lib/hecks/hecks/codebase.behaviors +6 -0
  97. data/lib/hecks/hecks/codebase.bluebook +21 -0
  98. data/lib/hecks/hecks/composer.behaviors +68 -0
  99. data/lib/hecks/hecks/context_map.hecksagon +1 -1
  100. data/lib/hecks/hecks/custodian.behaviors +0 -6
  101. data/lib/hecks/hecks/custodian.bluebook +3 -80
  102. data/lib/hecks/hecks/hecks.hecksagon +2 -2
  103. data/lib/hecks/hecks/hecks.world +1 -1
  104. data/lib/hecks/hecks/rehearsal.bluebook +7 -13
  105. data/lib/hecks/kernel_capabilities/templates.rb +4 -4
  106. data/lib/hecks/kernel_capabilities.rb +1 -1
  107. data/lib/hecks/language/bluebook/vocabulary.bluebook +2 -3
  108. data/lib/hecks/language/translation/translation_aggregate.bluebook +21 -24
  109. data/lib/hecks/ports/access_control.rb +2 -2
  110. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +3 -41
  111. data/lib/hecks/ports/persistence/plugins/era/lineage/coverage.rb +7 -7
  112. data/lib/hecks/ports/persistence/plugins/era/lineage/rewrite.rb +25 -2
  113. data/lib/hecks/ports/persistence/plugins/era/lineage/rules.rb +4 -4
  114. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +17 -25
  115. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/base_sql.rb +0 -12
  116. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/domain_rename.rb +1 -1
  117. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +2 -22
  118. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +0 -1
  119. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +9 -3
  120. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +3 -29
  121. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/refusals.rb +2 -26
  122. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +6 -33
  123. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +3 -3
  124. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +2 -4
  125. data/lib/hecks/ports/persistence/plugins/era/translation/expression_sql.rb +52 -0
  126. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +23 -29
  127. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +1 -1
  128. data/lib/hecks/ports/persistence/plugins/era/translation.rb +0 -1
  129. data/lib/hecks/ports/persistence/plugins/era.rb +3 -6
  130. data/lib/hecks/ports.rb +0 -1
  131. data/lib/hecks/projection_files.rb +3 -1
  132. data/lib/hecks/projections/deploy/templates/lambda/nat_comment_oauth.tmpl +1 -1
  133. data/lib/hecks/projections/glossary/entries.rb +10 -4
  134. data/lib/hecks/projections/glossary/groups.rb +42 -0
  135. data/lib/hecks/projections/glossary/html/page.rb +7 -2
  136. data/lib/hecks/projections/glossary/html/parser.rb +1 -1
  137. data/lib/hecks/projections/glossary/html/sections.rb +31 -11
  138. data/lib/hecks/projections/glossary/html.rb +1 -2
  139. data/lib/hecks/projections/glossary/markdown.rb +31 -28
  140. data/lib/hecks/projections/glossary/mermaid.rb +3 -0
  141. data/lib/hecks/projections/glossary/page.css +143 -153
  142. data/lib/hecks/projections/glossary/page.js +63 -3
  143. data/lib/hecks/projections/glossary/sentences/actions.rb +88 -0
  144. data/lib/hecks/projections/glossary/sentences/paths.rb +77 -0
  145. data/lib/hecks/projections/glossary/sentences/queries.rb +90 -0
  146. data/lib/hecks/projections/glossary/sentences/value_objects.rb +32 -5
  147. data/lib/hecks/projections/glossary/sentences.rb +13 -3
  148. data/lib/hecks/projections/glossary/slugs.rb +4 -1
  149. data/lib/hecks/projections/glossary.rb +2 -1
  150. data/lib/hecks/projections/plain_english.rb +97 -0
  151. data/lib/hecks/projections/rust_vocabulary/arguments.rb +2 -2
  152. data/lib/hecks/projections/rust_vocabulary/enum_body.rb +20 -25
  153. data/lib/hecks/projections/rust_vocabulary/tests.rb +2 -2
  154. data/lib/hecks/projections/rust_vocabulary.rb +14 -6
  155. data/lib/hecks/projections/site/cms_editor/color.rb +0 -6
  156. data/lib/hecks/projections/site/cms_editor/templates/src/host.ts.tmpl +7 -2
  157. data/lib/hecks/projections/statements/actions.rb +99 -0
  158. data/lib/hecks/projections/statements/relationships.rb +86 -0
  159. data/lib/hecks/projections/statements.rb +15 -24
  160. data/lib/hecks/projector/exporter/adapters.rb +112 -0
  161. data/lib/hecks/projector/exporter/translations.rb +18 -35
  162. data/lib/hecks/projector/exporter/worlds.rb +103 -0
  163. data/lib/hecks/projector/exporter.rb +5 -1
  164. data/lib/hecks/quality_control/adapters/agent.rb +6 -7
  165. data/lib/hecks/quality_control/adapters/agent_profile.rb +16 -114
  166. data/lib/hecks/quality_control/cli/qa_mine_combinations.rb +1 -1
  167. data/lib/hecks/runtime/loader/boot_steps.rb +0 -20
  168. data/lib/hecks/runtime/outbox.rb +2 -9
  169. data/lib/hecks/runtime/port_operation_interpreter.rb +1 -9
  170. data/lib/hecks/runtime/remote_dispatcher.rb +2 -2
  171. data/lib/hecks/runtime/value/unwrapping.rb +60 -0
  172. data/lib/hecks/runtime/value.rb +10 -21
  173. data/lib/hecks/rust_build/project_rust/target_ir.rb +23 -4
  174. data/lib/hecks/rust_build/wasm.rb +0 -12
  175. data/lib/hecks/rust_build/write_if_changed.rb +12 -12
  176. data/lib/hecks/rust_format.rb +77 -0
  177. data/lib/hecks/tools/fuzz_sweep/findings.rb +3 -9
  178. data/lib/hecks/tools/fuzz_sweep.rb +1 -3
  179. data/lib/hecks/tools/translation_audit/verdicts.rb +3 -28
  180. data/lib/hecks/tools/translation_audit.rb +8 -10
  181. data/lib/hecks/tools/translation_scaffold/reporting.rb +1 -1
  182. data/lib/hecks/version.rb +1 -1
  183. data/lib/hecks/vocabulary.rb +2 -1
  184. data/lib/hecks.rb +3 -0
  185. data/prebuilt/{1a6f596be124228be93eba1d86dbc9d2933190fa389b284331241cb9f27115a0.marshal → ad68f9e06c8068c2a4176b57abf3083d591d5d8980c2b3e897b97331f965b643.marshal} +0 -0
  186. data/prebuilt/{a883a7e568667aa5ac904c82daff356bdf9e996252a36af6def4a4a43efb967e.marshal → ee22ec8642c4006717ba2b80f46e5efb81de459eef9dfec043e838c0ad4fd720.marshal} +0 -0
  187. data/prebuilt/{verdicts-872993b1aa26d73fe48f28251374b8585fed145d02aab42be0f00de386b6ff80.json → verdicts-b69f5dcbba79c20daa0493476fa7c317004a30aa2bc3d3dc6a722ea70ae2b43b.json} +1 -1
  188. data/rust/Cargo.toml +40 -0
  189. data/rust/build/Cargo.toml +36 -0
  190. data/rust/build/clippy.toml +4 -0
  191. data/rust/build/src/adapters_pass.rs +196 -0
  192. data/rust/build/src/binding_pass.rs +54 -13
  193. data/rust/build/src/build_artifact.rs +23 -5
  194. data/rust/build/src/cargo_sync.rs +69 -26
  195. data/rust/build/src/lineage_pass.rs +101 -87
  196. data/rust/build/src/main.rs +11 -2
  197. data/rust/build/src/pipeline.rs +80 -19
  198. data/rust/build/src/reserved_names.rs +6 -50
  199. data/rust/build/src/resolve.rs +82 -35
  200. data/rust/build/src/rustfmt.rs +100 -0
  201. data/rust/build/src/seams_pass.rs +85 -16
  202. data/rust/build/src/subprocess.rs +30 -6
  203. data/rust/build/src/tmp.rs +5 -2
  204. data/rust/build/src/translation_pass.rs +72 -22
  205. data/rust/build/src/world_pass.rs +109 -0
  206. data/rust/clippy.toml +4 -0
  207. data/rust/codegen/Cargo.toml +36 -0
  208. data/rust/codegen/clippy.toml +4 -0
  209. data/rust/codegen/src/asks.rs +40 -14
  210. data/rust/codegen/src/attr.rs +2 -2
  211. data/rust/codegen/src/bridging.rs +676 -117
  212. data/rust/codegen/src/commands.rs +979 -373
  213. data/rust/codegen/src/constraints.rs +88 -25
  214. data/rust/codegen/src/dependency_planning.rs +128 -34
  215. data/rust/codegen/src/domain_generator.rs +306 -170
  216. data/rust/codegen/src/exemplar.rs +87 -32
  217. data/rust/codegen/src/expr_emitter.rs +54 -6
  218. data/rust/codegen/src/fielded.rs +193 -41
  219. data/rust/codegen/src/hecks_naming.rs +124 -16
  220. data/rust/codegen/src/hecks_naming_tests.rs +81 -0
  221. data/rust/codegen/src/json.rs +80 -44
  222. data/rust/codegen/src/json_codec.rs +585 -149
  223. data/rust/codegen/src/literal.rs +104 -138
  224. data/rust/codegen/src/main.rs +151 -52
  225. data/rust/codegen/src/manifest.rs +76 -14
  226. data/rust/codegen/src/mutations.rs +1212 -239
  227. data/rust/codegen/src/naming.rs +111 -60
  228. data/rust/codegen/src/optional_pass.rs +90 -28
  229. data/rust/codegen/src/ports.rs +5 -4
  230. data/rust/codegen/src/prelude.rs +139 -49
  231. data/rust/codegen/src/presenters.rs +335 -0
  232. data/rust/codegen/src/queries.rs +601 -226
  233. data/rust/codegen/src/reactions.rs +268 -74
  234. data/rust/codegen/src/read_models.rs +574 -229
  235. data/rust/codegen/src/reference_specs.rs +16 -3
  236. data/rust/codegen/src/registry.rs +128 -79
  237. data/rust/codegen/src/reserved_names.rs +6 -50
  238. data/rust/codegen/src/shared.rs +71 -22
  239. data/rust/codegen/src/sidecars.rs +15 -5
  240. data/rust/codegen/src/skip_reason.rs +8 -2
  241. data/rust/codegen/src/types.rs +306 -108
  242. data/rust/host/Cargo.lock +1 -0
  243. data/rust/host/Cargo.toml +40 -4
  244. data/rust/host/HECKS_RELEASE +1 -1
  245. data/rust/host/clippy.toml +4 -0
  246. data/rust/host/src/api.rs +698 -218
  247. data/rust/host/src/auth.rs +491 -214
  248. data/rust/host/src/b64.rs +91 -0
  249. data/rust/host/src/bin/expr_harness.rs +6 -2
  250. data/rust/host/src/bin/lineage_harness.rs +47 -11
  251. data/rust/host/src/bin/mint_harness.rs +70 -37
  252. data/rust/host/src/bin/validate_harness.rs +16 -8
  253. data/rust/host/src/boot.rs +144 -68
  254. data/rust/host/src/boundary_fuzz/api.rs +130 -32
  255. data/rust/host/src/boundary_fuzz/auth.rs +108 -23
  256. data/rust/host/src/boundary_fuzz/expr_json.rs +85 -11
  257. data/rust/host/src/boundary_fuzz/ir.rs +164 -12
  258. data/rust/host/src/boundary_fuzz/server.rs +283 -36
  259. data/rust/host/src/boundary_fuzz/web.rs +56 -8
  260. data/rust/host/src/checkout.rs +78 -32
  261. data/rust/host/src/clock.rs +8 -0
  262. data/rust/host/src/commerce_ir.rs +333 -95
  263. data/rust/host/src/crypto.rs +33 -0
  264. data/rust/host/src/dispatch.rs +1037 -331
  265. data/rust/host/src/expr_json.rs +1021 -273
  266. data/rust/host/src/extension.rs +24 -4
  267. data/rust/host/src/field_hints.rs +11 -7
  268. data/rust/host/src/fuzz_support.rs +85 -13
  269. data/rust/host/src/host.rs +100 -0
  270. data/rust/host/src/http.rs +22 -0
  271. data/rust/host/src/ir.rs +326 -16
  272. data/rust/host/src/journal.rs +277 -176
  273. data/rust/host/src/lambda_client.rs +219 -53
  274. data/rust/host/src/lib.rs +16 -4
  275. data/rust/host/src/log.rs +62 -11
  276. data/rust/host/src/mint.rs +1153 -326
  277. data/rust/host/src/needs.rs +185 -44
  278. data/rust/host/src/payments/keystore.rs +197 -45
  279. data/rust/host/src/payments/roles.rs +39 -7
  280. data/rust/host/src/payments/tests.rs +1427 -282
  281. data/rust/host/src/payments.rs +391 -112
  282. data/rust/host/src/platform_signin/attack_tests.rs +5 -5
  283. data/rust/host/src/platform_signin/config.rs +11 -11
  284. data/rust/host/src/platform_signin/cookie.rs +2 -2
  285. data/rust/host/src/platform_signin/extension.rs +2 -1
  286. data/rust/host/src/platform_signin/handler.rs +9 -8
  287. data/rust/host/src/platform_signin/jti.rs +4 -2
  288. data/rust/host/src/platform_signin/jwks.rs +2 -1
  289. data/rust/host/src/platform_signin/jws.rs +2 -37
  290. data/rust/host/src/platform_signin/known_answer_tests.rs +5 -1
  291. data/rust/host/src/platform_signin/mode_tests.rs +5 -3
  292. data/rust/host/src/platform_signin/pages.rs +20 -20
  293. data/rust/host/src/platform_signin/session.rs +8 -1
  294. data/rust/host/src/platform_signin/settings.rs +6 -6
  295. data/rust/host/src/platform_signin/strict_json.rs +12 -9
  296. data/rust/host/src/platform_signin/verify.rs +41 -41
  297. data/rust/host/src/platform_signin/wiring_support.rs +13 -18
  298. data/rust/host/src/presentation.rs +207 -63
  299. data/rust/host/src/presentation_write.rs +502 -187
  300. data/rust/host/src/query_step.rs +69 -18
  301. data/rust/host/src/rate_limit.rs +541 -142
  302. data/rust/host/src/reference_transform.rs +77 -25
  303. data/rust/host/src/reference_validate.rs +443 -104
  304. data/rust/host/src/resend.rs +190 -55
  305. data/rust/host/src/secrets.rs +116 -21
  306. data/rust/host/src/server.rs +555 -104
  307. data/rust/host/src/storage_shape.rs +88 -21
  308. data/rust/host/src/test_pg.rs +17 -7
  309. data/rust/host/src/ui_schema.rs +495 -207
  310. data/rust/host/src/wasm_runner.rs +21 -6
  311. data/rust/host/src/web/commerce.rs +101 -23
  312. data/rust/host/src/web/newsletter/cooldown.rs +10 -3
  313. data/rust/host/src/web/newsletter/template.rs +26 -7
  314. data/rust/host/src/web/newsletter/tests.rs +112 -23
  315. data/rust/host/src/web/newsletter.rs +209 -59
  316. data/rust/host/src/web/newsletter_send.rs +251 -79
  317. data/rust/host/src/web/registration_receipt/tests.rs +16 -4
  318. data/rust/host/src/web/registration_receipt.rs +100 -24
  319. data/rust/host/src/web/registrations.rs +1384 -332
  320. data/rust/host/src/web/resend_webhook.rs +138 -80
  321. data/rust/host/src/web.rs +2416 -508
  322. data/rust/lsp/Cargo.toml +36 -0
  323. data/rust/lsp/clippy.toml +4 -0
  324. data/rust/lsp/src/json.rs +29 -22
  325. data/rust/lsp/src/main.rs +81 -31
  326. data/rust/lsp/src/outline.rs +23 -7
  327. data/rust/lsp/src/rpc.rs +17 -4
  328. data/rust/parser/Cargo.toml +36 -0
  329. data/rust/parser/clippy.toml +4 -0
  330. data/rust/parser/src/build/closed_sets.rs +6 -1
  331. data/rust/parser/src/build/identity.rs +4 -3
  332. data/rust/parser/src/build/mod.rs +1 -1
  333. data/rust/parser/src/build/named_pattern.rs +4 -1
  334. data/rust/parser/src/build/naming.rs +2 -80
  335. data/rust/parser/src/build/pattern_subset.rs +6 -8
  336. data/rust/parser/src/build/query_derive.rs +3 -1
  337. data/rust/parser/src/build/query_inference.rs +14 -5
  338. data/rust/parser/src/build/read_model.rs +4 -1
  339. data/rust/parser/src/build/references.rs +4 -3
  340. data/rust/parser/src/canonical.rs +34 -15
  341. data/rust/parser/src/emit.rs +187 -105
  342. data/rust/parser/src/expr/ast_json.rs +134 -42
  343. data/rust/parser/src/expr/evaluator.rs +27 -9
  344. data/rust/parser/src/expr/mod.rs +54 -13
  345. data/rust/parser/src/expr/resolver.rs +299 -92
  346. data/rust/parser/src/ir.rs +35 -3
  347. data/rust/parser/src/keywords.rs +7168 -516
  348. data/rust/parser/src/lex.rs +11 -25
  349. data/rust/parser/src/lib.rs +1 -0
  350. data/rust/parser/src/main.rs +65 -6
  351. data/rust/parser/src/parse/adapter.rs +157 -0
  352. data/rust/parser/src/parse/aggregate.rs +19 -10
  353. data/rust/parser/src/parse/chapter.rs +16 -17
  354. data/rust/parser/src/parse/command.rs +35 -16
  355. data/rust/parser/src/parse/domain_port.rs +4 -7
  356. data/rust/parser/src/parse/entity.rs +21 -11
  357. data/rust/parser/src/parse/hecksagon.rs +10 -4
  358. data/rust/parser/src/parse/lifecycle.rs +9 -5
  359. data/rust/parser/src/parse/mod.rs +22 -91
  360. data/rust/parser/src/parse/needs.rs +21 -12
  361. data/rust/parser/src/parse/policy.rs +2 -2
  362. data/rust/parser/src/parse/process_manager.rs +9 -11
  363. data/rust/parser/src/parse/query.rs +8 -4
  364. data/rust/parser/src/parse/read_model.rs +57 -16
  365. data/rust/parser/src/parse/value_object.rs +7 -2
  366. data/rust/parser/src/parse/world.rs +322 -0
  367. data/rust/parser/src/ruby_value.rs +7 -6
  368. data/rust/src/exemplar/json.rs +4 -4
  369. data/rust/src/exemplar/mutations.rs +18 -0
  370. data/rust/src/exemplar/read_models.rs +1 -10
  371. data/rust/src/kernel/attribute_shapes/composite.rs +54 -14
  372. data/rust/src/kernel/attribute_shapes/list.rs +2 -2
  373. data/rust/src/kernel/attribute_shapes/mod.rs +8 -8
  374. data/rust/src/kernel/attribute_shapes/scalar.rs +2 -2
  375. data/rust/src/kernel/cli.rs +244 -114
  376. data/rust/src/kernel/dispatch.rs +323 -115
  377. data/rust/src/kernel/dispatch_guard_tests.rs +327 -65
  378. data/rust/src/kernel/expr.rs +203 -86
  379. data/rust/src/kernel/expression_operators/arithmetic.rs +138 -25
  380. data/rust/src/kernel/expression_operators/comparison.rs +87 -2
  381. data/rust/src/kernel/expression_operators/enumeration.rs +106 -22
  382. data/rust/src/kernel/expression_operators/logical.rs +9 -3
  383. data/rust/src/kernel/expression_operators/membership.rs +3 -1
  384. data/rust/src/kernel/expression_operators/mod.rs +34 -21
  385. data/rust/src/kernel/expression_operators/pattern_match.rs +46 -11
  386. data/rust/src/kernel/expression_operators/positional.rs +84 -30
  387. data/rust/src/kernel/expression_operators/presence.rs +52 -13
  388. data/rust/src/kernel/expression_operators/sign_test.rs +3 -1
  389. data/rust/src/kernel/expression_operators/sized.rs +24 -8
  390. data/rust/src/kernel/expression_operators/text.rs +120 -27
  391. data/rust/src/kernel/expression_operators/to_string.rs +10 -4
  392. data/rust/src/kernel/json.rs +244 -148
  393. data/rust/src/kernel/mod.rs +47 -33
  394. data/rust/src/kernel/named_query.rs +63 -15
  395. data/rust/src/kernel/needs.rs +112 -25
  396. data/rust/src/kernel/orchestrate.rs +980 -581
  397. data/rust/src/kernel/orchestrate_guard_tests.rs +538 -140
  398. data/rust/src/kernel/pattern.rs +107 -34
  399. data/rust/src/kernel/present.rs +261 -0
  400. data/rust/src/kernel/query_comparators.rs +118 -41
  401. data/rust/src/kernel/query_ordering.rs +13 -5
  402. data/rust/src/kernel/read_model.rs +479 -181
  403. data/rust/src/kernel/reference_lookup.rs +122 -26
  404. data/rust/src/kernel/repository.rs +293 -66
  405. data/rust/src/kernel/routing.rs +58 -42
  406. data/rust/src/kernel/ruby_float.rs +139 -0
  407. data/rust/src/kernel/vocab/aggregate_dispatch_order.rs +57 -57
  408. data/rust/src/kernel/vocab/entity_dispatch_order.rs +54 -54
  409. data/rust/src/kernel/vocab/field_hint.rs +19 -25
  410. data/rust/src/kernel/vocab/mod.rs +6 -6
  411. data/rust/src/kernel/vocab/query_comparator.rs +23 -24
  412. data/rust/src/kernel/vocab/refusal_template.rs +501 -297
  413. data/rust/src/lib.rs +50 -0
  414. data/rust/src/main.rs +4 -1
  415. data/rust/web/Cargo.toml +36 -0
  416. data/rust/web/clippy.toml +4 -0
  417. metadata +85 -15
  418. data/examples/pizzas/bluebook/hecksagon/mock_stripe_payment_adapter.rb +0 -36
  419. data/lib/hecks/adapters/driven/google_authentication.adapter +0 -3
  420. data/lib/hecks/adapters/driven/google_authentication.rb +0 -99
  421. data/lib/hecks/adapters/driven/mock_stripe_adapter.adapter +0 -3
  422. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +0 -32
  423. data/lib/hecks/ports/authentication.port +0 -6
  424. data/lib/hecks/ports/authentication.rb +0 -67
  425. data/lib/hecks/ports/persistence/plugins/era/translation/approval_file.rb +0 -255
  426. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +0 -22
  427. data/rust/host/src/approval.rs +0 -652
@@ -4,9 +4,9 @@
4
4
  use crate::auth;
5
5
  use crate::checkout::STRIPE_API_VERSION;
6
6
  use crate::dispatch;
7
+ use crate::host::Host;
7
8
  use crate::journal::LineageConfig;
8
- use crate::lambda_client::LambdaInvoker;
9
- use crate::web::{instances_for, last_refusal, respond};
9
+ use crate::web::{instances_for, json_error, last_refusal, respond, site_url};
10
10
  use serde_json::{json, Value};
11
11
  use std::collections::HashMap;
12
12
  use std::path::Path;
@@ -81,24 +81,33 @@ pub fn check_boot(checkout_enabled: bool) -> Result<(), String> {
81
81
  /// The webhook endpoint's description: the configured text when it is not
82
82
  /// blank, else "<domain> website".
83
83
  fn webhook_description(configured: Option<&str>, domain: &str) -> String {
84
- match configured.map(str::trim).filter(|text| !text.is_empty()) {
85
- Some(text) => text.to_string(),
86
- None => format!("{domain} website"),
87
- }
84
+ configured
85
+ .map(str::trim)
86
+ .filter(|text| !text.is_empty())
87
+ .map_or_else(|| format!("{domain} website"), ToString::to_string)
88
88
  }
89
89
 
90
90
  impl PlatformConfig {
91
91
  pub fn from_env() -> Self {
92
92
  let var = |name: &str| std::env::var(name).unwrap_or_default();
93
- let site_url = std::env::var("SITE_URL").unwrap_or_else(|_| "http://localhost:4321".to_string());
94
- let webhook_base_url = Some(var("PAYMENTS_WEBHOOK_BASE_URL")).filter(|url| !url.trim().is_empty()).unwrap_or_else(|| site_url.clone()).trim_end_matches('/').to_string();
93
+ let site_url = site_url();
94
+ let webhook_base_url = Some(var("PAYMENTS_WEBHOOK_BASE_URL"))
95
+ .filter(|url| !url.trim().is_empty())
96
+ .unwrap_or_else(|| site_url.clone())
97
+ .trim_end_matches('/')
98
+ .to_string();
95
99
  Self {
96
100
  site_url,
97
101
  webhook_base_url,
98
102
  store: keystore::default_store(),
99
103
  stored: Arc::new(StoredDocument::default()),
100
104
  api_base: STRIPE_API_BASE.to_string(),
101
- webhook_description: webhook_description(std::env::var("PAYMENTS_WEBHOOK_DESCRIPTION").ok().as_deref(), &var("HECKS_DOMAIN")),
105
+ webhook_description: webhook_description(
106
+ std::env::var("PAYMENTS_WEBHOOK_DESCRIPTION")
107
+ .ok()
108
+ .as_deref(),
109
+ &var("HECKS_DOMAIN"),
110
+ ),
102
111
  direct_test_key: var("STRIPE_ACCOUNT_TEST_KEY"),
103
112
  direct_live_key: var("STRIPE_ACCOUNT_LIVE_KEY"),
104
113
  direct_test_publishable_key: var("STRIPE_ACCOUNT_TEST_PUBLISHABLE_KEY"),
@@ -123,43 +132,61 @@ impl PlatformConfig {
123
132
  /// Reads the key store into `stored` (from its cache when fresh); a store that
124
133
  /// cannot be read leaves only environment keys, logged without naming any key.
125
134
  pub async fn apply_store(&mut self) {
126
- let Some(store) = self.store.clone() else { return };
135
+ let Some(store) = self.store.clone() else {
136
+ return;
137
+ };
127
138
  match store.document().await {
128
139
  Ok(document) => self.stored = document,
129
- Err(e) => eprintln!("the saved payment keys could not be read, so only environment keys are used: {e}"),
140
+ Err(e) => eprintln!(
141
+ "the saved payment keys could not be read, so only environment keys are used: {e}"
142
+ ),
130
143
  }
131
144
  }
132
145
 
133
146
  /// Whether this deploy can save keys from the Payments page.
134
- pub fn can_save_keys(&self) -> bool {
147
+ pub const fn can_save_keys(&self) -> bool {
135
148
  self.store.is_some()
136
149
  }
137
150
 
138
151
  /// Whether any real Stripe credential exists (environment or saved keys). While
139
152
  /// one does, the public mock webhook secret must never verify anything.
140
153
  pub fn has_real_credentials(&self) -> bool {
141
- let configured = [&self.direct_test_key, &self.direct_live_key].iter().any(|key| !key.is_empty());
142
- let saved = [&self.stored.test, &self.stored.live].iter().any(|keys| keys.as_ref().is_some_and(|keys| !keys.secret_key.is_empty()));
154
+ let configured = [&self.direct_test_key, &self.direct_live_key]
155
+ .iter()
156
+ .any(|key| !key.is_empty());
157
+ let saved = [&self.stored.test, &self.stored.live].iter().any(|keys| {
158
+ keys.as_ref()
159
+ .is_some_and(|keys| !keys.secret_key.is_empty())
160
+ });
143
161
  configured || saved
144
162
  }
145
163
 
146
164
  /// Every webhook signing secret this deploy accepts: the environment's, then
147
165
  /// any saved with the business's keys.
148
166
  pub fn webhook_secrets(&self) -> Vec<&str> {
149
- self.webhook_secret.iter().map(String::as_str).chain(self.stored.webhook_secrets()).collect()
167
+ self.webhook_secret
168
+ .iter()
169
+ .map(String::as_str)
170
+ .chain(self.stored.webhook_secrets())
171
+ .collect()
150
172
  }
151
173
 
152
174
  // The business's own keys for `mode`: the environment's pair when both are
153
175
  // set, else the pair saved from the Payments page, else whatever the
154
176
  // environment has (which then leaves the mode unavailable).
155
177
  fn direct_pair(&self, mode: &str) -> (&str, &str) {
156
- let (key, publishable) =
157
- if mode == "live" { (&self.direct_live_key, &self.direct_live_publishable_key) } else { (&self.direct_test_key, &self.direct_test_publishable_key) };
178
+ let (key, publishable) = if mode == "live" {
179
+ (&self.direct_live_key, &self.direct_live_publishable_key)
180
+ } else {
181
+ (&self.direct_test_key, &self.direct_test_publishable_key)
182
+ };
158
183
  if !key.is_empty() && !publishable.is_empty() {
159
184
  return (key, publishable);
160
185
  }
161
186
  match self.stored.mode(mode) {
162
- Some(saved) if !saved.secret_key.is_empty() && !saved.publishable_key.is_empty() => (&saved.secret_key, &saved.publishable_key),
187
+ Some(saved) if !saved.secret_key.is_empty() && !saved.publishable_key.is_empty() => {
188
+ (&saved.secret_key, &saved.publishable_key)
189
+ }
163
190
  _ => (key, publishable),
164
191
  }
165
192
  }
@@ -175,7 +202,12 @@ impl PlatformConfig {
175
202
  /// The modes the business's own account can be used in: those with both
176
203
  /// its secret key and its publishable key, from the environment or saved.
177
204
  pub fn direct_modes(&self) -> Vec<&'static str> {
178
- MODES.into_iter().filter(|mode| !self.direct_key(mode).is_empty() && !self.direct_publishable_key(mode).is_empty()).collect()
205
+ MODES
206
+ .into_iter()
207
+ .filter(|mode| {
208
+ !self.direct_key(mode).is_empty() && !self.direct_publishable_key(mode).is_empty()
209
+ })
210
+ .collect()
179
211
  }
180
212
 
181
213
  fn is_operator(&self, email: &str) -> bool {
@@ -203,10 +235,26 @@ impl Connection {
203
235
  /// This tenant's connection, read out of the replayed instances, or `None`
204
236
  /// until an Owner has connected an account.
205
237
  pub fn connection(read: &Value, domain: &str) -> Option<Connection> {
206
- let (_, state) = instances_for(read, &crate::commerce_ir::payment_connection_binding(domain).instance_prefix()).into_iter().find(|(id, _)| id == CONNECTION_SLUG)?;
207
- let text = |field: &str| state.get(field).and_then(|v| v.get("value")).and_then(|v| v.as_str()).unwrap_or_default().to_string();
238
+ let (_, state) = instances_for(
239
+ read,
240
+ &crate::commerce_ir::payment_connection_binding(domain).instance_prefix(),
241
+ )
242
+ .into_iter()
243
+ .find(|(id, _)| id == CONNECTION_SLUG)?;
244
+ let text = |field: &str| {
245
+ state
246
+ .get(field)
247
+ .and_then(|v| v.get("value"))
248
+ .and_then(|v| v.as_str())
249
+ .unwrap_or_default()
250
+ .to_string()
251
+ };
208
252
  Some(Connection {
209
- status: state.get("status").and_then(|v| v.as_str()).unwrap_or_default().to_string(),
253
+ status: state
254
+ .get("status")
255
+ .and_then(|v| v.as_str())
256
+ .unwrap_or_default()
257
+ .to_string(),
210
258
  processor: text("processor"),
211
259
  account_ref: text("account_ref"),
212
260
  mode: text("mode"),
@@ -220,7 +268,10 @@ pub enum CheckoutPlan {
220
268
  Mock,
221
269
  /// Payments are enabled: charged on the business's own account with its own
222
270
  /// key; the guest pays in a form embedded in the site via the publishable key.
223
- Stripe { api_key: String, publishable_key: String },
271
+ Stripe {
272
+ api_key: String,
273
+ publishable_key: String,
274
+ },
224
275
  /// Payments were enabled and cannot be taken now. Registrations answer
225
276
  /// 503; never a fallback to the mock.
226
277
  Paused,
@@ -229,7 +280,9 @@ pub enum CheckoutPlan {
229
280
  /// Decides checkout per request from the tenant's own connection, never a
230
281
  /// process-wide setting; an enabled connection this host cannot actually charge is paused too.
231
282
  pub fn checkout_plan(connection: Option<&Connection>, platform: &PlatformConfig) -> CheckoutPlan {
232
- let Some(connection) = connection else { return CheckoutPlan::Mock };
283
+ let Some(connection) = connection else {
284
+ return CheckoutPlan::Mock;
285
+ };
233
286
  match connection.status.as_str() {
234
287
  "enabled" => {
235
288
  let (key, publishable_key) = platform.direct_pair(&connection.mode);
@@ -243,7 +296,10 @@ pub fn checkout_plan(connection: Option<&Connection>, platform: &PlatformConfig)
243
296
  );
244
297
  CheckoutPlan::Paused
245
298
  } else {
246
- CheckoutPlan::Stripe { api_key: key.to_string(), publishable_key: publishable_key.to_string() }
299
+ CheckoutPlan::Stripe {
300
+ api_key: key.to_string(),
301
+ publishable_key: publishable_key.to_string(),
302
+ }
247
303
  }
248
304
  }
249
305
  "paused" => CheckoutPlan::Paused,
@@ -254,13 +310,13 @@ pub fn checkout_plan(connection: Option<&Connection>, platform: &PlatformConfig)
254
310
  async fn dispatch_on_connection(
255
311
  which: crate::commerce_ir::ConnectionVerb,
256
312
  facts: Value,
257
- client: &Mutex<Client>,
258
- wasm_path: &Path,
259
- config: &LineageConfig,
260
- invoker: &dyn LambdaInvoker,
313
+ host: Host<'_>,
261
314
  ) -> anyhow::Result<dispatch::Outcome> {
262
- let verb = crate::commerce_ir::payment_connection_binding(&config.domain).verb(which).to_string();
263
- dispatch::handle_routed(client, wasm_path, &verb, json!(CONNECTION_SLUG), facts, None, config, invoker).await
315
+ let Host { config, .. } = host;
316
+ let verb = crate::commerce_ir::payment_connection_binding(&config.domain)
317
+ .verb(which)
318
+ .to_string();
319
+ dispatch::handle_routed(host, &verb, json!(CONNECTION_SLUG), facts, None).await
264
320
  }
265
321
 
266
322
  /// Whether `path` belongs to the payments surface.
@@ -268,19 +324,22 @@ pub fn owns(method: &str, path: &str) -> bool {
268
324
  matches!(
269
325
  (method, path),
270
326
  ("GET", "/payments/connection")
271
- | ("POST", "/payments/connection/direct")
272
- | ("POST", "/payments/connection/disconnect")
273
- | ("POST", "/payments/connection/enable")
274
- | ("POST", "/payments/connection/disable")
327
+ | (
328
+ "POST",
329
+ "/payments/connection/direct"
330
+ | "/payments/connection/disconnect"
331
+ | "/payments/connection/enable"
332
+ | "/payments/connection/disable"
333
+ )
275
334
  )
276
335
  }
277
336
 
278
- fn json_error(status: u16, message: &str) -> Value {
279
- respond(status, "application/json", &json!({"error": message}).to_string())
280
- }
281
-
282
337
  fn refusal_message(refusal: &Value) -> String {
283
- ["message", "error"].iter().find_map(|key| refusal.get(*key).and_then(|v| v.as_str())).unwrap_or("Refused").to_string()
338
+ ["message", "error"]
339
+ .iter()
340
+ .find_map(|key| refusal.get(*key).and_then(|v| v.as_str()))
341
+ .unwrap_or("Refused")
342
+ .to_string()
284
343
  }
285
344
 
286
345
  struct Caller {
@@ -290,34 +349,67 @@ struct Caller {
290
349
 
291
350
  // The person behind an account cookie, only while they still
292
351
  // have access, with what they may do here. `Err` is the JSON response.
293
- async fn caller(domain_ir: &Value, cookies: &HashMap<String, String>, secret: &str, client: &Mutex<Client>, platform: &PlatformConfig) -> Result<Caller, Value> {
352
+ async fn caller(
353
+ domain_ir: &Value,
354
+ cookies: &HashMap<String, String>,
355
+ secret: &str,
356
+ client: &Mutex<Client>,
357
+ platform: &PlatformConfig,
358
+ ) -> Result<Caller, Value> {
294
359
  let not_logged_in = || json_error(401, "not logged in");
295
- let Some(email) = cookies.get(&auth::account_cookie_name()).and_then(|token| auth::verify_account_token(secret, token)) else {
360
+ let Some(email) = cookies
361
+ .get(&auth::account_cookie_name())
362
+ .and_then(|token| auth::verify_account_token(secret, token))
363
+ else {
296
364
  return Err(not_logged_in());
297
365
  };
298
366
  match auth::active_role(client, domain_ir, &email).await {
299
- Ok(Some(role)) => Ok(Caller { manager: roles::manages(&platform.manager_roles, &role), operator: platform.is_operator(&email) }),
367
+ Ok(Some(role)) => Ok(Caller {
368
+ manager: roles::manages(&platform.manager_roles, &role),
369
+ operator: platform.is_operator(&email),
370
+ }),
300
371
  Ok(None) => Err(not_logged_in()),
301
372
  Err(e) => Err(json_error(500, &format!("members lookup failed: {e}"))),
302
373
  }
303
374
  }
304
375
 
305
376
  fn require_manager(caller: &Caller) -> Result<(), Value> {
306
- if caller.manager { Ok(()) } else { Err(json_error(403, "only an Owner can manage payments")) }
377
+ if caller.manager {
378
+ Ok(())
379
+ } else {
380
+ Err(json_error(403, "only an Owner can manage payments"))
381
+ }
307
382
  }
308
383
 
309
384
  fn require_operator(caller: &Caller) -> Result<(), Value> {
310
- if caller.operator { Ok(()) } else { Err(json_error(403, "only the platform operator can change whether payments are enabled")) }
385
+ if caller.operator {
386
+ Ok(())
387
+ } else {
388
+ Err(json_error(
389
+ 403,
390
+ "only the platform operator can change whether payments are enabled",
391
+ ))
392
+ }
311
393
  }
312
394
 
313
395
  // What the payments page may show: public facts and which of the business's own
314
396
  // modes have keys. Built field by field, so a credential can never appear here:
315
397
  // none is ever read into this value.
316
- fn connection_json(connection: Option<&Connection>, platform: &PlatformConfig, caller: &Caller) -> Value {
317
- let label = connection.filter(|c| c.processor == PROCESSOR).map(|_| "Stripe");
318
- let present = |value: Option<&str>| value.filter(|s| !s.is_empty()).map(Value::from).unwrap_or(Value::Null);
398
+ fn connection_json(
399
+ connection: Option<&Connection>,
400
+ platform: &PlatformConfig,
401
+ caller: &Caller,
402
+ ) -> Value {
403
+ let label = connection
404
+ .filter(|c| c.processor == PROCESSOR)
405
+ .map(|_| "Stripe");
406
+ let present = |value: Option<&str>| {
407
+ value
408
+ .filter(|s| !s.is_empty())
409
+ .map_or(Value::Null, Value::from)
410
+ };
319
411
  json!({
320
- "status": connection.map(|c| c.status.as_str()).unwrap_or("not_connected"),
412
+ "status": connection.map_or("not_connected", |c| c.status.as_str()),
321
413
  "processor": present(connection.map(|c| c.processor.as_str())),
322
414
  "label": label,
323
415
  "account_ref": present(connection.map(|c| c.account_ref.as_str())),
@@ -335,24 +427,40 @@ fn connection_json(connection: Option<&Connection>, platform: &PlatformConfig, c
335
427
  })
336
428
  }
337
429
 
338
- async fn current_connection(client: &Mutex<Client>, wasm_path: &Path, config: &LineageConfig) -> Result<Option<Connection>, Value> {
430
+ async fn current_connection(
431
+ client: &Mutex<Client>,
432
+ wasm_path: &Path,
433
+ config: &LineageConfig,
434
+ ) -> Result<Option<Connection>, Value> {
339
435
  match dispatch::read(client, wasm_path).await {
340
436
  Ok(read) => Ok(connection(&read, &config.domain)),
341
- Err(e) => Err(json_error(500, &format!("payment connection lookup failed: {e:#}"))),
437
+ Err(e) => Err(json_error(
438
+ 500,
439
+ &format!("payment connection lookup failed: {e:#}"),
440
+ )),
342
441
  }
343
442
  }
344
443
 
345
- async fn connection_response(caller: &Caller, platform: &PlatformConfig, client: &Mutex<Client>, wasm_path: &Path, config: &LineageConfig) -> Value {
444
+ async fn connection_response(
445
+ caller: &Caller,
446
+ platform: &PlatformConfig,
447
+ client: &Mutex<Client>,
448
+ wasm_path: &Path,
449
+ config: &LineageConfig,
450
+ ) -> Value {
346
451
  match current_connection(client, wasm_path, config).await {
347
- Ok(connection) => {
348
- respond(200, "application/json", &connection_json(connection.as_ref(), platform, caller).to_string())
349
- }
452
+ Ok(connection) => respond(
453
+ 200,
454
+ "application/json",
455
+ &connection_json(connection.as_ref(), platform, caller).to_string(),
456
+ ),
350
457
  Err(response) => response,
351
458
  }
352
459
  }
353
460
 
354
461
  /// The `/payments/connection` routes for a caller holding the account cookie; every
355
462
  /// answer is JSON, a bad session is 401 not a redirect, and `None` means an unowned path.
463
+ #[allow(clippy::implicit_hasher)] // reason: the cookie map always comes from this crate's own default-hashed parser
356
464
  #[allow(clippy::too_many_arguments)]
357
465
  pub async fn route(
358
466
  method: &str,
@@ -362,11 +470,14 @@ pub async fn route(
362
470
  secret: &str,
363
471
  domain_ir: &Value,
364
472
  platform: &PlatformConfig,
365
- client: &Mutex<Client>,
366
- wasm_path: &Path,
367
- config: &LineageConfig,
368
- invoker: &dyn LambdaInvoker,
473
+ host: Host<'_>,
369
474
  ) -> Option<Value> {
475
+ let Host {
476
+ client,
477
+ wasm_path,
478
+ config,
479
+ ..
480
+ } = host;
370
481
  if !owns(method, path) {
371
482
  return None;
372
483
  }
@@ -376,15 +487,37 @@ pub async fn route(
376
487
  };
377
488
  Some(match path {
378
489
  "/payments/connection" => show(&caller, platform, client, wasm_path, config).await,
379
- "/payments/connection/direct" => direct_route(&caller, raw_body, platform, client, wasm_path, config, invoker).await,
380
- "/payments/connection/disconnect" => disconnect_route(&caller, platform, client, wasm_path, config, invoker).await,
381
- "/payments/connection/enable" => switch_route(&caller, crate::commerce_ir::ConnectionVerb::Enable, platform, client, wasm_path, config, invoker).await,
382
- _ => switch_route(&caller, crate::commerce_ir::ConnectionVerb::Disable, platform, client, wasm_path, config, invoker).await,
490
+ "/payments/connection/direct" => direct_route(&caller, raw_body, platform, host).await,
491
+ "/payments/connection/disconnect" => disconnect_route(&caller, platform, host).await,
492
+ "/payments/connection/enable" => {
493
+ switch_route(
494
+ &caller,
495
+ crate::commerce_ir::ConnectionVerb::Enable,
496
+ platform,
497
+ host,
498
+ )
499
+ .await
500
+ }
501
+ _ => {
502
+ switch_route(
503
+ &caller,
504
+ crate::commerce_ir::ConnectionVerb::Disable,
505
+ platform,
506
+ host,
507
+ )
508
+ .await
509
+ }
383
510
  })
384
511
  }
385
512
 
386
513
  // GET: readable by a payments manager or the operator, nobody else.
387
- async fn show(caller: &Caller, platform: &PlatformConfig, client: &Mutex<Client>, wasm_path: &Path, config: &LineageConfig) -> Value {
514
+ async fn show(
515
+ caller: &Caller,
516
+ platform: &PlatformConfig,
517
+ client: &Mutex<Client>,
518
+ wasm_path: &Path,
519
+ config: &LineageConfig,
520
+ ) -> Value {
388
521
  if !caller.manager && !caller.operator {
389
522
  return json_error(403, "not permitted");
390
523
  }
@@ -392,7 +525,10 @@ async fn show(caller: &Caller, platform: &PlatformConfig, client: &Mutex<Client>
392
525
  }
393
526
 
394
527
  fn body_field(body: &Value, key: &str) -> Result<String, Value> {
395
- body.get(key).and_then(|v| v.as_str()).map(String::from).ok_or_else(|| json_error(400, &format!("missing {key}")))
528
+ body.get(key)
529
+ .and_then(|v| v.as_str())
530
+ .map(String::from)
531
+ .ok_or_else(|| json_error(400, &format!("missing {key}")))
396
532
  }
397
533
 
398
534
  fn parse_body(raw_body: &str) -> Result<Value, Value> {
@@ -406,11 +542,14 @@ async fn record_connection(
406
542
  caller: &Caller,
407
543
  platform: &PlatformConfig,
408
544
  facts: Value,
409
- client: &Mutex<Client>,
410
- wasm_path: &Path,
411
- config: &LineageConfig,
412
- invoker: &dyn LambdaInvoker,
545
+ host: Host<'_>,
413
546
  ) -> Value {
547
+ let Host {
548
+ client,
549
+ wasm_path,
550
+ config,
551
+ ..
552
+ } = host;
414
553
  let existing = match current_connection(client, wasm_path, config).await {
415
554
  Ok(existing) => existing,
416
555
  Err(response) => return response,
@@ -419,14 +558,26 @@ async fn record_connection(
419
558
  None => {
420
559
  let mut creation = facts;
421
560
  creation["slug"] = json!({"value": CONNECTION_SLUG});
422
- dispatch::handle_facts(client, wasm_path, &crate::commerce_ir::payment_connection_binding(&config.domain).connect, creation, None, config, invoker).await
561
+ dispatch::handle_facts(
562
+ host,
563
+ &crate::commerce_ir::payment_connection_binding(&config.domain).connect,
564
+ creation,
565
+ None,
566
+ )
567
+ .await
568
+ }
569
+ Some("disconnected") => {
570
+ dispatch_on_connection(crate::commerce_ir::ConnectionVerb::Reconnect, facts, host).await
571
+ }
572
+ Some("paused") => {
573
+ dispatch_on_connection(crate::commerce_ir::ConnectionVerb::Resume, facts, host).await
423
574
  }
424
- Some("disconnected") => dispatch_on_connection(crate::commerce_ir::ConnectionVerb::Reconnect, facts, client, wasm_path, config, invoker).await,
425
- Some("paused") => dispatch_on_connection(crate::commerce_ir::ConnectionVerb::Resume, facts, client, wasm_path, config, invoker).await,
426
575
  Some(_) => return json_error(409, "an account is already connected — disconnect it first"),
427
576
  };
428
577
  match outcome {
429
- Ok(outcome) if outcome.accepted => connection_response(caller, platform, client, wasm_path, config).await,
578
+ Ok(outcome) if outcome.accepted => {
579
+ connection_response(caller, platform, client, wasm_path, config).await
580
+ }
430
581
  Ok(outcome) => json_error(422, &refusal_message(&last_refusal(&outcome.result))),
431
582
  Err(e) => json_error(500, &format!("{e:#}")),
432
583
  }
@@ -435,8 +586,12 @@ async fn record_connection(
435
586
  // An Owner either saves keys (`save_keys_route`) or picks a mode with keys already
436
587
  // in the environment; only public facts are stored (account ref, mode, display
437
588
  // name), and the key itself is never stored or logged.
438
- #[allow(clippy::too_many_arguments)]
439
- async fn direct_route(caller: &Caller, raw_body: &str, platform: &PlatformConfig, client: &Mutex<Client>, wasm_path: &Path, config: &LineageConfig, invoker: &dyn LambdaInvoker) -> Value {
589
+ async fn direct_route(
590
+ caller: &Caller,
591
+ raw_body: &str,
592
+ platform: &PlatformConfig,
593
+ host: Host<'_>,
594
+ ) -> Value {
440
595
  if let Err(response) = require_manager(caller) {
441
596
  return response;
442
597
  }
@@ -447,14 +602,17 @@ async fn direct_route(caller: &Caller, raw_body: &str, platform: &PlatformConfig
447
602
  // The Payments page's Save form sends the keys themselves; the other form
448
603
  // names a mode whose keys are already in the environment.
449
604
  if body.get("secret_key").is_some() || body.get("publishable_key").is_some() {
450
- return save_keys_route(caller, &body, platform, client, wasm_path, config, invoker).await;
605
+ return save_keys_route(caller, &body, platform, host).await;
451
606
  }
452
607
  let mode = match body_field(&body, "mode") {
453
608
  Ok(mode) => mode,
454
609
  Err(response) => return response,
455
610
  };
456
611
  if !platform.direct_modes().contains(&mode.as_str()) {
457
- return json_error(422, &format!("this site's own Stripe account is not set up for {mode} mode"));
612
+ return json_error(
613
+ 422,
614
+ &format!("this site's own Stripe account is not set up for {mode} mode"),
615
+ );
458
616
  }
459
617
  let display_name = own_account_display_name(platform, platform.direct_key(&mode)).await;
460
618
  let facts = json!({
@@ -463,13 +621,19 @@ async fn direct_route(caller: &Caller, raw_body: &str, platform: &PlatformConfig
463
621
  "mode": {"value": mode},
464
622
  "display_name": {"value": display_name},
465
623
  });
466
- record_connection(caller, platform, facts, client, wasm_path, config, invoker).await
624
+ record_connection(caller, platform, facts, host).await
467
625
  }
468
626
 
469
627
  // Not enabled: a plain disconnect. Enabled: registrations pause instead, since real
470
628
  // guests have paid through this account. Disconnecting the business's own account
471
629
  // also removes its webhook and deletes the saved keys.
472
- async fn disconnect_route(caller: &Caller, platform: &PlatformConfig, client: &Mutex<Client>, wasm_path: &Path, config: &LineageConfig, invoker: &dyn LambdaInvoker) -> Value {
630
+ async fn disconnect_route(caller: &Caller, platform: &PlatformConfig, host: Host<'_>) -> Value {
631
+ let Host {
632
+ client,
633
+ wasm_path,
634
+ config,
635
+ ..
636
+ } = host;
473
637
  if let Err(response) = require_manager(caller) {
474
638
  return response;
475
639
  }
@@ -477,12 +641,17 @@ async fn disconnect_route(caller: &Caller, platform: &PlatformConfig, client: &M
477
641
  Ok(existing) => existing,
478
642
  Err(response) => return response,
479
643
  };
480
- let Some(existing) = existing.filter(|c| matches!(c.status.as_str(), "connected" | "enabled")) else {
644
+ let Some(existing) = existing.filter(|c| matches!(c.status.as_str(), "connected" | "enabled"))
645
+ else {
481
646
  return json_error(409, "nothing to disconnect");
482
647
  };
483
648
 
484
- let which = if existing.status == "enabled" { crate::commerce_ir::ConnectionVerb::Suspend } else { crate::commerce_ir::ConnectionVerb::Disconnect };
485
- match dispatch_on_connection(which, json!({}), client, wasm_path, config, invoker).await {
649
+ let which = if existing.status == "enabled" {
650
+ crate::commerce_ir::ConnectionVerb::Suspend
651
+ } else {
652
+ crate::commerce_ir::ConnectionVerb::Disconnect
653
+ };
654
+ match dispatch_on_connection(which, json!({}), host).await {
486
655
  Ok(outcome) if outcome.accepted => {
487
656
  if existing.is_direct() {
488
657
  forget_saved_keys(platform, &existing.mode).await;
@@ -496,7 +665,18 @@ async fn disconnect_route(caller: &Caller, platform: &PlatformConfig, client: &M
496
665
  }
497
666
 
498
667
  // Enable or disable real payments: operator only.
499
- async fn switch_route(caller: &Caller, which: crate::commerce_ir::ConnectionVerb, platform: &PlatformConfig, client: &Mutex<Client>, wasm_path: &Path, config: &LineageConfig, invoker: &dyn LambdaInvoker) -> Value {
668
+ async fn switch_route(
669
+ caller: &Caller,
670
+ which: crate::commerce_ir::ConnectionVerb,
671
+ platform: &PlatformConfig,
672
+ host: Host<'_>,
673
+ ) -> Value {
674
+ let Host {
675
+ client,
676
+ wasm_path,
677
+ config,
678
+ ..
679
+ } = host;
500
680
  if let Err(response) = require_operator(caller) {
501
681
  return response;
502
682
  }
@@ -505,15 +685,20 @@ async fn switch_route(caller: &Caller, which: crate::commerce_ir::ConnectionVerb
505
685
  Ok(None) => return json_error(409, "connect an account first"),
506
686
  Err(response) => return response,
507
687
  }
508
- match dispatch_on_connection(which, json!({}), client, wasm_path, config, invoker).await {
509
- Ok(outcome) if outcome.accepted => connection_response(caller, platform, client, wasm_path, config).await,
688
+ match dispatch_on_connection(which, json!({}), host).await {
689
+ Ok(outcome) if outcome.accepted => {
690
+ connection_response(caller, platform, client, wasm_path, config).await
691
+ }
510
692
  Ok(outcome) => json_error(422, &refusal_message(&last_refusal(&outcome.result))),
511
693
  Err(e) => json_error(500, &format!("{e:#}")),
512
694
  }
513
695
  }
514
696
 
515
697
  fn stripe_http() -> Result<reqwest::Client, String> {
516
- reqwest::Client::builder().timeout(STRIPE_TIMEOUT).build().map_err(|e| e.to_string())
698
+ reqwest::Client::builder()
699
+ .timeout(STRIPE_TIMEOUT)
700
+ .build()
701
+ .map_err(|e| e.to_string())
517
702
  }
518
703
 
519
704
  // The name of the business's own account, read with that account's own key
@@ -521,19 +706,34 @@ fn stripe_http() -> Result<reqwest::Client, String> {
521
706
  // asked or has nothing better; the key is only ever sent as the bearer.
522
707
  async fn own_account_display_name(platform: &PlatformConfig, key: &str) -> String {
523
708
  let fetch = async {
524
- let response = stripe_http()?.get(format!("{}/v1/account", platform.api_base)).bearer_auth(key).send().await.map_err(|e| e.to_string())?;
709
+ let response = stripe_http()?
710
+ .get(format!("{}/v1/account", platform.api_base))
711
+ .bearer_auth(key)
712
+ .send()
713
+ .await
714
+ .map_err(|e| e.to_string())?;
525
715
  if !response.status().is_success() {
526
716
  return Err("account lookup refused".to_string());
527
717
  }
528
718
  response.json::<Value>().await.map_err(|e| e.to_string())
529
719
  };
530
- let Ok(account) = fetch.await else { return DIRECT_DISPLAY_FALLBACK.to_string() };
720
+ let Ok(account) = fetch.await else {
721
+ return DIRECT_DISPLAY_FALLBACK.to_string();
722
+ };
531
723
  let candidates = [
532
724
  account.get("business_profile").and_then(|p| p.get("name")),
533
- account.get("settings").and_then(|s| s.get("dashboard")).and_then(|d| d.get("display_name")),
725
+ account
726
+ .get("settings")
727
+ .and_then(|s| s.get("dashboard"))
728
+ .and_then(|d| d.get("display_name")),
534
729
  account.get("email"),
535
730
  ];
536
- let name = candidates.into_iter().flatten().find_map(|v| v.as_str().filter(|s| !s.is_empty())).unwrap_or(DIRECT_DISPLAY_FALLBACK).to_string();
731
+ let name = candidates
732
+ .into_iter()
733
+ .flatten()
734
+ .find_map(|v| v.as_str().filter(|s| !s.is_empty()))
735
+ .unwrap_or(DIRECT_DISPLAY_FALLBACK)
736
+ .to_string();
537
737
  name
538
738
  }
539
739
 
@@ -543,7 +743,11 @@ const STRIPE_UNREACHABLE: &str = "Could not reach Stripe. Please try again in a
543
743
  const WEBHOOK_PERMISSION: &str = "That key can't create the webhook. In Stripe, give it Webhook Endpoints → Write as well and try again.";
544
744
  const WEBHOOK_FAILED: &str = "Stripe could not set up the payment webhook. Please try again.";
545
745
  const STORE_FAILED: &str = "The keys could not be stored, so nothing was saved. Please try again.";
546
- const WEBHOOK_EVENTS: [&str; 3] = ["checkout.session.completed", "checkout.session.expired", "charge.refunded"];
746
+ const WEBHOOK_EVENTS: [&str; 3] = [
747
+ "checkout.session.completed",
748
+ "checkout.session.expired",
749
+ "charge.refunded",
750
+ ];
547
751
 
548
752
  // A copy of `platform` with the key store read again, for a response that must
549
753
  // show the keys just saved or removed.
@@ -580,10 +784,17 @@ fn keys_mode(secret_key: &str, publishable_key: &str) -> Result<&'static str, &'
580
784
  fn business_name(account: &Value) -> Option<String> {
581
785
  let candidates = [
582
786
  account.get("business_profile").and_then(|p| p.get("name")),
583
- account.get("settings").and_then(|s| s.get("dashboard")).and_then(|d| d.get("display_name")),
787
+ account
788
+ .get("settings")
789
+ .and_then(|s| s.get("dashboard"))
790
+ .and_then(|d| d.get("display_name")),
584
791
  account.get("email"),
585
792
  ];
586
- candidates.into_iter().flatten().find_map(|v| v.as_str().filter(|s| !s.is_empty())).map(String::from)
793
+ candidates
794
+ .into_iter()
795
+ .flatten()
796
+ .find_map(|v| v.as_str().filter(|s| !s.is_empty()))
797
+ .map(String::from)
587
798
  }
588
799
 
589
800
  // Asks Stripe whether the key works and what the business is called. A restricted
@@ -619,10 +830,21 @@ enum WebhookError {
619
830
  // Creates the webhook endpoint in the business's own Stripe account and returns
620
831
  // its id and signing secret. Stripe shows the signing secret only in this
621
832
  // answer, so it is captured here and nowhere else. Only the status is logged.
622
- async fn create_webhook(platform: &PlatformConfig, key: &str) -> Result<(String, String), WebhookError> {
833
+ async fn create_webhook(
834
+ platform: &PlatformConfig,
835
+ key: &str,
836
+ ) -> Result<(String, String), WebhookError> {
623
837
  let url = format!("{}/webhooks/stripe", platform.webhook_base_url);
624
- let mut params: Vec<(String, String)> = vec![("url".into(), url), ("description".into(), platform.webhook_description.clone())];
625
- params.extend(WEBHOOK_EVENTS.iter().enumerate().map(|(index, event)| (format!("enabled_events[{index}]"), event.to_string())));
838
+ let mut params: Vec<(String, String)> = vec![
839
+ ("url".into(), url),
840
+ ("description".into(), platform.webhook_description.clone()),
841
+ ];
842
+ params.extend(
843
+ WEBHOOK_EVENTS
844
+ .iter()
845
+ .enumerate()
846
+ .map(|(index, event)| (format!("enabled_events[{index}]"), event.to_string())),
847
+ );
626
848
  let response = stripe_http()
627
849
  .map_err(|_| WebhookError::Other)?
628
850
  .post(format!("{}/v1/webhook_endpoints", platform.api_base))
@@ -641,7 +863,12 @@ async fn create_webhook(platform: &PlatformConfig, key: &str) -> Result<(String,
641
863
  return Err(WebhookError::Other);
642
864
  }
643
865
  let body: Value = response.json().await.map_err(|_| WebhookError::Other)?;
644
- let text = |field: &str| body.get(field).and_then(|v| v.as_str()).filter(|s| !s.is_empty()).map(String::from);
866
+ let text = |field: &str| {
867
+ body.get(field)
868
+ .and_then(|v| v.as_str())
869
+ .filter(|s| !s.is_empty())
870
+ .map(String::from)
871
+ };
645
872
  match (text("id"), text("secret")) {
646
873
  (Some(id), Some(secret)) => Ok((id, secret)),
647
874
  _ => Err(WebhookError::Other),
@@ -651,19 +878,46 @@ async fn create_webhook(platform: &PlatformConfig, key: &str) -> Result<(String,
651
878
  // Removes a webhook endpoint from the business's Stripe account, best effort:
652
879
  // the answer says whether Stripe confirmed it, and nothing else is reported.
653
880
  async fn delete_webhook(platform: &PlatformConfig, key: &str, endpoint_id: &str) -> bool {
654
- let Ok(http) = stripe_http() else { return false };
655
- let sent = http.delete(format!("{}/v1/webhook_endpoints/{endpoint_id}", platform.api_base)).bearer_auth(key).header("Stripe-Version", STRIPE_API_VERSION).send().await;
881
+ let Ok(http) = stripe_http() else {
882
+ return false;
883
+ };
884
+ let sent = http
885
+ .delete(format!(
886
+ "{}/v1/webhook_endpoints/{endpoint_id}",
887
+ platform.api_base
888
+ ))
889
+ .bearer_auth(key)
890
+ .header("Stripe-Version", STRIPE_API_VERSION)
891
+ .send()
892
+ .await;
656
893
  matches!(sent, Ok(response) if response.status().is_success())
657
894
  }
658
895
 
659
896
  // Checks the keys, has Stripe deliver events here, and stores the keys and webhook
660
897
  // secret before recording the connection; saving again in place replaces both.
661
898
  // Nothing here answers with, logs, or errors with a key.
662
- #[allow(clippy::too_many_arguments)]
663
- async fn save_keys_route(caller: &Caller, body: &Value, platform: &PlatformConfig, client: &Mutex<Client>, wasm_path: &Path, config: &LineageConfig, invoker: &dyn LambdaInvoker) -> Value {
664
- let Some(store) = platform.store.clone() else { return json_error(422, KEYS_NOT_ENABLED) };
665
- let (secret_key, publishable_key) = match (body_field(body, "secret_key"), body_field(body, "publishable_key")) {
666
- (Ok(secret), Ok(publishable)) => (secret.trim().to_string(), publishable.trim().to_string()),
899
+ async fn save_keys_route(
900
+ caller: &Caller,
901
+ body: &Value,
902
+ platform: &PlatformConfig,
903
+ host: Host<'_>,
904
+ ) -> Value {
905
+ let Host {
906
+ client,
907
+ wasm_path,
908
+ config,
909
+ ..
910
+ } = host;
911
+ let Some(store) = platform.store.clone() else {
912
+ return json_error(422, KEYS_NOT_ENABLED);
913
+ };
914
+ let (secret_key, publishable_key) = match (
915
+ body_field(body, "secret_key"),
916
+ body_field(body, "publishable_key"),
917
+ ) {
918
+ (Ok(secret), Ok(publishable)) => {
919
+ (secret.trim().to_string(), publishable.trim().to_string())
920
+ }
667
921
  (Err(response), _) | (_, Err(response)) => return response,
668
922
  };
669
923
  let mode = match keys_mode(&secret_key, &publishable_key) {
@@ -691,14 +945,28 @@ async fn save_keys_route(caller: &Caller, body: &Value, platform: &PlatformConfi
691
945
  // What the last save made, to be replaced once the new webhook is safely
692
946
  // stored: a save that fails part way must never leave the business without
693
947
  // a working webhook.
694
- let previous = store.refresh().await.ok().and_then(|document| document.mode(mode).cloned()).filter(|old| !old.webhook_endpoint_id.is_empty());
948
+ let previous = store
949
+ .refresh()
950
+ .await
951
+ .ok()
952
+ .and_then(|document| document.mode(mode).cloned())
953
+ .filter(|old| !old.webhook_endpoint_id.is_empty());
695
954
  let (endpoint_id, webhook_secret) = match create_webhook(platform, &secret_key).await {
696
955
  Ok(created) => created,
697
956
  Err(WebhookError::Permission) => return json_error(422, WEBHOOK_PERMISSION),
698
957
  Err(WebhookError::Other) => return json_error(422, WEBHOOK_FAILED),
699
958
  };
700
- let saved = StoredKeys { secret_key: secret_key.clone(), publishable_key, webhook_secret, webhook_endpoint_id: endpoint_id.clone(), saved_at: keystore::now_timestamp() };
701
- if let Err(e) = store.update(|document| document.set(mode, Some(saved))).await {
959
+ let saved = StoredKeys {
960
+ secret_key: secret_key.clone(),
961
+ publishable_key,
962
+ webhook_secret,
963
+ webhook_endpoint_id: endpoint_id.clone(),
964
+ saved_at: keystore::now_timestamp(),
965
+ };
966
+ if let Err(e) = store
967
+ .update(|document| document.set(mode, Some(saved)))
968
+ .await
969
+ {
702
970
  eprintln!("saving the payment keys failed: {e}");
703
971
  delete_webhook(platform, &secret_key, &endpoint_id).await;
704
972
  return json_error(500, STORE_FAILED);
@@ -721,11 +989,15 @@ async fn save_keys_route(caller: &Caller, body: &Value, platform: &PlatformConfi
721
989
  "mode": {"value": mode},
722
990
  "display_name": {"value": display_name},
723
991
  });
724
- let response = record_connection(caller, &fresh, facts, client, wasm_path, config, invoker).await;
992
+ let response = record_connection(caller, &fresh, facts, host).await;
725
993
  if response["statusCode"].as_u64() != Some(200) {
726
994
  // The domain would not record the connection, so leave nothing behind.
727
995
  delete_webhook(platform, &secret_key, &endpoint_id).await;
728
- if store.update(|document| document.set(mode, None)).await.is_err() {
996
+ if store
997
+ .update(|document| document.set(mode, None))
998
+ .await
999
+ .is_err()
1000
+ {
729
1001
  eprintln!("the saved payment keys could not be removed after a refused connection");
730
1002
  }
731
1003
  }
@@ -736,7 +1008,9 @@ async fn save_keys_route(caller: &Caller, body: &Value, platform: &PlatformConfi
736
1008
  // that mode, best effort and logged without any secret. Environment keys and Stripe
737
1009
  // are untouched when nothing was ever saved.
738
1010
  async fn forget_saved_keys(platform: &PlatformConfig, mode: &str) {
739
- let Some(store) = platform.store.clone() else { return };
1011
+ let Some(store) = platform.store.clone() else {
1012
+ return;
1013
+ };
740
1014
  let saved = match store.refresh().await {
741
1015
  Ok(document) => document.mode(mode).cloned(),
742
1016
  Err(e) => {
@@ -745,7 +1019,9 @@ async fn forget_saved_keys(platform: &PlatformConfig, mode: &str) {
745
1019
  }
746
1020
  };
747
1021
  let Some(saved) = saved else { return };
748
- if !saved.webhook_endpoint_id.is_empty() && !delete_webhook(platform, &saved.secret_key, &saved.webhook_endpoint_id).await {
1022
+ if !saved.webhook_endpoint_id.is_empty()
1023
+ && !delete_webhook(platform, &saved.secret_key, &saved.webhook_endpoint_id).await
1024
+ {
749
1025
  eprintln!("the Stripe webhook could not be removed from the business's account");
750
1026
  }
751
1027
  if let Err(e) = store.update(|document| document.set(mode, None)).await {
@@ -777,7 +1053,10 @@ pub(crate) fn test_platform() -> PlatformConfig {
777
1053
  /// `test_platform` with a webhook signing secret set.
778
1054
  #[cfg(test)]
779
1055
  pub(crate) fn test_platform_with_secret(secret: &str) -> PlatformConfig {
780
- PlatformConfig { webhook_secret: Some(secret.to_string()), ..test_platform() }
1056
+ PlatformConfig {
1057
+ webhook_secret: Some(secret.to_string()),
1058
+ ..test_platform()
1059
+ }
781
1060
  }
782
1061
 
783
1062
  #[cfg(test)]