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.
- 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.bluebook +4 -4
- data/examples/pizzas/bluebook/pizzas.hecksagon +5 -3
- data/examples/pizzas/config.ru +22 -0
- data/lib/hecks/adapters/driven/prism.rb +0 -2
- data/lib/hecks/adapters/driven.rb +0 -2
- data/lib/hecks/adapters/driving/json.rb +1 -7
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +15 -5
- 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/named_pattern.rb +33 -0
- 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 +3 -0
- data/lib/hecks/chapters.rb +3 -3
- data/lib/hecks/cli/domain_stub.rb +1 -1
- data/lib/hecks/cli/interview_aggregate.rb +4 -4
- 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/deploy/bluebook/deploy.bluebook +34 -34
- 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/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/framework/bluebook/change.bluebook +2 -2
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +0 -3
- data/lib/hecks/fuzzing/domain_generator/block_rendering.rb +1 -1
- 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/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 +58 -37
- 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 +15 -92
- data/lib/hecks/hecks/hecks.bluebook +2 -2
- data/lib/hecks/hecks/hecks.hecksagon +2 -2
- data/lib/hecks/hecks/hecks.world +1 -1
- data/lib/hecks/hecks/rehearsal.bluebook +10 -16
- data/lib/hecks/hecks/tooling.bluebook +22 -22
- data/lib/hecks/kernel_capabilities/templates.rb +4 -4
- data/lib/hecks/kernel_capabilities.rb +1 -1
- data/lib/hecks/language/bluebook/aggregate.bluebook +1 -0
- data/lib/hecks/language/bluebook/command.bluebook +1 -0
- data/lib/hecks/language/bluebook/entity.bluebook +1 -0
- data/lib/hecks/language/bluebook/query.bluebook +1 -0
- data/lib/hecks/language/bluebook/shape.bluebook +8 -7
- data/lib/hecks/language/bluebook/vocabulary.bluebook +2 -3
- data/lib/hecks/language/hecksagon/port_operation.bluebook +1 -0
- 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/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/quality_control/quality_control.bluebook +51 -51
- 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.rb +13 -0
- 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/site/bluebook/site.bluebook +5 -5
- data/lib/hecks/sme/bluebook/sme.bluebook +12 -12
- data/lib/hecks/tickets/bluebook/tickets.bluebook +3 -3
- 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/{58208399bada10b0db99928c48e38789288fc58f7b71a72b21bfa386680e32ed.marshal → 02c09c73b272c1fb5a9ae6d87bb5e5e529208e00f6b4b597dc2c0fd70a9c0964.marshal} +0 -0
- data/prebuilt/{2b59737fbdbd5f44edda0fddce2611f45d76087b3a7c782d1f1a17ea58c77948.marshal → bb84a835cf6a15136bad5ca03aa5bf93a5b92f5f72444618d3ef2412b987fb32.marshal} +0 -0
- data/prebuilt/verdicts-f1f10da14c8e8f01b4c9b7e3e2d74be8cd2bf4676364ed17fa16975d2a076d75.json +1 -0
- 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 +300 -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 +145 -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/queries.rs +601 -226
- data/rust/codegen/src/reactions.rs +268 -74
- data/rust/codegen/src/read_models.rs +606 -205
- 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 +990 -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 +191 -0
- data/rust/host/src/payments/tests.rs +1445 -275
- data/rust/host/src/payments.rs +409 -121
- 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 +483 -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 -0
- data/rust/parser/src/build/named_pattern.rs +26 -0
- 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 -510
- 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 +43 -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 +229 -112
- 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 +230 -148
- data/rust/src/kernel/mod.rs +46 -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/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/prebuilt/verdicts-48318ca172edd21443b1ad02801c697fff100b746e828f09db987647641a5df2.json +0 -1
- 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;
|
|
@@ -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
|
|
69
|
-
/// names no secret; off AWS, or with
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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 =
|
|
87
|
-
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();
|
|
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(
|
|
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 {
|
|
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!(
|
|
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]
|
|
134
|
-
|
|
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
|
|
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
|
-
|
|
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() =>
|
|
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
|
|
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(
|
|
199
|
-
|
|
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
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
250
|
-
wasm_path: &Path,
|
|
251
|
-
config: &LineageConfig,
|
|
252
|
-
invoker: &dyn LambdaInvoker,
|
|
313
|
+
host: Host<'_>,
|
|
253
314
|
) -> anyhow::Result<dispatch::Outcome> {
|
|
254
|
-
let
|
|
255
|
-
|
|
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
|
-
| (
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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"]
|
|
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
|
-
|
|
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(
|
|
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
|
|
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 {
|
|
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
|
|
298
|
-
if caller.
|
|
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 {
|
|
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(
|
|
309
|
-
|
|
310
|
-
|
|
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.
|
|
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.
|
|
425
|
+
"can_manage": caller.manager,
|
|
326
426
|
"can_enable": caller.operator,
|
|
327
427
|
})
|
|
328
428
|
}
|
|
329
429
|
|
|
330
|
-
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> {
|
|
331
435
|
match dispatch::read(client, wasm_path).await {
|
|
332
436
|
Ok(read) => Ok(connection(&read, &config.domain)),
|
|
333
|
-
Err(e) => Err(json_error(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
372
|
-
"/payments/connection/disconnect" => disconnect_route(&caller, platform,
|
|
373
|
-
"/payments/connection/enable" =>
|
|
374
|
-
|
|
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
|
|
379
|
-
async fn show(
|
|
380
|
-
|
|
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)
|
|
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
|
-
|
|
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(
|
|
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 =>
|
|
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
|
-
|
|
431
|
-
|
|
432
|
-
|
|
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,
|
|
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(
|
|
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,
|
|
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,
|
|
465
|
-
|
|
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"))
|
|
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" {
|
|
477
|
-
|
|
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(
|
|
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!({}),
|
|
501
|
-
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
|
+
}
|
|
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()
|
|
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()
|
|
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 {
|
|
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
|
|
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
|
|
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] = [
|
|
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
|
|
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
|
|
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(
|
|
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![
|
|
617
|
-
|
|
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|
|
|
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 {
|
|
647
|
-
|
|
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
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
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
|
|
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 {
|
|
693
|
-
|
|
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,
|
|
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
|
|
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 {
|
|
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()
|
|
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 {
|
|
1056
|
+
PlatformConfig {
|
|
1057
|
+
webhook_secret: Some(secret.to_string()),
|
|
1058
|
+
..test_platform()
|
|
1059
|
+
}
|
|
772
1060
|
}
|
|
773
1061
|
|
|
774
1062
|
#[cfg(test)]
|