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.
- checksums.yaml +4 -4
- data/examples/pizzas/adapters/fake_processor.rb +34 -0
- data/examples/pizzas/adapters/payment_signature.rb +66 -0
- data/examples/pizzas/adapters/payment_webhook.rb +87 -0
- data/examples/pizzas/bluebook/pizzas.behaviors +33 -0
- data/examples/pizzas/bluebook/pizzas.hecksagon +5 -3
- data/examples/pizzas/config.ru +22 -0
- data/exe/hecks +1 -1
- data/lib/hecks/adapters/driven/prism.rb +0 -2
- data/lib/hecks/adapters/driven.rb +0 -2
- data/lib/hecks/adapters/driving/json.rb +9 -16
- data/lib/hecks/bluebook/dsl/bootstrap_table.rb +2 -2
- data/lib/hecks/bluebook/dsl/derivation_words.rb +58 -0
- data/lib/hecks/bluebook/dsl/translation_builder.rb +6 -37
- data/lib/hecks/bluebook/expression/ast_json.rb +0 -9
- data/lib/hecks/bluebook/hecksagon.rb +1 -1
- data/lib/hecks/bluebook/meta_validator/translation_judge/rules.rb +7 -10
- data/lib/hecks/bluebook/meta_validator/translation_judge.rb +4 -8
- data/lib/hecks/bluebook/meta_validator.rb +0 -11
- data/lib/hecks/bluebook/translation.rb +18 -10
- data/lib/hecks/bluebook/translation_expression/evaluator.rb +63 -0
- data/lib/hecks/bluebook/translation_expression/parser.rb +127 -0
- data/lib/hecks/bluebook/translation_expression.rb +39 -0
- data/lib/hecks/bluebook/translation_plan.rb +70 -0
- data/lib/hecks/bluebook.rb +2 -0
- data/lib/hecks/chapters.rb +3 -3
- data/lib/hecks/cli/project_cli/launcher_source.rb +1 -1
- data/lib/hecks/composer/adapters/claude_composer.adapter +3 -0
- data/lib/hecks/composer/adapters/claude_composer.rb +83 -0
- data/lib/hecks/composer/adapters/fake_agent.adapter +3 -0
- data/lib/hecks/composer/adapters/fake_agent.rb +55 -0
- data/lib/hecks/composer/adapters/jev.adapter +3 -0
- data/lib/hecks/composer/adapters/jev.rb +66 -0
- data/lib/hecks/composer/agent.rb +113 -0
- data/lib/hecks/composer/assistant.rb +110 -0
- data/lib/hecks/composer/behavior_run.rb +53 -0
- data/lib/hecks/composer/bluebook/composer.bluebook +178 -0
- data/lib/hecks/composer/bluebook/composer.hecksagon +16 -0
- data/lib/hecks/composer/board.rb +65 -0
- data/lib/hecks/composer/board_edit/command_text.rb +39 -0
- data/lib/hecks/composer/board_edit/source.rb +84 -0
- data/lib/hecks/composer/board_edit.rb +144 -0
- data/lib/hecks/composer/check.rb +89 -0
- data/lib/hecks/composer/domains.rb +81 -0
- data/lib/hecks/composer/jobs.rb +134 -0
- data/lib/hecks/composer/layout.rb +37 -0
- data/lib/hecks/composer/live.rb +150 -0
- data/lib/hecks/composer/outline.rb +48 -0
- data/lib/hecks/composer/page.html +989 -0
- data/lib/hecks/composer/panels.rb +142 -0
- data/lib/hecks/composer/parse.rb +83 -0
- data/lib/hecks/composer/plain.rb +28 -0
- data/lib/hecks/composer/ports/composer_agent.port +7 -0
- data/lib/hecks/composer/session.rb +141 -0
- data/lib/hecks/composer/toolbox.rb +166 -0
- data/lib/hecks/composer/web.rb +156 -0
- data/lib/hecks/composer/workbench.rb +51 -0
- data/lib/hecks/composer/workspace.rb +81 -0
- data/lib/hecks/composer.rb +17 -0
- data/lib/hecks/corpus/adapters.rb +22 -0
- data/lib/hecks/corpus/rust.rb +1 -1
- data/lib/hecks/corpus/worlds.rb +19 -0
- data/lib/hecks/corpus.rb +5 -0
- data/lib/hecks/forms/app/commands.rb +2 -3
- data/lib/hecks/forms/app/queries.rb +2 -3
- data/lib/hecks/forms/app/records.rb +17 -5
- data/lib/hecks/forms/app/responses.rb +3 -1
- data/lib/hecks/forms/command_form_renderer.rb +42 -16
- data/lib/hecks/forms/field_controls.rb +2 -2
- data/lib/hecks/forms/field_renderer.rb +1 -1
- data/lib/hecks/forms/field_shape.rb +36 -17
- data/lib/hecks/forms/index_renderer.rb +2 -1
- data/lib/hecks/forms/page.rb +1 -1
- data/lib/hecks/forms/params.rb +0 -13
- data/lib/hecks/forms/query_form_renderer.rb +7 -6
- data/lib/hecks/forms/record_renderer.rb +11 -7
- data/lib/hecks/forms/rule_words.rb +100 -0
- data/lib/hecks/forms/value_object_shape.rb +2 -1
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +0 -3
- data/lib/hecks/fuzzing/isolated_boot/worlds.rb +3 -6
- data/lib/hecks/fuzzing/replay/filters.rb +2 -6
- data/lib/hecks/gate/stages.yml +25 -0
- data/lib/hecks/grammar/evolve/seeds.rb +0 -6
- data/lib/hecks/grammar/evolve.rb +0 -3
- data/lib/hecks/grammar/translation.bluebook +1 -1
- data/lib/hecks/hecks/adapters/codebase/composing.rb +56 -0
- data/lib/hecks/hecks/adapters/codebase/source_tree.rb +3 -1
- data/lib/hecks/hecks/adapters/first_admin.rb +3 -3
- data/lib/hecks/hecks/adapters/journal_store/changes.rb +0 -26
- data/lib/hecks/hecks/adapters/journal_store/examination.rb +2 -23
- data/lib/hecks/hecks/adapters/journal_store/readings.rb +3 -11
- data/lib/hecks/hecks/adapters/journal_store.rb +0 -11
- data/lib/hecks/hecks/adapters/rehearsal/inquiry.rb +4 -11
- data/lib/hecks/hecks/adapters/rehearsal/memory_store.rb +0 -9
- data/lib/hecks/hecks/adapters/rehearsal/postgres_reading.rb +0 -4
- data/lib/hecks/hecks/codebase.behaviors +6 -0
- data/lib/hecks/hecks/codebase.bluebook +21 -0
- data/lib/hecks/hecks/composer.behaviors +68 -0
- data/lib/hecks/hecks/context_map.hecksagon +1 -1
- data/lib/hecks/hecks/custodian.behaviors +0 -6
- data/lib/hecks/hecks/custodian.bluebook +3 -80
- data/lib/hecks/hecks/hecks.hecksagon +2 -2
- data/lib/hecks/hecks/hecks.world +1 -1
- data/lib/hecks/hecks/rehearsal.bluebook +7 -13
- data/lib/hecks/kernel_capabilities/templates.rb +4 -4
- data/lib/hecks/kernel_capabilities.rb +1 -1
- data/lib/hecks/language/bluebook/vocabulary.bluebook +2 -3
- data/lib/hecks/language/translation/translation_aggregate.bluebook +21 -24
- data/lib/hecks/ports/access_control.rb +2 -2
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +3 -41
- data/lib/hecks/ports/persistence/plugins/era/lineage/coverage.rb +7 -7
- data/lib/hecks/ports/persistence/plugins/era/lineage/rewrite.rb +25 -2
- data/lib/hecks/ports/persistence/plugins/era/lineage/rules.rb +4 -4
- data/lib/hecks/ports/persistence/plugins/era/lineage.rb +17 -25
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/base_sql.rb +0 -12
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/domain_rename.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +2 -22
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +0 -1
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +9 -3
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +3 -29
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/refusals.rb +2 -26
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +6 -33
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +3 -3
- data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +2 -4
- data/lib/hecks/ports/persistence/plugins/era/translation/expression_sql.rb +52 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +23 -29
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/translation.rb +0 -1
- data/lib/hecks/ports/persistence/plugins/era.rb +3 -6
- data/lib/hecks/ports.rb +0 -1
- data/lib/hecks/projection_files.rb +3 -1
- data/lib/hecks/projections/deploy/templates/lambda/nat_comment_oauth.tmpl +1 -1
- data/lib/hecks/projections/glossary/entries.rb +10 -4
- data/lib/hecks/projections/glossary/groups.rb +42 -0
- data/lib/hecks/projections/glossary/html/page.rb +7 -2
- data/lib/hecks/projections/glossary/html/parser.rb +1 -1
- data/lib/hecks/projections/glossary/html/sections.rb +31 -11
- data/lib/hecks/projections/glossary/html.rb +1 -2
- data/lib/hecks/projections/glossary/markdown.rb +31 -28
- data/lib/hecks/projections/glossary/mermaid.rb +3 -0
- data/lib/hecks/projections/glossary/page.css +143 -153
- data/lib/hecks/projections/glossary/page.js +63 -3
- data/lib/hecks/projections/glossary/sentences/actions.rb +88 -0
- data/lib/hecks/projections/glossary/sentences/paths.rb +77 -0
- data/lib/hecks/projections/glossary/sentences/queries.rb +90 -0
- data/lib/hecks/projections/glossary/sentences/value_objects.rb +32 -5
- data/lib/hecks/projections/glossary/sentences.rb +13 -3
- data/lib/hecks/projections/glossary/slugs.rb +4 -1
- data/lib/hecks/projections/glossary.rb +2 -1
- data/lib/hecks/projections/plain_english.rb +97 -0
- data/lib/hecks/projections/rust_vocabulary/arguments.rb +2 -2
- data/lib/hecks/projections/rust_vocabulary/enum_body.rb +20 -25
- data/lib/hecks/projections/rust_vocabulary/tests.rb +2 -2
- data/lib/hecks/projections/rust_vocabulary.rb +14 -6
- data/lib/hecks/projections/site/cms_editor/color.rb +0 -6
- data/lib/hecks/projections/site/cms_editor/templates/src/host.ts.tmpl +7 -2
- data/lib/hecks/projections/statements/actions.rb +99 -0
- data/lib/hecks/projections/statements/relationships.rb +86 -0
- data/lib/hecks/projections/statements.rb +15 -24
- data/lib/hecks/projector/exporter/adapters.rb +112 -0
- data/lib/hecks/projector/exporter/translations.rb +18 -35
- data/lib/hecks/projector/exporter/worlds.rb +103 -0
- data/lib/hecks/projector/exporter.rb +5 -1
- data/lib/hecks/quality_control/adapters/agent.rb +6 -7
- data/lib/hecks/quality_control/adapters/agent_profile.rb +16 -114
- data/lib/hecks/quality_control/cli/qa_mine_combinations.rb +1 -1
- data/lib/hecks/runtime/loader/boot_steps.rb +0 -20
- data/lib/hecks/runtime/outbox.rb +2 -9
- data/lib/hecks/runtime/port_operation_interpreter.rb +1 -9
- data/lib/hecks/runtime/remote_dispatcher.rb +2 -2
- data/lib/hecks/runtime/value/unwrapping.rb +60 -0
- data/lib/hecks/runtime/value.rb +10 -21
- data/lib/hecks/rust_build/project_rust/target_ir.rb +23 -4
- data/lib/hecks/rust_build/wasm.rb +0 -12
- data/lib/hecks/rust_build/write_if_changed.rb +12 -12
- data/lib/hecks/rust_format.rb +77 -0
- data/lib/hecks/tools/fuzz_sweep/findings.rb +3 -9
- data/lib/hecks/tools/fuzz_sweep.rb +1 -3
- data/lib/hecks/tools/translation_audit/verdicts.rb +3 -28
- data/lib/hecks/tools/translation_audit.rb +8 -10
- data/lib/hecks/tools/translation_scaffold/reporting.rb +1 -1
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks/vocabulary.rb +2 -1
- data/lib/hecks.rb +3 -0
- data/prebuilt/{1a6f596be124228be93eba1d86dbc9d2933190fa389b284331241cb9f27115a0.marshal → ad68f9e06c8068c2a4176b57abf3083d591d5d8980c2b3e897b97331f965b643.marshal} +0 -0
- data/prebuilt/{a883a7e568667aa5ac904c82daff356bdf9e996252a36af6def4a4a43efb967e.marshal → ee22ec8642c4006717ba2b80f46e5efb81de459eef9dfec043e838c0ad4fd720.marshal} +0 -0
- data/prebuilt/{verdicts-872993b1aa26d73fe48f28251374b8585fed145d02aab42be0f00de386b6ff80.json → verdicts-b69f5dcbba79c20daa0493476fa7c317004a30aa2bc3d3dc6a722ea70ae2b43b.json} +1 -1
- data/rust/Cargo.toml +40 -0
- data/rust/build/Cargo.toml +36 -0
- data/rust/build/clippy.toml +4 -0
- data/rust/build/src/adapters_pass.rs +196 -0
- data/rust/build/src/binding_pass.rs +54 -13
- data/rust/build/src/build_artifact.rs +23 -5
- data/rust/build/src/cargo_sync.rs +69 -26
- data/rust/build/src/lineage_pass.rs +101 -87
- data/rust/build/src/main.rs +11 -2
- data/rust/build/src/pipeline.rs +80 -19
- data/rust/build/src/reserved_names.rs +6 -50
- data/rust/build/src/resolve.rs +82 -35
- data/rust/build/src/rustfmt.rs +100 -0
- data/rust/build/src/seams_pass.rs +85 -16
- data/rust/build/src/subprocess.rs +30 -6
- data/rust/build/src/tmp.rs +5 -2
- data/rust/build/src/translation_pass.rs +72 -22
- data/rust/build/src/world_pass.rs +109 -0
- data/rust/clippy.toml +4 -0
- data/rust/codegen/Cargo.toml +36 -0
- data/rust/codegen/clippy.toml +4 -0
- data/rust/codegen/src/asks.rs +40 -14
- data/rust/codegen/src/attr.rs +2 -2
- data/rust/codegen/src/bridging.rs +676 -117
- data/rust/codegen/src/commands.rs +979 -373
- data/rust/codegen/src/constraints.rs +88 -25
- data/rust/codegen/src/dependency_planning.rs +128 -34
- data/rust/codegen/src/domain_generator.rs +306 -170
- data/rust/codegen/src/exemplar.rs +87 -32
- data/rust/codegen/src/expr_emitter.rs +54 -6
- data/rust/codegen/src/fielded.rs +193 -41
- data/rust/codegen/src/hecks_naming.rs +124 -16
- data/rust/codegen/src/hecks_naming_tests.rs +81 -0
- data/rust/codegen/src/json.rs +80 -44
- data/rust/codegen/src/json_codec.rs +585 -149
- data/rust/codegen/src/literal.rs +104 -138
- data/rust/codegen/src/main.rs +151 -52
- data/rust/codegen/src/manifest.rs +76 -14
- data/rust/codegen/src/mutations.rs +1212 -239
- data/rust/codegen/src/naming.rs +111 -60
- data/rust/codegen/src/optional_pass.rs +90 -28
- data/rust/codegen/src/ports.rs +5 -4
- data/rust/codegen/src/prelude.rs +139 -49
- data/rust/codegen/src/presenters.rs +335 -0
- data/rust/codegen/src/queries.rs +601 -226
- data/rust/codegen/src/reactions.rs +268 -74
- data/rust/codegen/src/read_models.rs +574 -229
- data/rust/codegen/src/reference_specs.rs +16 -3
- data/rust/codegen/src/registry.rs +128 -79
- data/rust/codegen/src/reserved_names.rs +6 -50
- data/rust/codegen/src/shared.rs +71 -22
- data/rust/codegen/src/sidecars.rs +15 -5
- data/rust/codegen/src/skip_reason.rs +8 -2
- data/rust/codegen/src/types.rs +306 -108
- data/rust/host/Cargo.lock +1 -0
- data/rust/host/Cargo.toml +40 -4
- data/rust/host/HECKS_RELEASE +1 -1
- data/rust/host/clippy.toml +4 -0
- data/rust/host/src/api.rs +698 -218
- data/rust/host/src/auth.rs +491 -214
- data/rust/host/src/b64.rs +91 -0
- data/rust/host/src/bin/expr_harness.rs +6 -2
- data/rust/host/src/bin/lineage_harness.rs +47 -11
- data/rust/host/src/bin/mint_harness.rs +70 -37
- data/rust/host/src/bin/validate_harness.rs +16 -8
- data/rust/host/src/boot.rs +144 -68
- data/rust/host/src/boundary_fuzz/api.rs +130 -32
- data/rust/host/src/boundary_fuzz/auth.rs +108 -23
- data/rust/host/src/boundary_fuzz/expr_json.rs +85 -11
- data/rust/host/src/boundary_fuzz/ir.rs +164 -12
- data/rust/host/src/boundary_fuzz/server.rs +283 -36
- data/rust/host/src/boundary_fuzz/web.rs +56 -8
- data/rust/host/src/checkout.rs +78 -32
- data/rust/host/src/clock.rs +8 -0
- data/rust/host/src/commerce_ir.rs +333 -95
- data/rust/host/src/crypto.rs +33 -0
- data/rust/host/src/dispatch.rs +1037 -331
- data/rust/host/src/expr_json.rs +1021 -273
- data/rust/host/src/extension.rs +24 -4
- data/rust/host/src/field_hints.rs +11 -7
- data/rust/host/src/fuzz_support.rs +85 -13
- data/rust/host/src/host.rs +100 -0
- data/rust/host/src/http.rs +22 -0
- data/rust/host/src/ir.rs +326 -16
- data/rust/host/src/journal.rs +277 -176
- data/rust/host/src/lambda_client.rs +219 -53
- data/rust/host/src/lib.rs +16 -4
- data/rust/host/src/log.rs +62 -11
- data/rust/host/src/mint.rs +1153 -326
- data/rust/host/src/needs.rs +185 -44
- data/rust/host/src/payments/keystore.rs +197 -45
- data/rust/host/src/payments/roles.rs +39 -7
- data/rust/host/src/payments/tests.rs +1427 -282
- data/rust/host/src/payments.rs +391 -112
- data/rust/host/src/platform_signin/attack_tests.rs +5 -5
- data/rust/host/src/platform_signin/config.rs +11 -11
- data/rust/host/src/platform_signin/cookie.rs +2 -2
- data/rust/host/src/platform_signin/extension.rs +2 -1
- data/rust/host/src/platform_signin/handler.rs +9 -8
- data/rust/host/src/platform_signin/jti.rs +4 -2
- data/rust/host/src/platform_signin/jwks.rs +2 -1
- data/rust/host/src/platform_signin/jws.rs +2 -37
- data/rust/host/src/platform_signin/known_answer_tests.rs +5 -1
- data/rust/host/src/platform_signin/mode_tests.rs +5 -3
- data/rust/host/src/platform_signin/pages.rs +20 -20
- data/rust/host/src/platform_signin/session.rs +8 -1
- data/rust/host/src/platform_signin/settings.rs +6 -6
- data/rust/host/src/platform_signin/strict_json.rs +12 -9
- data/rust/host/src/platform_signin/verify.rs +41 -41
- data/rust/host/src/platform_signin/wiring_support.rs +13 -18
- data/rust/host/src/presentation.rs +207 -63
- data/rust/host/src/presentation_write.rs +502 -187
- data/rust/host/src/query_step.rs +69 -18
- data/rust/host/src/rate_limit.rs +541 -142
- data/rust/host/src/reference_transform.rs +77 -25
- data/rust/host/src/reference_validate.rs +443 -104
- data/rust/host/src/resend.rs +190 -55
- data/rust/host/src/secrets.rs +116 -21
- data/rust/host/src/server.rs +555 -104
- data/rust/host/src/storage_shape.rs +88 -21
- data/rust/host/src/test_pg.rs +17 -7
- data/rust/host/src/ui_schema.rs +495 -207
- data/rust/host/src/wasm_runner.rs +21 -6
- data/rust/host/src/web/commerce.rs +101 -23
- data/rust/host/src/web/newsletter/cooldown.rs +10 -3
- data/rust/host/src/web/newsletter/template.rs +26 -7
- data/rust/host/src/web/newsletter/tests.rs +112 -23
- data/rust/host/src/web/newsletter.rs +209 -59
- data/rust/host/src/web/newsletter_send.rs +251 -79
- data/rust/host/src/web/registration_receipt/tests.rs +16 -4
- data/rust/host/src/web/registration_receipt.rs +100 -24
- data/rust/host/src/web/registrations.rs +1384 -332
- data/rust/host/src/web/resend_webhook.rs +138 -80
- data/rust/host/src/web.rs +2416 -508
- data/rust/lsp/Cargo.toml +36 -0
- data/rust/lsp/clippy.toml +4 -0
- data/rust/lsp/src/json.rs +29 -22
- data/rust/lsp/src/main.rs +81 -31
- data/rust/lsp/src/outline.rs +23 -7
- data/rust/lsp/src/rpc.rs +17 -4
- data/rust/parser/Cargo.toml +36 -0
- data/rust/parser/clippy.toml +4 -0
- data/rust/parser/src/build/closed_sets.rs +6 -1
- data/rust/parser/src/build/identity.rs +4 -3
- data/rust/parser/src/build/mod.rs +1 -1
- data/rust/parser/src/build/named_pattern.rs +4 -1
- data/rust/parser/src/build/naming.rs +2 -80
- data/rust/parser/src/build/pattern_subset.rs +6 -8
- data/rust/parser/src/build/query_derive.rs +3 -1
- data/rust/parser/src/build/query_inference.rs +14 -5
- data/rust/parser/src/build/read_model.rs +4 -1
- data/rust/parser/src/build/references.rs +4 -3
- data/rust/parser/src/canonical.rs +34 -15
- data/rust/parser/src/emit.rs +187 -105
- data/rust/parser/src/expr/ast_json.rs +134 -42
- data/rust/parser/src/expr/evaluator.rs +27 -9
- data/rust/parser/src/expr/mod.rs +54 -13
- data/rust/parser/src/expr/resolver.rs +299 -92
- data/rust/parser/src/ir.rs +35 -3
- data/rust/parser/src/keywords.rs +7168 -516
- data/rust/parser/src/lex.rs +11 -25
- data/rust/parser/src/lib.rs +1 -0
- data/rust/parser/src/main.rs +65 -6
- data/rust/parser/src/parse/adapter.rs +157 -0
- data/rust/parser/src/parse/aggregate.rs +19 -10
- data/rust/parser/src/parse/chapter.rs +16 -17
- data/rust/parser/src/parse/command.rs +35 -16
- data/rust/parser/src/parse/domain_port.rs +4 -7
- data/rust/parser/src/parse/entity.rs +21 -11
- data/rust/parser/src/parse/hecksagon.rs +10 -4
- data/rust/parser/src/parse/lifecycle.rs +9 -5
- data/rust/parser/src/parse/mod.rs +22 -91
- data/rust/parser/src/parse/needs.rs +21 -12
- data/rust/parser/src/parse/policy.rs +2 -2
- data/rust/parser/src/parse/process_manager.rs +9 -11
- data/rust/parser/src/parse/query.rs +8 -4
- data/rust/parser/src/parse/read_model.rs +57 -16
- data/rust/parser/src/parse/value_object.rs +7 -2
- data/rust/parser/src/parse/world.rs +322 -0
- data/rust/parser/src/ruby_value.rs +7 -6
- data/rust/src/exemplar/json.rs +4 -4
- data/rust/src/exemplar/mutations.rs +18 -0
- data/rust/src/exemplar/read_models.rs +1 -10
- data/rust/src/kernel/attribute_shapes/composite.rs +54 -14
- data/rust/src/kernel/attribute_shapes/list.rs +2 -2
- data/rust/src/kernel/attribute_shapes/mod.rs +8 -8
- data/rust/src/kernel/attribute_shapes/scalar.rs +2 -2
- data/rust/src/kernel/cli.rs +244 -114
- data/rust/src/kernel/dispatch.rs +323 -115
- data/rust/src/kernel/dispatch_guard_tests.rs +327 -65
- data/rust/src/kernel/expr.rs +203 -86
- data/rust/src/kernel/expression_operators/arithmetic.rs +138 -25
- data/rust/src/kernel/expression_operators/comparison.rs +87 -2
- data/rust/src/kernel/expression_operators/enumeration.rs +106 -22
- data/rust/src/kernel/expression_operators/logical.rs +9 -3
- data/rust/src/kernel/expression_operators/membership.rs +3 -1
- data/rust/src/kernel/expression_operators/mod.rs +34 -21
- data/rust/src/kernel/expression_operators/pattern_match.rs +46 -11
- data/rust/src/kernel/expression_operators/positional.rs +84 -30
- data/rust/src/kernel/expression_operators/presence.rs +52 -13
- data/rust/src/kernel/expression_operators/sign_test.rs +3 -1
- data/rust/src/kernel/expression_operators/sized.rs +24 -8
- data/rust/src/kernel/expression_operators/text.rs +120 -27
- data/rust/src/kernel/expression_operators/to_string.rs +10 -4
- data/rust/src/kernel/json.rs +244 -148
- data/rust/src/kernel/mod.rs +47 -33
- data/rust/src/kernel/named_query.rs +63 -15
- data/rust/src/kernel/needs.rs +112 -25
- data/rust/src/kernel/orchestrate.rs +980 -581
- data/rust/src/kernel/orchestrate_guard_tests.rs +538 -140
- data/rust/src/kernel/pattern.rs +107 -34
- data/rust/src/kernel/present.rs +261 -0
- data/rust/src/kernel/query_comparators.rs +118 -41
- data/rust/src/kernel/query_ordering.rs +13 -5
- data/rust/src/kernel/read_model.rs +479 -181
- data/rust/src/kernel/reference_lookup.rs +122 -26
- data/rust/src/kernel/repository.rs +293 -66
- data/rust/src/kernel/routing.rs +58 -42
- data/rust/src/kernel/ruby_float.rs +139 -0
- data/rust/src/kernel/vocab/aggregate_dispatch_order.rs +57 -57
- data/rust/src/kernel/vocab/entity_dispatch_order.rs +54 -54
- data/rust/src/kernel/vocab/field_hint.rs +19 -25
- data/rust/src/kernel/vocab/mod.rs +6 -6
- data/rust/src/kernel/vocab/query_comparator.rs +23 -24
- data/rust/src/kernel/vocab/refusal_template.rs +501 -297
- data/rust/src/lib.rs +50 -0
- data/rust/src/main.rs +4 -1
- data/rust/web/Cargo.toml +36 -0
- data/rust/web/clippy.toml +4 -0
- metadata +85 -15
- data/examples/pizzas/bluebook/hecksagon/mock_stripe_payment_adapter.rb +0 -36
- data/lib/hecks/adapters/driven/google_authentication.adapter +0 -3
- data/lib/hecks/adapters/driven/google_authentication.rb +0 -99
- data/lib/hecks/adapters/driven/mock_stripe_adapter.adapter +0 -3
- data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +0 -32
- data/lib/hecks/ports/authentication.port +0 -6
- data/lib/hecks/ports/authentication.rb +0 -67
- data/lib/hecks/ports/persistence/plugins/era/translation/approval_file.rb +0 -255
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +0 -22
- data/rust/host/src/approval.rs +0 -652
data/rust/host/src/payments.rs
CHANGED
|
@@ -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::
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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 =
|
|
94
|
-
let webhook_base_url = Some(var("PAYMENTS_WEBHOOK_BASE_URL"))
|
|
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(
|
|
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 {
|
|
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!(
|
|
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]
|
|
142
|
-
|
|
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
|
|
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
|
-
|
|
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() =>
|
|
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
|
|
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(
|
|
207
|
-
|
|
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
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
258
|
-
wasm_path: &Path,
|
|
259
|
-
config: &LineageConfig,
|
|
260
|
-
invoker: &dyn LambdaInvoker,
|
|
313
|
+
host: Host<'_>,
|
|
261
314
|
) -> anyhow::Result<dispatch::Outcome> {
|
|
262
|
-
let
|
|
263
|
-
|
|
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
|
-
| (
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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"]
|
|
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(
|
|
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
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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(
|
|
317
|
-
|
|
318
|
-
|
|
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.
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
380
|
-
"/payments/connection/disconnect" => disconnect_route(&caller, platform,
|
|
381
|
-
"/payments/connection/enable" =>
|
|
382
|
-
|
|
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(
|
|
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)
|
|
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
|
-
|
|
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(
|
|
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 =>
|
|
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
|
-
|
|
439
|
-
|
|
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,
|
|
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(
|
|
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,
|
|
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,
|
|
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"))
|
|
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" {
|
|
485
|
-
|
|
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(
|
|
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!({}),
|
|
509
|
-
Ok(outcome) if outcome.accepted =>
|
|
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()
|
|
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()
|
|
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 {
|
|
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
|
|
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
|
|
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] = [
|
|
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
|
|
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
|
|
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(
|
|
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![
|
|
625
|
-
|
|
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|
|
|
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 {
|
|
655
|
-
|
|
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
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
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
|
|
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 {
|
|
701
|
-
|
|
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,
|
|
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
|
|
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 {
|
|
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()
|
|
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 {
|
|
1056
|
+
PlatformConfig {
|
|
1057
|
+
webhook_secret: Some(secret.to_string()),
|
|
1058
|
+
..test_platform()
|
|
1059
|
+
}
|
|
781
1060
|
}
|
|
782
1061
|
|
|
783
1062
|
#[cfg(test)]
|