hecks 3.14.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/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/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/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/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/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/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.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/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/{a883a7e568667aa5ac904c82daff356bdf9e996252a36af6def4a4a43efb967e.marshal → 02c09c73b272c1fb5a9ae6d87bb5e5e529208e00f6b4b597dc2c0fd70a9c0964.marshal} +0 -0
- data/prebuilt/{1a6f596be124228be93eba1d86dbc9d2933190fa389b284331241cb9f27115a0.marshal → bb84a835cf6a15136bad5ca03aa5bf93a5b92f5f72444618d3ef2412b987fb32.marshal} +0 -0
- data/prebuilt/{verdicts-872993b1aa26d73fe48f28251374b8585fed145d02aab42be0f00de386b6ff80.json → verdicts-f1f10da14c8e8f01b4c9b7e3e2d74be8cd2bf4676364ed17fa16975d2a076d75.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 +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 +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 +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 -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 +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 +82 -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
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
use super::newsletter_send::{
|
|
2
|
-
use super::{instances_for, last_refusal, respond};
|
|
1
|
+
use super::newsletter_send::{unsubscribe_token, unsubscribe_token_matches, unsubscribe_url};
|
|
2
|
+
use super::{instances_for, last_refusal, respond, site_url};
|
|
3
3
|
use crate::auth;
|
|
4
|
+
use crate::commerce_ir::{newsletter_provider, NewsletterProvider};
|
|
4
5
|
use crate::dispatch;
|
|
6
|
+
use crate::host::Host;
|
|
5
7
|
use crate::ir::ir;
|
|
6
|
-
use crate::commerce_ir::{newsletter_provider, NewsletterProvider};
|
|
7
|
-
use crate::journal::LineageConfig;
|
|
8
|
-
use crate::lambda_client::LambdaInvoker;
|
|
9
8
|
use crate::resend::{Email, Mailer};
|
|
10
9
|
use serde_json::{json, Value};
|
|
11
10
|
use std::collections::HashMap;
|
|
@@ -26,20 +25,28 @@ pub(super) async fn newsletter_route(
|
|
|
26
25
|
path: &str,
|
|
27
26
|
query: &HashMap<String, String>,
|
|
28
27
|
raw_body: &str,
|
|
29
|
-
|
|
30
|
-
wasm_path: &Path,
|
|
31
|
-
config: &LineageConfig,
|
|
32
|
-
invoker: &dyn LambdaInvoker,
|
|
28
|
+
host: Host<'_>,
|
|
33
29
|
) -> Option<Value> {
|
|
30
|
+
let Host {
|
|
31
|
+
client, wasm_path, ..
|
|
32
|
+
} = host;
|
|
34
33
|
// The chapter that declares `provides "newsletter"` names the verbs
|
|
35
34
|
// and the subscribing aggregate; a domain that attaches none serves
|
|
36
35
|
// none of these routes.
|
|
37
36
|
let provider = ir().and_then(newsletter_provider)?;
|
|
38
37
|
match (method, path) {
|
|
39
|
-
("POST", "/newsletter/subscribers") =>
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
("GET", "/newsletter/subscribers
|
|
38
|
+
("POST", "/newsletter/subscribers") => {
|
|
39
|
+
Some(newsletter_subscribe_route(&provider, raw_body, host).await)
|
|
40
|
+
}
|
|
41
|
+
("GET", "/newsletter/subscribers") => {
|
|
42
|
+
Some(newsletter_subscribers_list_route(&provider, client, wasm_path).await)
|
|
43
|
+
}
|
|
44
|
+
("GET", "/newsletter/subscribers/confirm") => {
|
|
45
|
+
Some(newsletter_confirm_route(&provider, query, host).await)
|
|
46
|
+
}
|
|
47
|
+
("GET", "/newsletter/subscribers/unsubscribe") => {
|
|
48
|
+
Some(newsletter_unsubscribe_route(&provider, query, host).await)
|
|
49
|
+
}
|
|
43
50
|
_ => None,
|
|
44
51
|
}
|
|
45
52
|
}
|
|
@@ -49,31 +56,51 @@ pub(super) async fn newsletter_route(
|
|
|
49
56
|
async fn newsletter_subscribe_route(
|
|
50
57
|
provider: &NewsletterProvider,
|
|
51
58
|
raw_body: &str,
|
|
52
|
-
|
|
53
|
-
wasm_path: &Path,
|
|
54
|
-
config: &LineageConfig,
|
|
55
|
-
invoker: &dyn LambdaInvoker,
|
|
59
|
+
host: Host<'_>,
|
|
56
60
|
) -> Value {
|
|
61
|
+
let Host {
|
|
62
|
+
client, wasm_path, ..
|
|
63
|
+
} = host;
|
|
57
64
|
let body: Value = match serde_json::from_str(raw_body) {
|
|
58
65
|
Ok(v) => v,
|
|
59
|
-
Err(e) =>
|
|
66
|
+
Err(e) => {
|
|
67
|
+
return respond(
|
|
68
|
+
400,
|
|
69
|
+
"application/json",
|
|
70
|
+
&json!({"error": format!("invalid JSON: {e}")}).to_string(),
|
|
71
|
+
)
|
|
72
|
+
}
|
|
60
73
|
};
|
|
61
74
|
let Some(email) = body.get("email").and_then(|v| v.as_str()) else {
|
|
62
|
-
return respond(
|
|
75
|
+
return respond(
|
|
76
|
+
400,
|
|
77
|
+
"application/json",
|
|
78
|
+
&json!({"error": "email"}).to_string(),
|
|
79
|
+
);
|
|
63
80
|
};
|
|
64
|
-
let first_name = body
|
|
65
|
-
|
|
81
|
+
let first_name = body
|
|
82
|
+
.get("first_name")
|
|
83
|
+
.and_then(|v| v.as_str())
|
|
84
|
+
.filter(|s| !s.is_empty());
|
|
85
|
+
let last_name = body
|
|
86
|
+
.get("last_name")
|
|
87
|
+
.and_then(|v| v.as_str())
|
|
88
|
+
.filter(|s| !s.is_empty());
|
|
66
89
|
|
|
67
90
|
let read = match dispatch::read(client, wasm_path).await {
|
|
68
91
|
Ok(r) => r,
|
|
69
92
|
Err(e) => return respond(500, "text/plain", &format!("{e:#}")),
|
|
70
93
|
};
|
|
71
|
-
let existing = instances_for(&read, &provider.instance_prefix())
|
|
94
|
+
let existing = instances_for(&read, &provider.instance_prefix())
|
|
95
|
+
.iter()
|
|
96
|
+
.any(|(id, _)| id == email);
|
|
72
97
|
|
|
73
98
|
if existing {
|
|
74
99
|
if let (Some(first), Some(last)) = (first_name, last_name) {
|
|
75
100
|
let facts = json!({"first_name": {"value": first}, "last_name": {"value": last}});
|
|
76
|
-
if let Err(e) =
|
|
101
|
+
if let Err(e) =
|
|
102
|
+
dispatch::handle_routed(host, &provider.add_name, json!(email), facts, None).await
|
|
103
|
+
{
|
|
77
104
|
return respond(500, "text/plain", &format!("{e:#}"));
|
|
78
105
|
}
|
|
79
106
|
}
|
|
@@ -85,12 +112,16 @@ async fn newsletter_subscribe_route(
|
|
|
85
112
|
if let Some(last) = last_name {
|
|
86
113
|
facts["last_name"] = json!({"value": last});
|
|
87
114
|
}
|
|
88
|
-
let outcome = match dispatch::handle_facts(
|
|
115
|
+
let outcome = match dispatch::handle_facts(host, &provider.subscribe, facts, None).await {
|
|
89
116
|
Ok(o) => o,
|
|
90
117
|
Err(e) => return respond(500, "text/plain", &format!("{e:#}")),
|
|
91
118
|
};
|
|
92
119
|
if !outcome.accepted {
|
|
93
|
-
return respond(
|
|
120
|
+
return respond(
|
|
121
|
+
422,
|
|
122
|
+
"application/json",
|
|
123
|
+
&last_refusal(&outcome.result).to_string(),
|
|
124
|
+
);
|
|
94
125
|
}
|
|
95
126
|
send_confirmation(email).await;
|
|
96
127
|
}
|
|
@@ -104,16 +135,31 @@ async fn newsletter_subscribe_route(
|
|
|
104
135
|
};
|
|
105
136
|
let subscribers = instances_for(&read, &provider.instance_prefix());
|
|
106
137
|
let Some((_, subscriber)) = subscribers.iter().find(|(id, _)| id == email) else {
|
|
107
|
-
return respond(
|
|
138
|
+
return respond(
|
|
139
|
+
500,
|
|
140
|
+
"text/plain",
|
|
141
|
+
"subscriber vanished immediately after being written",
|
|
142
|
+
);
|
|
108
143
|
};
|
|
109
|
-
let status = subscriber
|
|
110
|
-
|
|
144
|
+
let status = subscriber
|
|
145
|
+
.get("status")
|
|
146
|
+
.and_then(|v| v.as_str())
|
|
147
|
+
.unwrap_or_else(|| provider.initial_status());
|
|
148
|
+
respond(
|
|
149
|
+
200,
|
|
150
|
+
"application/json",
|
|
151
|
+
&json!({"email": email, "status": status}).to_string(),
|
|
152
|
+
)
|
|
111
153
|
}
|
|
112
154
|
|
|
113
155
|
// Reads directly off `instances`, not `dispatch::query`: Subscriber lives
|
|
114
156
|
// under the Newsletter chapter, not `config.domain`, so the qualified
|
|
115
157
|
// question name would not resolve there.
|
|
116
|
-
async fn newsletter_subscribers_list_route(
|
|
158
|
+
async fn newsletter_subscribers_list_route(
|
|
159
|
+
provider: &NewsletterProvider,
|
|
160
|
+
client: &Mutex<Client>,
|
|
161
|
+
wasm_path: &Path,
|
|
162
|
+
) -> Value {
|
|
117
163
|
let read = match dispatch::read(client, wasm_path).await {
|
|
118
164
|
Ok(r) => r,
|
|
119
165
|
Err(e) => return respond(500, "text/plain", &format!("{e:#}")),
|
|
@@ -129,7 +175,12 @@ async fn newsletter_subscribers_list_route(provider: &NewsletterProvider, client
|
|
|
129
175
|
})
|
|
130
176
|
})
|
|
131
177
|
.collect();
|
|
132
|
-
subscribers.sort_by(|a, b|
|
|
178
|
+
subscribers.sort_by(|a, b| {
|
|
179
|
+
a["email"]
|
|
180
|
+
.as_str()
|
|
181
|
+
.unwrap_or("")
|
|
182
|
+
.cmp(b["email"].as_str().unwrap_or(""))
|
|
183
|
+
});
|
|
133
184
|
respond(200, "application/json", &json!(subscribers).to_string())
|
|
134
185
|
}
|
|
135
186
|
|
|
@@ -137,30 +188,50 @@ const CONFIRM_PURPOSE: &str = "newsletter-confirm";
|
|
|
137
188
|
|
|
138
189
|
// SESSION_SECRET; unset means no confirm link can be signed and none is sent.
|
|
139
190
|
fn confirm_secret() -> Option<String> {
|
|
140
|
-
|
|
191
|
+
crate::secrets::var("SESSION_SECRET")
|
|
192
|
+
.ok()
|
|
193
|
+
.filter(|s| !s.is_empty())
|
|
141
194
|
}
|
|
142
195
|
|
|
143
196
|
fn confirm_token(secret: &str, email: &str) -> String {
|
|
144
|
-
auth::purpose_token(
|
|
197
|
+
auth::purpose_token(
|
|
198
|
+
secret,
|
|
199
|
+
CONFIRM_PURPOSE,
|
|
200
|
+
json!({ "email": email }),
|
|
201
|
+
crate::commerce_ir::newsletter_windows_binding().0,
|
|
202
|
+
)
|
|
145
203
|
}
|
|
146
204
|
|
|
147
205
|
fn confirm_token_matches(secret: &str, token: &str, email: &str) -> bool {
|
|
148
206
|
auth::verify_purpose_token(secret, CONFIRM_PURPOSE, token)
|
|
149
|
-
.and_then(|claims|
|
|
207
|
+
.and_then(|claims| {
|
|
208
|
+
claims
|
|
209
|
+
.get("email")
|
|
210
|
+
.and_then(|v| v.as_str())
|
|
211
|
+
.map(|signed| signed == email)
|
|
212
|
+
})
|
|
150
213
|
.unwrap_or(false)
|
|
151
214
|
}
|
|
152
215
|
|
|
153
216
|
// Percent-encodes the address so a '+' in the local part survives the URL.
|
|
154
217
|
fn confirm_url(site_url: &str, email: &str, token: &str) -> String {
|
|
155
|
-
|
|
156
|
-
url
|
|
218
|
+
// An unparseable site URL degrades to a placeholder host rather than failing the send.
|
|
219
|
+
let Ok(mut url) = reqwest::Url::parse(&format!("{site_url}/newsletter-confirmed.html"))
|
|
220
|
+
.or_else(|_| reqwest::Url::parse("http://invalid.invalid/"))
|
|
221
|
+
else {
|
|
222
|
+
return String::new();
|
|
223
|
+
};
|
|
224
|
+
url.query_pairs_mut()
|
|
225
|
+
.append_pair("email", email)
|
|
226
|
+
.append_pair("token", token);
|
|
157
227
|
url.to_string()
|
|
158
228
|
}
|
|
159
229
|
|
|
160
230
|
// Per-process only (not shared across instances); see `ConfirmationCooldown`.
|
|
161
231
|
fn confirmation_cooldown() -> &'static cooldown::ConfirmationCooldown {
|
|
162
232
|
static COOLDOWN: OnceLock<cooldown::ConfirmationCooldown> = OnceLock::new();
|
|
163
|
-
COOLDOWN
|
|
233
|
+
COOLDOWN
|
|
234
|
+
.get_or_init(|| cooldown::ConfirmationCooldown::new(cooldown::WINDOW, cooldown::CAPACITY))
|
|
164
235
|
}
|
|
165
236
|
|
|
166
237
|
fn confirmation_body(confirm_url: &str) -> String {
|
|
@@ -195,21 +266,32 @@ pub(super) async fn send_confirmation(email: &str) {
|
|
|
195
266
|
let site = site_url();
|
|
196
267
|
let link = confirm_url(&site, email, &confirm_token(&secret, email));
|
|
197
268
|
let unsubscribe = unsubscribe_url(&site, email, &unsubscribe_token(&secret, email));
|
|
198
|
-
let body =
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
};
|
|
269
|
+
let body = template::branded_body(&link, &unsubscribe)
|
|
270
|
+
.await
|
|
271
|
+
.unwrap_or_else(|| confirmation_body(&link));
|
|
202
272
|
let delivery = mailer
|
|
203
|
-
.deliver(&Email {
|
|
273
|
+
.deliver(&Email {
|
|
274
|
+
to: email,
|
|
275
|
+
subject: "Confirm your newsletter subscription",
|
|
276
|
+
body: &body,
|
|
277
|
+
unsubscribe_url: Some(&unsubscribe),
|
|
278
|
+
})
|
|
204
279
|
.await;
|
|
205
280
|
if !delivery.ok {
|
|
206
|
-
eprintln!(
|
|
281
|
+
eprintln!(
|
|
282
|
+
"newsletter: the confirmation email was not delivered: {}",
|
|
283
|
+
delivery.reason.unwrap_or_default()
|
|
284
|
+
);
|
|
207
285
|
}
|
|
208
286
|
}
|
|
209
287
|
|
|
210
288
|
// Only mails when the address is now `pending`; an already-confirmed or
|
|
211
289
|
// unsubscribed registrant is left alone.
|
|
212
|
-
pub(super) async fn send_confirmation_if_pending(
|
|
290
|
+
pub(super) async fn send_confirmation_if_pending(
|
|
291
|
+
email: &str,
|
|
292
|
+
client: &Mutex<Client>,
|
|
293
|
+
wasm_path: &Path,
|
|
294
|
+
) {
|
|
213
295
|
let Some(provider) = ir().and_then(newsletter_provider) else {
|
|
214
296
|
return;
|
|
215
297
|
};
|
|
@@ -218,7 +300,11 @@ pub(super) async fn send_confirmation_if_pending(email: &str, client: &Mutex<Cli
|
|
|
218
300
|
};
|
|
219
301
|
let pending = instances_for(&read, &provider.instance_prefix())
|
|
220
302
|
.iter()
|
|
221
|
-
.any(|(id, subscriber)|
|
|
303
|
+
.any(|(id, subscriber)| {
|
|
304
|
+
id == email
|
|
305
|
+
&& provider
|
|
306
|
+
.is_awaiting_confirmation(subscriber.get("status").and_then(|v| v.as_str()))
|
|
307
|
+
});
|
|
222
308
|
if pending {
|
|
223
309
|
send_confirmation(email).await;
|
|
224
310
|
}
|
|
@@ -228,16 +314,31 @@ pub(super) async fn send_confirmation_if_pending(email: &str, client: &Mutex<Cli
|
|
|
228
314
|
// a missing, wrong or expired one refuses before any subscriber is touched.
|
|
229
315
|
// Idempotent: dispatches Confirm only while still `pending`, so a double
|
|
230
316
|
// click or link-prefetch lands on the same success response, not a 422.
|
|
231
|
-
async fn newsletter_confirm_route(
|
|
317
|
+
async fn newsletter_confirm_route(
|
|
318
|
+
provider: &NewsletterProvider,
|
|
319
|
+
query: &HashMap<String, String>,
|
|
320
|
+
host: Host<'_>,
|
|
321
|
+
) -> Value {
|
|
322
|
+
let Host {
|
|
323
|
+
client, wasm_path, ..
|
|
324
|
+
} = host;
|
|
232
325
|
let Some(email) = query.get("email") else {
|
|
233
|
-
return respond(
|
|
326
|
+
return respond(
|
|
327
|
+
400,
|
|
328
|
+
"application/json",
|
|
329
|
+
&json!({"error": "email"}).to_string(),
|
|
330
|
+
);
|
|
234
331
|
};
|
|
235
332
|
let signed = match (confirm_secret(), query.get("token")) {
|
|
236
333
|
(Some(secret), Some(token)) => confirm_token_matches(&secret, token, email),
|
|
237
334
|
_ => false,
|
|
238
335
|
};
|
|
239
336
|
if !signed {
|
|
240
|
-
return respond(
|
|
337
|
+
return respond(
|
|
338
|
+
403,
|
|
339
|
+
"application/json",
|
|
340
|
+
&json!({"error": "this confirmation link is invalid or has expired"}).to_string(),
|
|
341
|
+
);
|
|
241
342
|
}
|
|
242
343
|
|
|
243
344
|
let read = match dispatch::read(client, wasm_path).await {
|
|
@@ -246,11 +347,17 @@ async fn newsletter_confirm_route(provider: &NewsletterProvider, query: &HashMap
|
|
|
246
347
|
};
|
|
247
348
|
let subscribers = instances_for(&read, &provider.instance_prefix());
|
|
248
349
|
let Some((_, subscriber)) = subscribers.iter().find(|(id, _)| id == email) else {
|
|
249
|
-
return respond(
|
|
350
|
+
return respond(
|
|
351
|
+
404,
|
|
352
|
+
"application/json",
|
|
353
|
+
&json!({"error": "no such subscriber"}).to_string(),
|
|
354
|
+
);
|
|
250
355
|
};
|
|
251
356
|
|
|
252
357
|
if provider.is_awaiting_confirmation(subscriber.get("status").and_then(|v| v.as_str())) {
|
|
253
|
-
if let Err(e) =
|
|
358
|
+
if let Err(e) =
|
|
359
|
+
dispatch::handle_routed(host, &provider.confirm, json!(email), json!({}), None).await
|
|
360
|
+
{
|
|
254
361
|
return respond(500, "text/plain", &format!("{e:#}"));
|
|
255
362
|
}
|
|
256
363
|
}
|
|
@@ -260,15 +367,31 @@ async fn newsletter_confirm_route(provider: &NewsletterProvider, query: &HashMap
|
|
|
260
367
|
Err(e) => return respond(500, "text/plain", &format!("{e:#}")),
|
|
261
368
|
};
|
|
262
369
|
let subscribers = instances_for(&read, &provider.instance_prefix());
|
|
263
|
-
let status = subscribers
|
|
264
|
-
|
|
370
|
+
let status = subscribers
|
|
371
|
+
.iter()
|
|
372
|
+
.find(|(id, _)| id == email)
|
|
373
|
+
.and_then(|(_, s)| s.get("status"))
|
|
374
|
+
.and_then(|v| v.as_str())
|
|
375
|
+
.unwrap_or_else(|| provider.initial_status());
|
|
376
|
+
respond(
|
|
377
|
+
200,
|
|
378
|
+
"application/json",
|
|
379
|
+
&json!({"email": email, "status": status}).to_string(),
|
|
380
|
+
)
|
|
265
381
|
}
|
|
266
382
|
|
|
267
383
|
// 400 with no address; 403 for a missing, wrong, other-address or expired
|
|
268
384
|
// token, or no secret to check against. Otherwise Ok(email).
|
|
269
|
-
fn unsubscribe_authorized<'a>(
|
|
385
|
+
fn unsubscribe_authorized<'a>(
|
|
386
|
+
secret: Option<&str>,
|
|
387
|
+
query: &'a HashMap<String, String>,
|
|
388
|
+
) -> Result<&'a str, Value> {
|
|
270
389
|
let Some(email) = query.get("email") else {
|
|
271
|
-
return Err(respond(
|
|
390
|
+
return Err(respond(
|
|
391
|
+
400,
|
|
392
|
+
"application/json",
|
|
393
|
+
&json!({"error": "email"}).to_string(),
|
|
394
|
+
));
|
|
272
395
|
};
|
|
273
396
|
let signed = match (secret, query.get("token")) {
|
|
274
397
|
(Some(secret), Some(token)) => unsubscribe_token_matches(secret, token, email),
|
|
@@ -277,14 +400,25 @@ fn unsubscribe_authorized<'a>(secret: Option<&str>, query: &'a HashMap<String, S
|
|
|
277
400
|
if signed {
|
|
278
401
|
Ok(email)
|
|
279
402
|
} else {
|
|
280
|
-
Err(respond(
|
|
403
|
+
Err(respond(
|
|
404
|
+
403,
|
|
405
|
+
"application/json",
|
|
406
|
+
&json!({"error": "this unsubscribe link is invalid or has expired"}).to_string(),
|
|
407
|
+
))
|
|
281
408
|
}
|
|
282
409
|
}
|
|
283
410
|
|
|
284
411
|
// Same token/idempotency shape as newsletter_confirm_route: Unsubscribe's
|
|
285
412
|
// own `given` accepts a pending or confirmed subscriber, so a repeat click
|
|
286
413
|
// on an already-unsubscribed row lands on the same success response.
|
|
287
|
-
async fn newsletter_unsubscribe_route(
|
|
414
|
+
async fn newsletter_unsubscribe_route(
|
|
415
|
+
provider: &NewsletterProvider,
|
|
416
|
+
query: &HashMap<String, String>,
|
|
417
|
+
host: Host<'_>,
|
|
418
|
+
) -> Value {
|
|
419
|
+
let Host {
|
|
420
|
+
client, wasm_path, ..
|
|
421
|
+
} = host;
|
|
288
422
|
let email = match unsubscribe_authorized(confirm_secret().as_deref(), query) {
|
|
289
423
|
Ok(email) => email,
|
|
290
424
|
Err(refusal) => return refusal,
|
|
@@ -296,11 +430,18 @@ async fn newsletter_unsubscribe_route(provider: &NewsletterProvider, query: &Has
|
|
|
296
430
|
};
|
|
297
431
|
let subscribers = instances_for(&read, &provider.instance_prefix());
|
|
298
432
|
let Some((_, subscriber)) = subscribers.iter().find(|(id, _)| id == email) else {
|
|
299
|
-
return respond(
|
|
433
|
+
return respond(
|
|
434
|
+
404,
|
|
435
|
+
"application/json",
|
|
436
|
+
&json!({"error": "no such subscriber"}).to_string(),
|
|
437
|
+
);
|
|
300
438
|
};
|
|
301
439
|
|
|
302
440
|
if !provider.has_left(subscriber.get("status").and_then(|v| v.as_str())) {
|
|
303
|
-
if let Err(e) =
|
|
441
|
+
if let Err(e) =
|
|
442
|
+
dispatch::handle_routed(host, &provider.unsubscribe, json!(email), json!({}), None)
|
|
443
|
+
.await
|
|
444
|
+
{
|
|
304
445
|
return respond(500, "text/plain", &format!("{e:#}"));
|
|
305
446
|
}
|
|
306
447
|
}
|
|
@@ -310,6 +451,15 @@ async fn newsletter_unsubscribe_route(provider: &NewsletterProvider, query: &Has
|
|
|
310
451
|
Err(e) => return respond(500, "text/plain", &format!("{e:#}")),
|
|
311
452
|
};
|
|
312
453
|
let subscribers = instances_for(&read, &provider.instance_prefix());
|
|
313
|
-
let status = subscribers
|
|
314
|
-
|
|
454
|
+
let status = subscribers
|
|
455
|
+
.iter()
|
|
456
|
+
.find(|(id, _)| id == email)
|
|
457
|
+
.and_then(|(_, s)| s.get("status"))
|
|
458
|
+
.and_then(|v| v.as_str())
|
|
459
|
+
.unwrap_or_else(|| provider.initial_status());
|
|
460
|
+
respond(
|
|
461
|
+
200,
|
|
462
|
+
"application/json",
|
|
463
|
+
&json!({"email": email, "status": status}).to_string(),
|
|
464
|
+
)
|
|
315
465
|
}
|