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