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/server.rs
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
//! `dispatch_body` with the Lambda custom-runtime path, so neither repeats the other's logic.
|
|
3
3
|
|
|
4
4
|
use crate::dispatch;
|
|
5
|
+
use crate::host::Host;
|
|
5
6
|
use crate::journal::LineageConfig;
|
|
6
7
|
use crate::lambda_client::LambdaInvoker;
|
|
7
8
|
use crate::log;
|
|
@@ -17,34 +18,43 @@ use axum::Router;
|
|
|
17
18
|
use lambda_runtime::Error;
|
|
18
19
|
use serde_json::Value;
|
|
19
20
|
use std::net::SocketAddr;
|
|
20
|
-
use std::path::
|
|
21
|
+
use std::path::PathBuf;
|
|
21
22
|
use std::sync::Arc;
|
|
22
23
|
use tokio::sync::Mutex;
|
|
23
24
|
use tokio_postgres::Client;
|
|
24
25
|
|
|
25
26
|
/// The per-request dispatch decision, shared between the Lambda custom-runtime path and the
|
|
26
27
|
/// axum fallback route below. `body` is already-parsed JSON in one of its recognized shapes.
|
|
27
|
-
pub async fn dispatch_body(
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
if let Some(response) = web::render(&body,
|
|
28
|
+
pub async fn dispatch_body(body: Value, host: Host<'_>) -> Result<Value, Error> {
|
|
29
|
+
let Host {
|
|
30
|
+
client,
|
|
31
|
+
wasm_path,
|
|
32
|
+
config,
|
|
33
|
+
..
|
|
34
|
+
} = host;
|
|
35
|
+
if let Some(response) = web::render(&body, host).await {
|
|
35
36
|
return Ok(response);
|
|
36
37
|
}
|
|
37
38
|
|
|
38
|
-
if body.get("read").and_then(
|
|
39
|
+
if body.get("read").and_then(serde_json::Value::as_bool) == Some(true) {
|
|
39
40
|
// `{e:#}`: anyhow's alternate Display walks `source()` to the real message,
|
|
40
41
|
// where a bare `tokio_postgres` error's Display is just "db error".
|
|
41
|
-
let result = dispatch::read(client, wasm_path)
|
|
42
|
+
let result = dispatch::read(client, wasm_path)
|
|
43
|
+
.await
|
|
44
|
+
.map_err(|e| format!("{e:#}"))?;
|
|
42
45
|
return Ok(result);
|
|
43
46
|
}
|
|
44
47
|
|
|
45
48
|
// A query step reads current state and writes nothing to the journal.
|
|
46
49
|
if body.get("query").is_some() {
|
|
47
|
-
return Ok(crate::query_step::answer(
|
|
50
|
+
return Ok(crate::query_step::answer(
|
|
51
|
+
&body,
|
|
52
|
+
crate::ir::ir(),
|
|
53
|
+
&config.domain,
|
|
54
|
+
client,
|
|
55
|
+
wasm_path,
|
|
56
|
+
)
|
|
57
|
+
.await?);
|
|
48
58
|
}
|
|
49
59
|
|
|
50
60
|
let verb = body
|
|
@@ -52,30 +62,45 @@ pub async fn dispatch_body(
|
|
|
52
62
|
.and_then(|v| v.as_str())
|
|
53
63
|
.ok_or("event missing \"verb\"")?
|
|
54
64
|
.to_string();
|
|
55
|
-
let role = body
|
|
65
|
+
let role = body
|
|
66
|
+
.get("role")
|
|
67
|
+
.and_then(|v| v.as_str())
|
|
68
|
+
.map(std::string::ToString::to_string);
|
|
56
69
|
// Who the caller is, for Governance. Honored only where the internal protocol is: from this
|
|
57
70
|
// host's own peers (`trusts_internal_dispatch`), never from a public caller.
|
|
58
|
-
let actor_id = body
|
|
59
|
-
|
|
71
|
+
let actor_id = body
|
|
72
|
+
.get("actor_id")
|
|
73
|
+
.and_then(|v| v.as_str())
|
|
74
|
+
.map(std::string::ToString::to_string);
|
|
75
|
+
let caller = dispatch::Caller {
|
|
76
|
+
role: role.as_deref(),
|
|
77
|
+
actor_id: actor_id.as_deref(),
|
|
78
|
+
};
|
|
60
79
|
|
|
61
80
|
let outcome = if let Some(to) = body.get("to").cloned() {
|
|
62
81
|
if body.get("args").is_some() {
|
|
63
82
|
return Err("cannot combine to/with with legacy args".into());
|
|
64
83
|
}
|
|
65
|
-
let facts = body
|
|
66
|
-
|
|
84
|
+
let facts = body
|
|
85
|
+
.get("with")
|
|
86
|
+
.cloned()
|
|
87
|
+
.unwrap_or_else(|| serde_json::json!({}));
|
|
88
|
+
dispatch::handle_routed_as(host, &verb, to, facts, caller)
|
|
67
89
|
.await
|
|
68
90
|
.map_err(|e| format!("{e:#}"))?
|
|
69
91
|
} else if let Some(facts) = body.get("with").cloned() {
|
|
70
92
|
if body.get("args").is_some() {
|
|
71
93
|
return Err("cannot combine \"with\" with legacy args".into());
|
|
72
94
|
}
|
|
73
|
-
dispatch::handle_facts_as(
|
|
95
|
+
dispatch::handle_facts_as(host, &verb, facts, caller)
|
|
74
96
|
.await
|
|
75
97
|
.map_err(|e| format!("{e:#}"))?
|
|
76
98
|
} else {
|
|
77
|
-
let args = body
|
|
78
|
-
|
|
99
|
+
let args = body
|
|
100
|
+
.get("args")
|
|
101
|
+
.cloned()
|
|
102
|
+
.unwrap_or_else(|| serde_json::json!({}));
|
|
103
|
+
dispatch::handle_as(host, &verb, args, caller)
|
|
79
104
|
.await
|
|
80
105
|
.map_err(|e| format!("{e:#}"))?
|
|
81
106
|
};
|
|
@@ -93,10 +118,24 @@ pub struct ServerState {
|
|
|
93
118
|
pub limits: Arc<RateLimits>,
|
|
94
119
|
}
|
|
95
120
|
|
|
121
|
+
impl ServerState {
|
|
122
|
+
/// The shared pieces a dispatch needs, borrowed for one call.
|
|
123
|
+
pub fn host(&self) -> Host<'_> {
|
|
124
|
+
Host::shared(
|
|
125
|
+
&self.client,
|
|
126
|
+
&self.wasm_path,
|
|
127
|
+
&self.lineage_config,
|
|
128
|
+
&self.invoker,
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
96
133
|
/// The document `GET /version` serves: era, IR hash, and build — all fixed for the life of
|
|
97
134
|
/// the process, so this is built once at boot. Carries no secrets.
|
|
98
135
|
pub fn version_body(era: &str, ir_hash: &str, build_env: Option<&str>) -> Value {
|
|
99
|
-
let build = build_env
|
|
136
|
+
let build = build_env
|
|
137
|
+
.filter(|v| !v.is_empty())
|
|
138
|
+
.unwrap_or(env!("CARGO_PKG_VERSION"));
|
|
100
139
|
serde_json::json!({ "era": era, "ir_hash": ir_hash, "build": build })
|
|
101
140
|
}
|
|
102
141
|
|
|
@@ -104,13 +143,19 @@ pub fn version_body(era: &str, ir_hash: &str, build_env: Option<&str>) -> Value
|
|
|
104
143
|
/// request answers a clone of the boot-time document, never touching auth or dispatch.
|
|
105
144
|
pub fn version_router(body: Value) -> Router {
|
|
106
145
|
let body = Arc::new(body);
|
|
107
|
-
Router::new().route(
|
|
146
|
+
Router::new().route(
|
|
147
|
+
"/version",
|
|
148
|
+
get(move || std::future::ready(axum::Json(body.as_ref().clone()))),
|
|
149
|
+
)
|
|
108
150
|
}
|
|
109
151
|
|
|
110
152
|
/// Compiles the domain's wasm module, then binds `$PORT` (falls back to 8080) and serves
|
|
111
153
|
/// forever. Nothing listens until the compile finishes, so the health check never lies.
|
|
112
154
|
pub async fn serve(state: ServerState, version: Value) -> Result<(), Error> {
|
|
113
|
-
let port: u16 = std::env::var("PORT")
|
|
155
|
+
let port: u16 = std::env::var("PORT")
|
|
156
|
+
.ok()
|
|
157
|
+
.and_then(|v| v.parse().ok())
|
|
158
|
+
.unwrap_or(8080);
|
|
114
159
|
|
|
115
160
|
warm_wasm(&state.wasm_path).await;
|
|
116
161
|
|
|
@@ -122,7 +167,11 @@ pub async fn serve(state: ServerState, version: Value) -> Result<(), Error> {
|
|
|
122
167
|
|
|
123
168
|
/// The router and serve loop over an already-bound `listener`, split out so a test can
|
|
124
169
|
/// run the real service on an ephemeral port.
|
|
125
|
-
async fn serve_on(
|
|
170
|
+
async fn serve_on(
|
|
171
|
+
listener: tokio::net::TcpListener,
|
|
172
|
+
state: ServerState,
|
|
173
|
+
version: Value,
|
|
174
|
+
) -> Result<(), Error> {
|
|
126
175
|
let app = Router::new()
|
|
127
176
|
.route("/", get(health))
|
|
128
177
|
.fallback(any(dispatch_route))
|
|
@@ -130,7 +179,11 @@ async fn serve_on(listener: tokio::net::TcpListener, state: ServerState, version
|
|
|
130
179
|
.merge(version_router(version));
|
|
131
180
|
|
|
132
181
|
// The connect info is the TCP peer, which `rate_limit` needs to tell a visitor from a proxy.
|
|
133
|
-
axum::serve(
|
|
182
|
+
axum::serve(
|
|
183
|
+
listener,
|
|
184
|
+
app.into_make_service_with_connect_info::<SocketAddr>(),
|
|
185
|
+
)
|
|
186
|
+
.await?;
|
|
134
187
|
Ok(())
|
|
135
188
|
}
|
|
136
189
|
|
|
@@ -143,8 +196,14 @@ async fn warm_wasm(wasm_path: &Arc<PathBuf>) {
|
|
|
143
196
|
Ok(Ok(())) => {
|
|
144
197
|
phase.end();
|
|
145
198
|
}
|
|
146
|
-
Ok(Err(e)) => log::error(
|
|
147
|
-
|
|
199
|
+
Ok(Err(e)) => log::error(
|
|
200
|
+
"wasm_warm_failed",
|
|
201
|
+
serde_json::json!({ "error": format!("{e:#}") }),
|
|
202
|
+
),
|
|
203
|
+
Err(e) => log::error(
|
|
204
|
+
"wasm_warm_failed",
|
|
205
|
+
serde_json::json!({ "error": format!("{e}") }),
|
|
206
|
+
),
|
|
148
207
|
}
|
|
149
208
|
}
|
|
150
209
|
|
|
@@ -179,30 +238,55 @@ async fn dispatch_route(
|
|
|
179
238
|
response
|
|
180
239
|
}
|
|
181
240
|
|
|
182
|
-
async fn route_request(
|
|
241
|
+
async fn route_request(
|
|
242
|
+
state: ServerState,
|
|
243
|
+
peer: SocketAddr,
|
|
244
|
+
method: Method,
|
|
245
|
+
uri: Uri,
|
|
246
|
+
headers: HeaderMap,
|
|
247
|
+
body: Bytes,
|
|
248
|
+
) -> Response {
|
|
183
249
|
let envelope = match admit(&state.limits, peer, &method, &uri, &headers, &body) {
|
|
184
250
|
Ok(envelope) => envelope,
|
|
185
251
|
Err(refusal) => return *refusal,
|
|
186
252
|
};
|
|
187
253
|
|
|
188
|
-
match dispatch_body(envelope,
|
|
254
|
+
match dispatch_body(envelope, state.host()).await {
|
|
189
255
|
Ok(value) => value_to_response(value),
|
|
190
256
|
Err(e) => {
|
|
191
|
-
log::error(
|
|
192
|
-
|
|
257
|
+
log::error(
|
|
258
|
+
"dispatch_failed",
|
|
259
|
+
serde_json::json!({ "error": format!("{e}") }),
|
|
260
|
+
);
|
|
261
|
+
(
|
|
262
|
+
StatusCode::INTERNAL_SERVER_ERROR,
|
|
263
|
+
axum::Json(serde_json::json!({ "error": format!("{e}") })),
|
|
264
|
+
)
|
|
265
|
+
.into_response()
|
|
193
266
|
}
|
|
194
267
|
}
|
|
195
268
|
}
|
|
196
269
|
|
|
197
270
|
/// Turns a raw HTTP request into `dispatch_body`'s envelope, or into the response that ends
|
|
198
271
|
/// it first: a `400` for non-JSON, a `429` for a caller over a public route's rate limit.
|
|
199
|
-
fn admit(
|
|
272
|
+
fn admit(
|
|
273
|
+
limits: &RateLimits,
|
|
274
|
+
peer: SocketAddr,
|
|
275
|
+
method: &Method,
|
|
276
|
+
uri: &Uri,
|
|
277
|
+
headers: &HeaderMap,
|
|
278
|
+
body: &Bytes,
|
|
279
|
+
) -> Result<Value, Box<Response>> {
|
|
200
280
|
let parsed: Value = if body.is_empty() {
|
|
201
281
|
serde_json::json!({})
|
|
202
282
|
} else {
|
|
203
283
|
match serde_json::from_slice(body) {
|
|
204
284
|
Ok(value) => value,
|
|
205
|
-
Err(e) =>
|
|
285
|
+
Err(e) => {
|
|
286
|
+
return Err(Box::new(
|
|
287
|
+
(StatusCode::BAD_REQUEST, format!("invalid JSON body: {e}")).into_response(),
|
|
288
|
+
))
|
|
289
|
+
}
|
|
206
290
|
}
|
|
207
291
|
};
|
|
208
292
|
|
|
@@ -213,14 +297,22 @@ fn admit(limits: &RateLimits, peer: SocketAddr, method: &Method, uri: &Uri, head
|
|
|
213
297
|
// they're honored only from a peer on this host (the task's own sidecar); everyone
|
|
214
298
|
// else — the load balancer, i.e. the public internet — gets an ordinary request
|
|
215
299
|
// envelope for the path it actually hit.
|
|
216
|
-
|
|
300
|
+
// A proxy on this host (an AwsBox's Caddy, which a tunnel also goes through) makes every
|
|
301
|
+
// public request arrive from loopback too; the forwarding headers it always adds are what
|
|
302
|
+
// tell its traffic apart from a sidecar's.
|
|
303
|
+
let envelope = if trusts_internal_dispatch(peer)
|
|
304
|
+
&& !came_through_a_proxy(headers)
|
|
305
|
+
&& is_internal_dispatch_shape(&parsed)
|
|
306
|
+
{
|
|
217
307
|
parsed
|
|
218
308
|
} else {
|
|
219
309
|
synthesize_function_url_envelope(method, uri, headers, body)
|
|
220
310
|
};
|
|
221
311
|
|
|
222
312
|
if let Some((route_method, route_path)) = envelope_route(&envelope) {
|
|
223
|
-
if let Verdict::Limited { retry_after_secs } =
|
|
313
|
+
if let Verdict::Limited { retry_after_secs } =
|
|
314
|
+
limits.check(route_method, route_path, headers, Some(peer.ip()))
|
|
315
|
+
{
|
|
224
316
|
return Err(Box::new(rate_limit::too_many_requests(retry_after_secs)));
|
|
225
317
|
}
|
|
226
318
|
}
|
|
@@ -230,7 +322,11 @@ fn admit(limits: &RateLimits, peer: SocketAddr, method: &Method, uri: &Uri, head
|
|
|
230
322
|
/// The method and path a Function-URL-shaped envelope names, or `None` for
|
|
231
323
|
/// the verb and read shapes, which carry no route.
|
|
232
324
|
fn envelope_route(envelope: &Value) -> Option<(&str, &str)> {
|
|
233
|
-
let method = envelope
|
|
325
|
+
let method = envelope
|
|
326
|
+
.get("requestContext")?
|
|
327
|
+
.get("http")?
|
|
328
|
+
.get("method")?
|
|
329
|
+
.as_str()?;
|
|
234
330
|
let path = envelope.get("rawPath")?.as_str()?;
|
|
235
331
|
Some((method, path))
|
|
236
332
|
}
|
|
@@ -240,10 +336,29 @@ fn envelope_route(envelope: &Value) -> Option<(&str, &str)> {
|
|
|
240
336
|
fn trusts_internal_dispatch(peer: SocketAddr) -> bool {
|
|
241
337
|
match peer.ip() {
|
|
242
338
|
std::net::IpAddr::V4(addr) => addr.is_loopback(),
|
|
243
|
-
std::net::IpAddr::V6(addr) =>
|
|
339
|
+
std::net::IpAddr::V6(addr) => {
|
|
340
|
+
addr.is_loopback()
|
|
341
|
+
|| addr
|
|
342
|
+
.to_ipv4_mapped()
|
|
343
|
+
.is_some_and(|mapped| mapped.is_loopback())
|
|
344
|
+
}
|
|
244
345
|
}
|
|
245
346
|
}
|
|
246
347
|
|
|
348
|
+
/// Whether a proxy forwarded this request: Caddy's `reverse_proxy` always sets the
|
|
349
|
+
/// `X-Forwarded-*` headers, and a client cannot make it drop them.
|
|
350
|
+
fn came_through_a_proxy(headers: &HeaderMap) -> bool {
|
|
351
|
+
[
|
|
352
|
+
"x-forwarded-for",
|
|
353
|
+
"x-forwarded-host",
|
|
354
|
+
"x-forwarded-proto",
|
|
355
|
+
"forwarded",
|
|
356
|
+
"via",
|
|
357
|
+
]
|
|
358
|
+
.iter()
|
|
359
|
+
.any(|name| headers.contains_key(*name))
|
|
360
|
+
}
|
|
361
|
+
|
|
247
362
|
/// Whether `value` already matches one of `dispatch_body`'s recognized shapes; anything
|
|
248
363
|
/// else (including a bare `{}`) is real REST traffic needing `synthesize_function_url_envelope`.
|
|
249
364
|
fn is_internal_dispatch_shape(value: &Value) -> bool {
|
|
@@ -255,11 +370,19 @@ fn is_internal_dispatch_shape(value: &Value) -> bool {
|
|
|
255
370
|
|
|
256
371
|
/// Rebuilds the envelope shape a real Lambda Function URL invocation already produces
|
|
257
372
|
/// automatically, from the raw axum request parts.
|
|
258
|
-
fn synthesize_function_url_envelope(
|
|
373
|
+
fn synthesize_function_url_envelope(
|
|
374
|
+
method: &Method,
|
|
375
|
+
uri: &Uri,
|
|
376
|
+
headers: &HeaderMap,
|
|
377
|
+
body: &Bytes,
|
|
378
|
+
) -> Value {
|
|
259
379
|
let mut header_map = serde_json::Map::new();
|
|
260
|
-
for (name, value) in headers
|
|
380
|
+
for (name, value) in headers {
|
|
261
381
|
if let Ok(value) = value.to_str() {
|
|
262
|
-
header_map.insert(
|
|
382
|
+
header_map.insert(
|
|
383
|
+
name.as_str().to_ascii_lowercase(),
|
|
384
|
+
Value::String(value.to_string()),
|
|
385
|
+
);
|
|
263
386
|
}
|
|
264
387
|
}
|
|
265
388
|
|
|
@@ -276,17 +399,25 @@ fn synthesize_function_url_envelope(method: &Method, uri: &Uri, headers: &Header
|
|
|
276
399
|
/// Translates `dispatch_body`'s JSON result into a real HTTP response: `web::render`'s
|
|
277
400
|
/// Function-URL envelope when `statusCode` is present, else the raw outcome as `200` JSON.
|
|
278
401
|
fn value_to_response(value: Value) -> Response {
|
|
279
|
-
let Some(status_code) = value.get("statusCode").and_then(
|
|
402
|
+
let Some(status_code) = value.get("statusCode").and_then(serde_json::Value::as_u64) else {
|
|
280
403
|
return (StatusCode::OK, axum::Json(value)).into_response();
|
|
281
404
|
};
|
|
282
405
|
// `try_from`, not `as u16`: 65736 would wrap to 200 and turn a handler's garbage into a success.
|
|
283
|
-
let status = u16::try_from(status_code)
|
|
406
|
+
let status = u16::try_from(status_code)
|
|
407
|
+
.ok()
|
|
408
|
+
.and_then(|code| StatusCode::from_u16(code).ok())
|
|
409
|
+
.unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
|
|
284
410
|
|
|
285
411
|
let mut headers = HeaderMap::new();
|
|
286
412
|
if let Some(header_obj) = value.get("headers").and_then(|h| h.as_object()) {
|
|
287
413
|
for (name, header_value) in header_obj {
|
|
288
|
-
let Some(header_value) = header_value.as_str() else {
|
|
289
|
-
|
|
414
|
+
let Some(header_value) = header_value.as_str() else {
|
|
415
|
+
continue;
|
|
416
|
+
};
|
|
417
|
+
if let (Ok(name), Ok(header_value)) = (
|
|
418
|
+
HeaderName::from_bytes(name.as_bytes()),
|
|
419
|
+
HeaderValue::from_str(header_value),
|
|
420
|
+
) {
|
|
290
421
|
headers.insert(name, header_value);
|
|
291
422
|
}
|
|
292
423
|
}
|
|
@@ -304,7 +435,11 @@ fn value_to_response(value: Value) -> Response {
|
|
|
304
435
|
}
|
|
305
436
|
|
|
306
437
|
let raw_body = value.get("body").and_then(|v| v.as_str()).unwrap_or("");
|
|
307
|
-
let body_bytes = if value
|
|
438
|
+
let body_bytes = if value
|
|
439
|
+
.get("isBase64Encoded")
|
|
440
|
+
.and_then(serde_json::Value::as_bool)
|
|
441
|
+
== Some(true)
|
|
442
|
+
{
|
|
308
443
|
web::base64_decode(raw_body)
|
|
309
444
|
} else {
|
|
310
445
|
raw_body.as_bytes().to_vec()
|
|
@@ -319,29 +454,39 @@ mod tests {
|
|
|
319
454
|
|
|
320
455
|
#[test]
|
|
321
456
|
fn a_function_url_shaped_body_is_recognized_as_internal_dispatch() {
|
|
322
|
-
assert!(is_internal_dispatch_shape(
|
|
457
|
+
assert!(is_internal_dispatch_shape(
|
|
458
|
+
&serde_json::json!({"requestContext": {"http": {"method": "GET"}}})
|
|
459
|
+
));
|
|
323
460
|
}
|
|
324
461
|
|
|
325
462
|
#[test]
|
|
326
463
|
fn a_bare_read_body_is_recognized_as_internal_dispatch() {
|
|
327
|
-
assert!(is_internal_dispatch_shape(
|
|
464
|
+
assert!(is_internal_dispatch_shape(
|
|
465
|
+
&serde_json::json!({"read": true})
|
|
466
|
+
));
|
|
328
467
|
}
|
|
329
468
|
|
|
330
469
|
#[test]
|
|
331
470
|
fn a_declared_query_body_is_recognized_as_internal_dispatch() {
|
|
332
471
|
// `dispatch_body` answers `{"query": ...}`, and the generated editor's list and picture
|
|
333
472
|
// pickers ask it; without this a loopback caller was sent to the login page.
|
|
334
|
-
assert!(is_internal_dispatch_shape(
|
|
473
|
+
assert!(is_internal_dispatch_shape(
|
|
474
|
+
&serde_json::json!({"query": "Pictures::MediaItem.Pictures", "args": {}})
|
|
475
|
+
));
|
|
335
476
|
}
|
|
336
477
|
|
|
337
478
|
#[test]
|
|
338
479
|
fn a_verb_command_body_is_recognized_as_internal_dispatch() {
|
|
339
|
-
assert!(is_internal_dispatch_shape(
|
|
480
|
+
assert!(is_internal_dispatch_shape(
|
|
481
|
+
&serde_json::json!({"verb": "Register", "args": {}})
|
|
482
|
+
));
|
|
340
483
|
}
|
|
341
484
|
|
|
342
485
|
#[test]
|
|
343
486
|
fn a_plain_rest_payload_is_not_internal_dispatch() {
|
|
344
|
-
assert!(!is_internal_dispatch_shape(
|
|
487
|
+
assert!(!is_internal_dispatch_shape(
|
|
488
|
+
&serde_json::json!({"email": "a@example.com"})
|
|
489
|
+
));
|
|
345
490
|
}
|
|
346
491
|
|
|
347
492
|
#[test]
|
|
@@ -355,21 +500,43 @@ mod tests {
|
|
|
355
500
|
|
|
356
501
|
#[test]
|
|
357
502
|
fn only_a_peer_on_this_host_is_trusted_with_the_internal_protocol() {
|
|
358
|
-
for trusted in [
|
|
359
|
-
|
|
503
|
+
for trusted in [
|
|
504
|
+
"127.0.0.1:5000",
|
|
505
|
+
"127.4.5.6:5000",
|
|
506
|
+
"[::1]:5000",
|
|
507
|
+
"[::ffff:127.0.0.1]:5000",
|
|
508
|
+
] {
|
|
509
|
+
assert!(
|
|
510
|
+
trusts_internal_dispatch(peer(trusted)),
|
|
511
|
+
"{trusted} is this host"
|
|
512
|
+
);
|
|
360
513
|
}
|
|
361
|
-
for outside in [
|
|
362
|
-
|
|
514
|
+
for outside in [
|
|
515
|
+
"10.0.0.5:5000",
|
|
516
|
+
"172.31.4.9:5000",
|
|
517
|
+
"203.0.113.7:5000",
|
|
518
|
+
"[2001:db8::1]:5000",
|
|
519
|
+
"[::ffff:10.0.0.5]:5000",
|
|
520
|
+
] {
|
|
521
|
+
assert!(
|
|
522
|
+
!trusts_internal_dispatch(peer(outside)),
|
|
523
|
+
"{outside} is not this host"
|
|
524
|
+
);
|
|
363
525
|
}
|
|
364
526
|
}
|
|
365
527
|
|
|
366
528
|
fn admitted(from: &str, path: &str, body: &str) -> Value {
|
|
367
529
|
let uri: Uri = path.parse().unwrap();
|
|
368
530
|
let body = Bytes::from(body.to_string());
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
531
|
+
admit(
|
|
532
|
+
&limits_from(&[]),
|
|
533
|
+
peer(from),
|
|
534
|
+
&Method::POST,
|
|
535
|
+
&uri,
|
|
536
|
+
&HeaderMap::new(),
|
|
537
|
+
&body,
|
|
538
|
+
)
|
|
539
|
+
.unwrap_or_else(|_| panic!("{from} POST {path} was refused before dispatch"))
|
|
373
540
|
}
|
|
374
541
|
|
|
375
542
|
// The gap this closes: a public POST to any path the load balancer forwards carried
|
|
@@ -377,11 +544,17 @@ mod tests {
|
|
|
377
544
|
// caller's own word for its role and no session check.
|
|
378
545
|
#[test]
|
|
379
546
|
fn a_verb_or_read_body_from_outside_this_host_is_an_ordinary_request_not_a_command() {
|
|
380
|
-
for body in [
|
|
547
|
+
for body in [
|
|
548
|
+
r#"{"verb":"Approve","role":"admin"}"#,
|
|
549
|
+
r#"{"read":true}"#,
|
|
550
|
+
r#"{"query":"Pictures::MediaItem.Pictures","args":{}}"#,
|
|
551
|
+
] {
|
|
381
552
|
let envelope = admitted("10.0.0.5:5000", "/webhooks/x", body);
|
|
382
553
|
|
|
383
554
|
assert!(
|
|
384
|
-
envelope.get("verb").is_none()
|
|
555
|
+
envelope.get("verb").is_none()
|
|
556
|
+
&& envelope.get("read").is_none()
|
|
557
|
+
&& envelope.get("query").is_none(),
|
|
385
558
|
"{body} must not reach dispatch as a command"
|
|
386
559
|
);
|
|
387
560
|
assert_eq!(envelope["requestContext"]["http"]["method"], "POST");
|
|
@@ -397,18 +570,58 @@ mod tests {
|
|
|
397
570
|
|
|
398
571
|
assert_eq!(envelope["rawPath"], "/webhooks/x");
|
|
399
572
|
assert_eq!(envelope["requestContext"]["http"]["method"], "POST");
|
|
400
|
-
assert!(
|
|
573
|
+
assert!(
|
|
574
|
+
envelope["headers"].get("cookie").is_none(),
|
|
575
|
+
"headers come from the real request, not the body"
|
|
576
|
+
);
|
|
401
577
|
}
|
|
402
578
|
|
|
403
579
|
#[test]
|
|
404
580
|
fn a_verb_or_read_body_from_this_host_still_reaches_dispatch_unchanged() {
|
|
405
|
-
for body in [
|
|
581
|
+
for body in [
|
|
582
|
+
r#"{"verb":"Register","to":"r1","with":{}}"#,
|
|
583
|
+
r#"{"read":true}"#,
|
|
584
|
+
r#"{"query":"Banking::Customer.Suspended","args":{}}"#,
|
|
585
|
+
] {
|
|
406
586
|
let envelope = admitted("127.0.0.1:5000", "/anything", body);
|
|
407
587
|
|
|
408
588
|
assert_eq!(envelope, serde_json::from_str::<Value>(body).unwrap());
|
|
409
589
|
}
|
|
410
590
|
}
|
|
411
591
|
|
|
592
|
+
// The gap this closes: on an AwsBox, Caddy proxies the public internet to 127.0.0.1, so
|
|
593
|
+
// a public `{"read": true}` or `{"verb": ..., "role": "Admin"}` arrived from loopback and
|
|
594
|
+
// reached the kernel as the internal protocol.
|
|
595
|
+
#[test]
|
|
596
|
+
fn a_verb_or_read_body_a_proxy_on_this_host_forwarded_is_an_ordinary_request() {
|
|
597
|
+
for header in [
|
|
598
|
+
"x-forwarded-for",
|
|
599
|
+
"x-forwarded-host",
|
|
600
|
+
"x-forwarded-proto",
|
|
601
|
+
"forwarded",
|
|
602
|
+
"via",
|
|
603
|
+
] {
|
|
604
|
+
let mut headers = HeaderMap::new();
|
|
605
|
+
headers.insert(header, "203.0.113.7".parse().unwrap());
|
|
606
|
+
let body = Bytes::from(r#"{"verb":"Approve","role":"Admin"}"#);
|
|
607
|
+
let envelope = admit(
|
|
608
|
+
&limits_from(&[]),
|
|
609
|
+
peer("127.0.0.1:5000"),
|
|
610
|
+
&Method::POST,
|
|
611
|
+
&"/anything".parse().unwrap(),
|
|
612
|
+
&headers,
|
|
613
|
+
&body,
|
|
614
|
+
)
|
|
615
|
+
.unwrap_or_else(|_| panic!("a request carrying {header} was refused"));
|
|
616
|
+
|
|
617
|
+
assert!(
|
|
618
|
+
envelope.get("verb").is_none(),
|
|
619
|
+
"a body forwarded with {header} must not reach dispatch as a command"
|
|
620
|
+
);
|
|
621
|
+
assert_eq!(envelope["rawPath"], "/anything");
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
412
625
|
// Pins a real REST client's plain POST, with no Function-URL wrapping: it must
|
|
413
626
|
// synthesize the same shape `web::render`/`checkout_route` read, not error on a
|
|
414
627
|
// missing "verb".
|
|
@@ -417,8 +630,14 @@ mod tests {
|
|
|
417
630
|
let method = Method::POST;
|
|
418
631
|
let uri: Uri = "/registrations?utm_source=test".parse().unwrap();
|
|
419
632
|
let mut headers = HeaderMap::new();
|
|
420
|
-
headers.insert(
|
|
421
|
-
|
|
633
|
+
headers.insert(
|
|
634
|
+
HeaderName::from_static("stripe-signature"),
|
|
635
|
+
HeaderValue::from_static("t=1,v1=abc"),
|
|
636
|
+
);
|
|
637
|
+
headers.insert(
|
|
638
|
+
HeaderName::from_static("content-type"),
|
|
639
|
+
HeaderValue::from_static("application/json"),
|
|
640
|
+
);
|
|
422
641
|
let body = Bytes::from_static(br#"{"event_id":"ci-verification-test"}"#);
|
|
423
642
|
|
|
424
643
|
let envelope = synthesize_function_url_envelope(&method, &uri, &headers, &body);
|
|
@@ -430,7 +649,10 @@ mod tests {
|
|
|
430
649
|
assert_eq!(envelope["headers"]["content-type"], "application/json");
|
|
431
650
|
assert_eq!(envelope["body"], r#"{"event_id":"ci-verification-test"}"#);
|
|
432
651
|
assert_eq!(envelope["isBase64Encoded"], false);
|
|
433
|
-
assert!(
|
|
652
|
+
assert!(
|
|
653
|
+
is_internal_dispatch_shape(&envelope),
|
|
654
|
+
"the synthesized envelope must itself be recognized on a second pass"
|
|
655
|
+
);
|
|
434
656
|
}
|
|
435
657
|
|
|
436
658
|
#[test]
|
|
@@ -453,7 +675,9 @@ mod tests {
|
|
|
453
675
|
// Serves `version_router` on an ephemeral port, the way the host serves
|
|
454
676
|
// it, so a request travels the real HTTP stack.
|
|
455
677
|
async fn serve_version(body: Value) -> std::net::SocketAddr {
|
|
456
|
-
let listener = tokio::net::TcpListener::bind(("127.0.0.1", 0))
|
|
678
|
+
let listener = tokio::net::TcpListener::bind(("127.0.0.1", 0))
|
|
679
|
+
.await
|
|
680
|
+
.unwrap();
|
|
457
681
|
let addr = listener.local_addr().unwrap();
|
|
458
682
|
tokio::spawn(async move { axum::serve(listener, version_router(body)).await });
|
|
459
683
|
addr
|
|
@@ -462,31 +686,56 @@ mod tests {
|
|
|
462
686
|
#[tokio::test]
|
|
463
687
|
async fn version_answers_200_json_with_no_session() {
|
|
464
688
|
let addr = serve_version(sample_version()).await;
|
|
465
|
-
let response = reqwest::get(format!("http://{addr}/version"))
|
|
689
|
+
let response = reqwest::get(format!("http://{addr}/version"))
|
|
690
|
+
.await
|
|
691
|
+
.unwrap();
|
|
466
692
|
assert_eq!(response.status(), 200);
|
|
467
|
-
let content_type = response.headers()["content-type"]
|
|
468
|
-
|
|
693
|
+
let content_type = response.headers()["content-type"]
|
|
694
|
+
.to_str()
|
|
695
|
+
.unwrap()
|
|
696
|
+
.to_string();
|
|
697
|
+
assert!(
|
|
698
|
+
content_type.starts_with("application/json"),
|
|
699
|
+
"got {content_type}"
|
|
700
|
+
);
|
|
469
701
|
let body: Value = response.json().await.unwrap();
|
|
470
|
-
assert_eq!(
|
|
702
|
+
assert_eq!(
|
|
703
|
+
body,
|
|
704
|
+
serde_json::json!({ "era": "199b08", "ir_hash": "a".repeat(64), "build": "build-42" })
|
|
705
|
+
);
|
|
471
706
|
}
|
|
472
707
|
|
|
473
708
|
#[tokio::test]
|
|
474
709
|
async fn version_era_is_a_non_empty_string() {
|
|
475
710
|
let addr = serve_version(sample_version()).await;
|
|
476
|
-
let body: Value = reqwest::get(format!("http://{addr}/version"))
|
|
711
|
+
let body: Value = reqwest::get(format!("http://{addr}/version"))
|
|
712
|
+
.await
|
|
713
|
+
.unwrap()
|
|
714
|
+
.json()
|
|
715
|
+
.await
|
|
716
|
+
.unwrap();
|
|
477
717
|
assert!(body["era"].as_str().is_some_and(|era| !era.is_empty()));
|
|
478
718
|
}
|
|
479
719
|
|
|
480
720
|
#[tokio::test]
|
|
481
721
|
async fn version_is_get_only() {
|
|
482
722
|
let addr = serve_version(sample_version()).await;
|
|
483
|
-
let response = reqwest::Client::new()
|
|
723
|
+
let response = reqwest::Client::new()
|
|
724
|
+
.post(format!("http://{addr}/version"))
|
|
725
|
+
.send()
|
|
726
|
+
.await
|
|
727
|
+
.unwrap();
|
|
484
728
|
assert_eq!(response.status(), 405);
|
|
485
729
|
}
|
|
486
730
|
|
|
487
731
|
fn limits_from(pairs: &[(&str, &str)]) -> Arc<RateLimits> {
|
|
488
|
-
let map: std::collections::HashMap<String, String> = pairs
|
|
489
|
-
|
|
732
|
+
let map: std::collections::HashMap<String, String> = pairs
|
|
733
|
+
.iter()
|
|
734
|
+
.map(|(k, v)| (k.to_string(), v.to_string()))
|
|
735
|
+
.collect();
|
|
736
|
+
Arc::new(RateLimits::new(
|
|
737
|
+
crate::rate_limit::Config::from_lookup(|name| map.get(name).cloned()).0,
|
|
738
|
+
))
|
|
490
739
|
}
|
|
491
740
|
|
|
492
741
|
async fn gate_only(
|
|
@@ -506,15 +755,30 @@ mod tests {
|
|
|
506
755
|
// A disposable server on an ephemeral port: the real `admit` gate reading the real connection
|
|
507
756
|
// peer, with an acknowledgement standing in for dispatch, which needs Postgres and wasm.
|
|
508
757
|
async fn gated_server(limits: Arc<RateLimits>) -> SocketAddr {
|
|
509
|
-
let listener = tokio::net::TcpListener::bind(("127.0.0.1", 0))
|
|
758
|
+
let listener = tokio::net::TcpListener::bind(("127.0.0.1", 0))
|
|
759
|
+
.await
|
|
760
|
+
.unwrap();
|
|
510
761
|
let addr = listener.local_addr().unwrap();
|
|
511
762
|
let app = Router::new().fallback(any(gate_only)).with_state(limits);
|
|
512
|
-
tokio::spawn(async move {
|
|
763
|
+
tokio::spawn(async move {
|
|
764
|
+
axum::serve(
|
|
765
|
+
listener,
|
|
766
|
+
app.into_make_service_with_connect_info::<SocketAddr>(),
|
|
767
|
+
)
|
|
768
|
+
.await
|
|
769
|
+
});
|
|
513
770
|
addr
|
|
514
771
|
}
|
|
515
772
|
|
|
516
|
-
async fn post(
|
|
517
|
-
|
|
773
|
+
async fn post(
|
|
774
|
+
addr: SocketAddr,
|
|
775
|
+
path: &str,
|
|
776
|
+
forwarded_for: Option<&str>,
|
|
777
|
+
body: &str,
|
|
778
|
+
) -> reqwest::Response {
|
|
779
|
+
let mut request = reqwest::Client::new()
|
|
780
|
+
.post(format!("http://{addr}{path}"))
|
|
781
|
+
.body(body.to_string());
|
|
518
782
|
if let Some(forwarded_for) = forwarded_for {
|
|
519
783
|
request = request.header("x-forwarded-for", forwarded_for);
|
|
520
784
|
}
|
|
@@ -527,37 +791,84 @@ mod tests {
|
|
|
527
791
|
async fn a_burst_on_a_public_write_route_gets_429_with_retry_after_and_a_json_error() {
|
|
528
792
|
let addr = gated_server(limits_from(&[("HECKS_RATE_LIMIT_SUBSCRIBE", "3")])).await;
|
|
529
793
|
for attempt in 1..=3 {
|
|
530
|
-
assert_eq!(
|
|
794
|
+
assert_eq!(
|
|
795
|
+
post(addr, SUBSCRIBE, None, r#"{"email":"a@example.com"}"#)
|
|
796
|
+
.await
|
|
797
|
+
.status(),
|
|
798
|
+
200,
|
|
799
|
+
"request {attempt}"
|
|
800
|
+
);
|
|
531
801
|
}
|
|
532
802
|
|
|
533
803
|
let refused = post(addr, SUBSCRIBE, None, r#"{"email":"a@example.com"}"#).await;
|
|
534
804
|
assert_eq!(refused.status(), 429);
|
|
535
|
-
let retry_after: u64 = refused.headers()["retry-after"]
|
|
805
|
+
let retry_after: u64 = refused.headers()["retry-after"]
|
|
806
|
+
.to_str()
|
|
807
|
+
.unwrap()
|
|
808
|
+
.parse()
|
|
809
|
+
.unwrap();
|
|
536
810
|
assert!((1..=3600).contains(&retry_after), "got {retry_after}");
|
|
537
|
-
assert!(refused.headers()["content-type"]
|
|
811
|
+
assert!(refused.headers()["content-type"]
|
|
812
|
+
.to_str()
|
|
813
|
+
.unwrap()
|
|
814
|
+
.starts_with("application/json"));
|
|
538
815
|
let body: Value = refused.json().await.unwrap();
|
|
539
|
-
assert_eq!(
|
|
816
|
+
assert_eq!(
|
|
817
|
+
body,
|
|
818
|
+
serde_json::json!({ "error": "too many requests, please try again later" })
|
|
819
|
+
);
|
|
540
820
|
}
|
|
541
821
|
|
|
542
822
|
#[tokio::test]
|
|
543
823
|
async fn registrations_are_limited_apart_from_subscribes() {
|
|
544
|
-
let addr = gated_server(limits_from(&[
|
|
824
|
+
let addr = gated_server(limits_from(&[
|
|
825
|
+
("HECKS_RATE_LIMIT_SUBSCRIBE", "1"),
|
|
826
|
+
("HECKS_RATE_LIMIT_REGISTER", "2"),
|
|
827
|
+
]))
|
|
828
|
+
.await;
|
|
545
829
|
assert_eq!(post(addr, SUBSCRIBE, None, "{}").await.status(), 200);
|
|
546
830
|
assert_eq!(post(addr, SUBSCRIBE, None, "{}").await.status(), 429);
|
|
547
831
|
|
|
548
|
-
assert_eq!(
|
|
832
|
+
assert_eq!(
|
|
833
|
+
post(addr, "/registrations", None, "{}").await.status(),
|
|
834
|
+
200,
|
|
835
|
+
"subscribe being spent leaves register alone"
|
|
836
|
+
);
|
|
549
837
|
assert_eq!(post(addr, "/registrations", None, "{}").await.status(), 200);
|
|
550
838
|
assert_eq!(post(addr, "/registrations", None, "{}").await.status(), 429);
|
|
551
839
|
}
|
|
552
840
|
|
|
553
841
|
#[tokio::test]
|
|
554
842
|
async fn reads_and_other_routes_are_never_limited() {
|
|
555
|
-
let addr = gated_server(limits_from(&[
|
|
843
|
+
let addr = gated_server(limits_from(&[
|
|
844
|
+
("HECKS_RATE_LIMIT_SUBSCRIBE", "1"),
|
|
845
|
+
("HECKS_RATE_LIMIT_REGISTER", "1"),
|
|
846
|
+
]))
|
|
847
|
+
.await;
|
|
556
848
|
let client = reqwest::Client::new();
|
|
557
849
|
for _ in 0..5 {
|
|
558
|
-
assert_eq!(
|
|
559
|
-
|
|
560
|
-
|
|
850
|
+
assert_eq!(
|
|
851
|
+
client
|
|
852
|
+
.get(format!("http://{addr}{SUBSCRIBE}"))
|
|
853
|
+
.send()
|
|
854
|
+
.await
|
|
855
|
+
.unwrap()
|
|
856
|
+
.status(),
|
|
857
|
+
200
|
|
858
|
+
);
|
|
859
|
+
assert_eq!(
|
|
860
|
+
client
|
|
861
|
+
.get(format!("http://{addr}/registrations/REG-1"))
|
|
862
|
+
.send()
|
|
863
|
+
.await
|
|
864
|
+
.unwrap()
|
|
865
|
+
.status(),
|
|
866
|
+
200
|
|
867
|
+
);
|
|
868
|
+
assert_eq!(
|
|
869
|
+
post(addr, "/webhooks/stripe", None, "{}").await.status(),
|
|
870
|
+
200
|
|
871
|
+
);
|
|
561
872
|
assert_eq!(post(addr, "/members", None, "{}").await.status(), 200);
|
|
562
873
|
}
|
|
563
874
|
}
|
|
@@ -567,18 +878,46 @@ mod tests {
|
|
|
567
878
|
// The test client is the peer at 127.0.0.1 and nothing trusts it, so every header is
|
|
568
879
|
// typed by the caller and the count stays with the connection.
|
|
569
880
|
let addr = gated_server(limits_from(&[("HECKS_RATE_LIMIT_SUBSCRIBE", "1")])).await;
|
|
570
|
-
assert_eq!(
|
|
881
|
+
assert_eq!(
|
|
882
|
+
post(addr, SUBSCRIBE, Some("1.1.1.1"), "{}").await.status(),
|
|
883
|
+
200
|
|
884
|
+
);
|
|
571
885
|
for spoof in ["2.2.2.2", "3.3.3.3", "4.4.4.4, 5.5.5.5"] {
|
|
572
|
-
assert_eq!(
|
|
886
|
+
assert_eq!(
|
|
887
|
+
post(addr, SUBSCRIBE, Some(spoof), "{}").await.status(),
|
|
888
|
+
429,
|
|
889
|
+
"{spoof}"
|
|
890
|
+
);
|
|
573
891
|
}
|
|
574
892
|
}
|
|
575
893
|
|
|
576
894
|
#[tokio::test]
|
|
577
895
|
async fn behind_a_trusted_proxy_each_visitor_has_their_own_allowance() {
|
|
578
|
-
let addr = gated_server(limits_from(&[
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
896
|
+
let addr = gated_server(limits_from(&[
|
|
897
|
+
("HECKS_RATE_LIMIT_SUBSCRIBE", "1"),
|
|
898
|
+
("HECKS_TRUSTED_PROXIES", "127.0.0.1"),
|
|
899
|
+
]))
|
|
900
|
+
.await;
|
|
901
|
+
assert_eq!(
|
|
902
|
+
post(addr, SUBSCRIBE, Some("1.1.1.1, 203.0.113.7"), "{}")
|
|
903
|
+
.await
|
|
904
|
+
.status(),
|
|
905
|
+
200
|
|
906
|
+
);
|
|
907
|
+
assert_eq!(
|
|
908
|
+
post(addr, SUBSCRIBE, Some("2.2.2.2, 203.0.113.7"), "{}")
|
|
909
|
+
.await
|
|
910
|
+
.status(),
|
|
911
|
+
429,
|
|
912
|
+
"same visitor, new typed entry"
|
|
913
|
+
);
|
|
914
|
+
assert_eq!(
|
|
915
|
+
post(addr, SUBSCRIBE, Some("2.2.2.2, 198.51.100.4"), "{}")
|
|
916
|
+
.await
|
|
917
|
+
.status(),
|
|
918
|
+
200,
|
|
919
|
+
"a different visitor"
|
|
920
|
+
);
|
|
582
921
|
}
|
|
583
922
|
|
|
584
923
|
#[tokio::test]
|
|
@@ -587,7 +926,11 @@ mod tests {
|
|
|
587
926
|
let envelope = r#"{"requestContext":{"http":{"method":"POST"}},"rawPath":"/registrations","body":"{}"}"#;
|
|
588
927
|
assert_eq!(post(addr, "/anything", None, envelope).await.status(), 200);
|
|
589
928
|
assert_eq!(post(addr, "/anything", None, envelope).await.status(), 429);
|
|
590
|
-
assert_eq!(
|
|
929
|
+
assert_eq!(
|
|
930
|
+
post(addr, "/registrations", None, "{}").await.status(),
|
|
931
|
+
429,
|
|
932
|
+
"the same budget as the real path"
|
|
933
|
+
);
|
|
591
934
|
}
|
|
592
935
|
|
|
593
936
|
#[tokio::test]
|
|
@@ -601,7 +944,11 @@ mod tests {
|
|
|
601
944
|
|
|
602
945
|
#[tokio::test]
|
|
603
946
|
async fn turning_the_limits_off_lets_every_request_through() {
|
|
604
|
-
let addr = gated_server(limits_from(&[
|
|
947
|
+
let addr = gated_server(limits_from(&[
|
|
948
|
+
("HECKS_RATE_LIMIT", "off"),
|
|
949
|
+
("HECKS_RATE_LIMIT_SUBSCRIBE", "1"),
|
|
950
|
+
]))
|
|
951
|
+
.await;
|
|
605
952
|
for _ in 0..10 {
|
|
606
953
|
assert_eq!(post(addr, SUBSCRIBE, None, "{}").await.status(), 200);
|
|
607
954
|
}
|
|
@@ -611,16 +958,28 @@ mod tests {
|
|
|
611
958
|
// checkout fixture's wasm, so the first request is a real compile.
|
|
612
959
|
async fn fixture_host(database: &str, copy_name: &str) -> (SocketAddr, Arc<PathBuf>) {
|
|
613
960
|
let client = crate::dispatch::tests::scratch_db(database).await;
|
|
614
|
-
crate::dispatch::tests::provision_lineage(
|
|
961
|
+
crate::dispatch::tests::provision_lineage(
|
|
962
|
+
&*client.lock().await,
|
|
963
|
+
"CheckoutFixture",
|
|
964
|
+
1,
|
|
965
|
+
&["Event"],
|
|
966
|
+
)
|
|
967
|
+
.await;
|
|
615
968
|
let wasm_path = Arc::new(crate::wasm_runner::tests::cold_copy_of_fixture(copy_name));
|
|
616
969
|
let state = ServerState {
|
|
617
970
|
client: Arc::new(client),
|
|
618
971
|
wasm_path: Arc::clone(&wasm_path),
|
|
619
|
-
lineage_config: Arc::new(LineageConfig {
|
|
972
|
+
lineage_config: Arc::new(LineageConfig {
|
|
973
|
+
domain: "CheckoutFixture".to_string(),
|
|
974
|
+
era: Some(1),
|
|
975
|
+
mirrored: None,
|
|
976
|
+
}),
|
|
620
977
|
invoker: Arc::new(crate::lambda_client::NeverInvoker),
|
|
621
978
|
limits: limits_from(&[]),
|
|
622
979
|
};
|
|
623
|
-
let listener = tokio::net::TcpListener::bind(("127.0.0.1", 0))
|
|
980
|
+
let listener = tokio::net::TcpListener::bind(("127.0.0.1", 0))
|
|
981
|
+
.await
|
|
982
|
+
.unwrap();
|
|
624
983
|
let addr = listener.local_addr().unwrap();
|
|
625
984
|
tokio::spawn(async move { serve_on(listener, state, sample_version()).await });
|
|
626
985
|
(addr, wasm_path)
|
|
@@ -631,7 +990,12 @@ mod tests {
|
|
|
631
990
|
let requests: Vec<_> = (0..burst)
|
|
632
991
|
.map(|_| {
|
|
633
992
|
tokio::spawn(async move {
|
|
634
|
-
let response = reqwest::Client::new()
|
|
993
|
+
let response = reqwest::Client::new()
|
|
994
|
+
.post(format!("http://{addr}/dispatch"))
|
|
995
|
+
.body(r#"{"read": true}"#)
|
|
996
|
+
.send()
|
|
997
|
+
.await
|
|
998
|
+
.unwrap();
|
|
635
999
|
let status = StatusCode::from_u16(response.status().as_u16()).unwrap();
|
|
636
1000
|
(status, response.json::<Value>().await.unwrap())
|
|
637
1001
|
})
|
|
@@ -639,7 +1003,12 @@ mod tests {
|
|
|
639
1003
|
.collect();
|
|
640
1004
|
let mut answers = Vec::new();
|
|
641
1005
|
for request in requests {
|
|
642
|
-
answers.push(
|
|
1006
|
+
answers.push(
|
|
1007
|
+
tokio::time::timeout(std::time::Duration::from_secs(120), request)
|
|
1008
|
+
.await
|
|
1009
|
+
.expect("a read was never answered")
|
|
1010
|
+
.unwrap(),
|
|
1011
|
+
);
|
|
643
1012
|
}
|
|
644
1013
|
answers
|
|
645
1014
|
}
|
|
@@ -665,7 +1034,11 @@ mod tests {
|
|
|
665
1034
|
let answers = read_burst(addr, 1).await;
|
|
666
1035
|
assert_eq!(answers[0].0, StatusCode::OK, "{}", answers[0].1);
|
|
667
1036
|
assert_eq!(crate::wasm_runner::compile_count(&wasm_path), 1);
|
|
668
|
-
assert!(
|
|
1037
|
+
assert!(
|
|
1038
|
+
started.elapsed() < std::time::Duration::from_secs(1),
|
|
1039
|
+
"a warmed read took {:?}",
|
|
1040
|
+
started.elapsed()
|
|
1041
|
+
);
|
|
669
1042
|
}
|
|
670
1043
|
|
|
671
1044
|
#[test]
|
|
@@ -675,8 +1048,14 @@ mod tests {
|
|
|
675
1048
|
|
|
676
1049
|
#[test]
|
|
677
1050
|
fn build_falls_back_to_the_crate_version_when_unset_or_empty() {
|
|
678
|
-
assert_eq!(
|
|
679
|
-
|
|
1051
|
+
assert_eq!(
|
|
1052
|
+
version_body("e", "h", None)["build"],
|
|
1053
|
+
env!("CARGO_PKG_VERSION")
|
|
1054
|
+
);
|
|
1055
|
+
assert_eq!(
|
|
1056
|
+
version_body("e", "h", Some(""))["build"],
|
|
1057
|
+
env!("CARGO_PKG_VERSION")
|
|
1058
|
+
);
|
|
680
1059
|
}
|
|
681
1060
|
}
|
|
682
1061
|
|