hecks 3.14.0 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/examples/pizzas/adapters/fake_processor.rb +34 -0
- data/examples/pizzas/adapters/payment_signature.rb +66 -0
- data/examples/pizzas/adapters/payment_webhook.rb +87 -0
- data/examples/pizzas/bluebook/pizzas.behaviors +33 -0
- data/examples/pizzas/bluebook/pizzas.hecksagon +5 -3
- data/examples/pizzas/config.ru +22 -0
- data/exe/hecks +1 -1
- data/lib/hecks/adapters/driven/prism.rb +0 -2
- data/lib/hecks/adapters/driven.rb +0 -2
- data/lib/hecks/adapters/driving/json.rb +9 -16
- data/lib/hecks/bluebook/dsl/bootstrap_table.rb +2 -2
- data/lib/hecks/bluebook/dsl/derivation_words.rb +58 -0
- data/lib/hecks/bluebook/dsl/translation_builder.rb +6 -37
- data/lib/hecks/bluebook/expression/ast_json.rb +0 -9
- data/lib/hecks/bluebook/hecksagon.rb +1 -1
- data/lib/hecks/bluebook/meta_validator/translation_judge/rules.rb +7 -10
- data/lib/hecks/bluebook/meta_validator/translation_judge.rb +4 -8
- data/lib/hecks/bluebook/meta_validator.rb +0 -11
- data/lib/hecks/bluebook/translation.rb +18 -10
- data/lib/hecks/bluebook/translation_expression/evaluator.rb +63 -0
- data/lib/hecks/bluebook/translation_expression/parser.rb +127 -0
- data/lib/hecks/bluebook/translation_expression.rb +39 -0
- data/lib/hecks/bluebook/translation_plan.rb +70 -0
- data/lib/hecks/bluebook.rb +2 -0
- data/lib/hecks/chapters.rb +3 -3
- data/lib/hecks/cli/project_cli/launcher_source.rb +1 -1
- data/lib/hecks/composer/adapters/claude_composer.adapter +3 -0
- data/lib/hecks/composer/adapters/claude_composer.rb +83 -0
- data/lib/hecks/composer/adapters/fake_agent.adapter +3 -0
- data/lib/hecks/composer/adapters/fake_agent.rb +55 -0
- data/lib/hecks/composer/adapters/jev.adapter +3 -0
- data/lib/hecks/composer/adapters/jev.rb +66 -0
- data/lib/hecks/composer/agent.rb +113 -0
- data/lib/hecks/composer/assistant.rb +110 -0
- data/lib/hecks/composer/behavior_run.rb +53 -0
- data/lib/hecks/composer/bluebook/composer.bluebook +178 -0
- data/lib/hecks/composer/bluebook/composer.hecksagon +16 -0
- data/lib/hecks/composer/board.rb +65 -0
- data/lib/hecks/composer/board_edit/command_text.rb +39 -0
- data/lib/hecks/composer/board_edit/source.rb +84 -0
- data/lib/hecks/composer/board_edit.rb +144 -0
- data/lib/hecks/composer/check.rb +89 -0
- data/lib/hecks/composer/domains.rb +81 -0
- data/lib/hecks/composer/jobs.rb +134 -0
- data/lib/hecks/composer/layout.rb +37 -0
- data/lib/hecks/composer/live.rb +150 -0
- data/lib/hecks/composer/outline.rb +48 -0
- data/lib/hecks/composer/page.html +989 -0
- data/lib/hecks/composer/panels.rb +142 -0
- data/lib/hecks/composer/parse.rb +83 -0
- data/lib/hecks/composer/plain.rb +28 -0
- data/lib/hecks/composer/ports/composer_agent.port +7 -0
- data/lib/hecks/composer/session.rb +141 -0
- data/lib/hecks/composer/toolbox.rb +166 -0
- data/lib/hecks/composer/web.rb +156 -0
- data/lib/hecks/composer/workbench.rb +51 -0
- data/lib/hecks/composer/workspace.rb +81 -0
- data/lib/hecks/composer.rb +17 -0
- data/lib/hecks/corpus/adapters.rb +22 -0
- data/lib/hecks/corpus/rust.rb +1 -1
- data/lib/hecks/corpus/worlds.rb +19 -0
- data/lib/hecks/corpus.rb +5 -0
- data/lib/hecks/forms/app/commands.rb +2 -3
- data/lib/hecks/forms/app/queries.rb +2 -3
- data/lib/hecks/forms/app/records.rb +17 -5
- data/lib/hecks/forms/app/responses.rb +3 -1
- data/lib/hecks/forms/command_form_renderer.rb +42 -16
- data/lib/hecks/forms/field_controls.rb +2 -2
- data/lib/hecks/forms/field_renderer.rb +1 -1
- data/lib/hecks/forms/field_shape.rb +36 -17
- data/lib/hecks/forms/index_renderer.rb +2 -1
- data/lib/hecks/forms/page.rb +1 -1
- data/lib/hecks/forms/params.rb +0 -13
- data/lib/hecks/forms/query_form_renderer.rb +7 -6
- data/lib/hecks/forms/record_renderer.rb +11 -7
- data/lib/hecks/forms/rule_words.rb +100 -0
- data/lib/hecks/forms/value_object_shape.rb +2 -1
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +0 -3
- data/lib/hecks/fuzzing/isolated_boot/worlds.rb +3 -6
- data/lib/hecks/fuzzing/replay/filters.rb +2 -6
- data/lib/hecks/gate/stages.yml +25 -0
- data/lib/hecks/grammar/evolve/seeds.rb +0 -6
- data/lib/hecks/grammar/evolve.rb +0 -3
- data/lib/hecks/grammar/translation.bluebook +1 -1
- data/lib/hecks/hecks/adapters/codebase/composing.rb +56 -0
- data/lib/hecks/hecks/adapters/codebase/source_tree.rb +3 -1
- data/lib/hecks/hecks/adapters/first_admin.rb +3 -3
- data/lib/hecks/hecks/adapters/journal_store/changes.rb +0 -26
- data/lib/hecks/hecks/adapters/journal_store/examination.rb +2 -23
- data/lib/hecks/hecks/adapters/journal_store/readings.rb +3 -11
- data/lib/hecks/hecks/adapters/journal_store.rb +0 -11
- data/lib/hecks/hecks/adapters/rehearsal/inquiry.rb +4 -11
- data/lib/hecks/hecks/adapters/rehearsal/memory_store.rb +0 -9
- data/lib/hecks/hecks/adapters/rehearsal/postgres_reading.rb +0 -4
- data/lib/hecks/hecks/codebase.behaviors +6 -0
- data/lib/hecks/hecks/codebase.bluebook +21 -0
- data/lib/hecks/hecks/composer.behaviors +68 -0
- data/lib/hecks/hecks/context_map.hecksagon +1 -1
- data/lib/hecks/hecks/custodian.behaviors +0 -6
- data/lib/hecks/hecks/custodian.bluebook +3 -80
- data/lib/hecks/hecks/hecks.hecksagon +2 -2
- data/lib/hecks/hecks/hecks.world +1 -1
- data/lib/hecks/hecks/rehearsal.bluebook +7 -13
- data/lib/hecks/kernel_capabilities/templates.rb +4 -4
- data/lib/hecks/kernel_capabilities.rb +1 -1
- data/lib/hecks/language/bluebook/vocabulary.bluebook +2 -3
- data/lib/hecks/language/translation/translation_aggregate.bluebook +21 -24
- data/lib/hecks/ports/access_control.rb +2 -2
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +3 -41
- data/lib/hecks/ports/persistence/plugins/era/lineage/coverage.rb +7 -7
- data/lib/hecks/ports/persistence/plugins/era/lineage/rewrite.rb +25 -2
- data/lib/hecks/ports/persistence/plugins/era/lineage/rules.rb +4 -4
- data/lib/hecks/ports/persistence/plugins/era/lineage.rb +17 -25
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/base_sql.rb +0 -12
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/domain_rename.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +2 -22
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +0 -1
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +9 -3
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +3 -29
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/refusals.rb +2 -26
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +6 -33
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +3 -3
- data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +2 -4
- data/lib/hecks/ports/persistence/plugins/era/translation/expression_sql.rb +52 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +23 -29
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/translation.rb +0 -1
- data/lib/hecks/ports/persistence/plugins/era.rb +3 -6
- data/lib/hecks/ports.rb +0 -1
- data/lib/hecks/projection_files.rb +3 -1
- data/lib/hecks/projections/deploy/templates/lambda/nat_comment_oauth.tmpl +1 -1
- data/lib/hecks/projections/glossary/entries.rb +10 -4
- data/lib/hecks/projections/glossary/groups.rb +42 -0
- data/lib/hecks/projections/glossary/html/page.rb +7 -2
- data/lib/hecks/projections/glossary/html/parser.rb +1 -1
- data/lib/hecks/projections/glossary/html/sections.rb +31 -11
- data/lib/hecks/projections/glossary/html.rb +1 -2
- data/lib/hecks/projections/glossary/markdown.rb +31 -28
- data/lib/hecks/projections/glossary/mermaid.rb +3 -0
- data/lib/hecks/projections/glossary/page.css +143 -153
- data/lib/hecks/projections/glossary/page.js +63 -3
- data/lib/hecks/projections/glossary/sentences/actions.rb +88 -0
- data/lib/hecks/projections/glossary/sentences/paths.rb +77 -0
- data/lib/hecks/projections/glossary/sentences/queries.rb +90 -0
- data/lib/hecks/projections/glossary/sentences/value_objects.rb +32 -5
- data/lib/hecks/projections/glossary/sentences.rb +13 -3
- data/lib/hecks/projections/glossary/slugs.rb +4 -1
- data/lib/hecks/projections/glossary.rb +2 -1
- data/lib/hecks/projections/plain_english.rb +97 -0
- data/lib/hecks/projections/rust_vocabulary/arguments.rb +2 -2
- data/lib/hecks/projections/rust_vocabulary/enum_body.rb +20 -25
- data/lib/hecks/projections/rust_vocabulary/tests.rb +2 -2
- data/lib/hecks/projections/rust_vocabulary.rb +14 -6
- data/lib/hecks/projections/site/cms_editor/color.rb +0 -6
- data/lib/hecks/projections/site/cms_editor/templates/src/host.ts.tmpl +7 -2
- data/lib/hecks/projections/statements/actions.rb +99 -0
- data/lib/hecks/projections/statements/relationships.rb +86 -0
- data/lib/hecks/projections/statements.rb +15 -24
- data/lib/hecks/projector/exporter/adapters.rb +112 -0
- data/lib/hecks/projector/exporter/translations.rb +18 -35
- data/lib/hecks/projector/exporter/worlds.rb +103 -0
- data/lib/hecks/projector/exporter.rb +5 -1
- data/lib/hecks/quality_control/adapters/agent.rb +6 -7
- data/lib/hecks/quality_control/adapters/agent_profile.rb +16 -114
- data/lib/hecks/quality_control/cli/qa_mine_combinations.rb +1 -1
- data/lib/hecks/runtime/loader/boot_steps.rb +0 -20
- data/lib/hecks/runtime/outbox.rb +2 -9
- data/lib/hecks/runtime/port_operation_interpreter.rb +1 -9
- data/lib/hecks/runtime/remote_dispatcher.rb +2 -2
- data/lib/hecks/runtime/value/unwrapping.rb +60 -0
- data/lib/hecks/runtime/value.rb +10 -21
- data/lib/hecks/rust_build/project_rust/target_ir.rb +23 -4
- data/lib/hecks/rust_build/wasm.rb +0 -12
- data/lib/hecks/rust_build/write_if_changed.rb +12 -12
- data/lib/hecks/rust_format.rb +77 -0
- data/lib/hecks/tools/fuzz_sweep/findings.rb +3 -9
- data/lib/hecks/tools/fuzz_sweep.rb +1 -3
- data/lib/hecks/tools/translation_audit/verdicts.rb +3 -28
- data/lib/hecks/tools/translation_audit.rb +8 -10
- data/lib/hecks/tools/translation_scaffold/reporting.rb +1 -1
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks/vocabulary.rb +2 -1
- data/lib/hecks.rb +3 -0
- data/prebuilt/{1a6f596be124228be93eba1d86dbc9d2933190fa389b284331241cb9f27115a0.marshal → ad68f9e06c8068c2a4176b57abf3083d591d5d8980c2b3e897b97331f965b643.marshal} +0 -0
- data/prebuilt/{a883a7e568667aa5ac904c82daff356bdf9e996252a36af6def4a4a43efb967e.marshal → ee22ec8642c4006717ba2b80f46e5efb81de459eef9dfec043e838c0ad4fd720.marshal} +0 -0
- data/prebuilt/{verdicts-872993b1aa26d73fe48f28251374b8585fed145d02aab42be0f00de386b6ff80.json → verdicts-b69f5dcbba79c20daa0493476fa7c317004a30aa2bc3d3dc6a722ea70ae2b43b.json} +1 -1
- data/rust/Cargo.toml +40 -0
- data/rust/build/Cargo.toml +36 -0
- data/rust/build/clippy.toml +4 -0
- data/rust/build/src/adapters_pass.rs +196 -0
- data/rust/build/src/binding_pass.rs +54 -13
- data/rust/build/src/build_artifact.rs +23 -5
- data/rust/build/src/cargo_sync.rs +69 -26
- data/rust/build/src/lineage_pass.rs +101 -87
- data/rust/build/src/main.rs +11 -2
- data/rust/build/src/pipeline.rs +80 -19
- data/rust/build/src/reserved_names.rs +6 -50
- data/rust/build/src/resolve.rs +82 -35
- data/rust/build/src/rustfmt.rs +100 -0
- data/rust/build/src/seams_pass.rs +85 -16
- data/rust/build/src/subprocess.rs +30 -6
- data/rust/build/src/tmp.rs +5 -2
- data/rust/build/src/translation_pass.rs +72 -22
- data/rust/build/src/world_pass.rs +109 -0
- data/rust/clippy.toml +4 -0
- data/rust/codegen/Cargo.toml +36 -0
- data/rust/codegen/clippy.toml +4 -0
- data/rust/codegen/src/asks.rs +40 -14
- data/rust/codegen/src/attr.rs +2 -2
- data/rust/codegen/src/bridging.rs +676 -117
- data/rust/codegen/src/commands.rs +979 -373
- data/rust/codegen/src/constraints.rs +88 -25
- data/rust/codegen/src/dependency_planning.rs +128 -34
- data/rust/codegen/src/domain_generator.rs +306 -170
- data/rust/codegen/src/exemplar.rs +87 -32
- data/rust/codegen/src/expr_emitter.rs +54 -6
- data/rust/codegen/src/fielded.rs +193 -41
- data/rust/codegen/src/hecks_naming.rs +124 -16
- data/rust/codegen/src/hecks_naming_tests.rs +81 -0
- data/rust/codegen/src/json.rs +80 -44
- data/rust/codegen/src/json_codec.rs +585 -149
- data/rust/codegen/src/literal.rs +104 -138
- data/rust/codegen/src/main.rs +151 -52
- data/rust/codegen/src/manifest.rs +76 -14
- data/rust/codegen/src/mutations.rs +1212 -239
- data/rust/codegen/src/naming.rs +111 -60
- data/rust/codegen/src/optional_pass.rs +90 -28
- data/rust/codegen/src/ports.rs +5 -4
- data/rust/codegen/src/prelude.rs +139 -49
- data/rust/codegen/src/presenters.rs +335 -0
- data/rust/codegen/src/queries.rs +601 -226
- data/rust/codegen/src/reactions.rs +268 -74
- data/rust/codegen/src/read_models.rs +574 -229
- data/rust/codegen/src/reference_specs.rs +16 -3
- data/rust/codegen/src/registry.rs +128 -79
- data/rust/codegen/src/reserved_names.rs +6 -50
- data/rust/codegen/src/shared.rs +71 -22
- data/rust/codegen/src/sidecars.rs +15 -5
- data/rust/codegen/src/skip_reason.rs +8 -2
- data/rust/codegen/src/types.rs +306 -108
- data/rust/host/Cargo.lock +1 -0
- data/rust/host/Cargo.toml +40 -4
- data/rust/host/HECKS_RELEASE +1 -1
- data/rust/host/clippy.toml +4 -0
- data/rust/host/src/api.rs +698 -218
- data/rust/host/src/auth.rs +491 -214
- data/rust/host/src/b64.rs +91 -0
- data/rust/host/src/bin/expr_harness.rs +6 -2
- data/rust/host/src/bin/lineage_harness.rs +47 -11
- data/rust/host/src/bin/mint_harness.rs +70 -37
- data/rust/host/src/bin/validate_harness.rs +16 -8
- data/rust/host/src/boot.rs +144 -68
- data/rust/host/src/boundary_fuzz/api.rs +130 -32
- data/rust/host/src/boundary_fuzz/auth.rs +108 -23
- data/rust/host/src/boundary_fuzz/expr_json.rs +85 -11
- data/rust/host/src/boundary_fuzz/ir.rs +164 -12
- data/rust/host/src/boundary_fuzz/server.rs +283 -36
- data/rust/host/src/boundary_fuzz/web.rs +56 -8
- data/rust/host/src/checkout.rs +78 -32
- data/rust/host/src/clock.rs +8 -0
- data/rust/host/src/commerce_ir.rs +333 -95
- data/rust/host/src/crypto.rs +33 -0
- data/rust/host/src/dispatch.rs +1037 -331
- data/rust/host/src/expr_json.rs +1021 -273
- data/rust/host/src/extension.rs +24 -4
- data/rust/host/src/field_hints.rs +11 -7
- data/rust/host/src/fuzz_support.rs +85 -13
- data/rust/host/src/host.rs +100 -0
- data/rust/host/src/http.rs +22 -0
- data/rust/host/src/ir.rs +326 -16
- data/rust/host/src/journal.rs +277 -176
- data/rust/host/src/lambda_client.rs +219 -53
- data/rust/host/src/lib.rs +16 -4
- data/rust/host/src/log.rs +62 -11
- data/rust/host/src/mint.rs +1153 -326
- data/rust/host/src/needs.rs +185 -44
- data/rust/host/src/payments/keystore.rs +197 -45
- data/rust/host/src/payments/roles.rs +39 -7
- data/rust/host/src/payments/tests.rs +1427 -282
- data/rust/host/src/payments.rs +391 -112
- data/rust/host/src/platform_signin/attack_tests.rs +5 -5
- data/rust/host/src/platform_signin/config.rs +11 -11
- data/rust/host/src/platform_signin/cookie.rs +2 -2
- data/rust/host/src/platform_signin/extension.rs +2 -1
- data/rust/host/src/platform_signin/handler.rs +9 -8
- data/rust/host/src/platform_signin/jti.rs +4 -2
- data/rust/host/src/platform_signin/jwks.rs +2 -1
- data/rust/host/src/platform_signin/jws.rs +2 -37
- data/rust/host/src/platform_signin/known_answer_tests.rs +5 -1
- data/rust/host/src/platform_signin/mode_tests.rs +5 -3
- data/rust/host/src/platform_signin/pages.rs +20 -20
- data/rust/host/src/platform_signin/session.rs +8 -1
- data/rust/host/src/platform_signin/settings.rs +6 -6
- data/rust/host/src/platform_signin/strict_json.rs +12 -9
- data/rust/host/src/platform_signin/verify.rs +41 -41
- data/rust/host/src/platform_signin/wiring_support.rs +13 -18
- data/rust/host/src/presentation.rs +207 -63
- data/rust/host/src/presentation_write.rs +502 -187
- data/rust/host/src/query_step.rs +69 -18
- data/rust/host/src/rate_limit.rs +541 -142
- data/rust/host/src/reference_transform.rs +77 -25
- data/rust/host/src/reference_validate.rs +443 -104
- data/rust/host/src/resend.rs +190 -55
- data/rust/host/src/secrets.rs +116 -21
- data/rust/host/src/server.rs +555 -104
- data/rust/host/src/storage_shape.rs +88 -21
- data/rust/host/src/test_pg.rs +17 -7
- data/rust/host/src/ui_schema.rs +495 -207
- data/rust/host/src/wasm_runner.rs +21 -6
- data/rust/host/src/web/commerce.rs +101 -23
- data/rust/host/src/web/newsletter/cooldown.rs +10 -3
- data/rust/host/src/web/newsletter/template.rs +26 -7
- data/rust/host/src/web/newsletter/tests.rs +112 -23
- data/rust/host/src/web/newsletter.rs +209 -59
- data/rust/host/src/web/newsletter_send.rs +251 -79
- data/rust/host/src/web/registration_receipt/tests.rs +16 -4
- data/rust/host/src/web/registration_receipt.rs +100 -24
- data/rust/host/src/web/registrations.rs +1384 -332
- data/rust/host/src/web/resend_webhook.rs +138 -80
- data/rust/host/src/web.rs +2416 -508
- data/rust/lsp/Cargo.toml +36 -0
- data/rust/lsp/clippy.toml +4 -0
- data/rust/lsp/src/json.rs +29 -22
- data/rust/lsp/src/main.rs +81 -31
- data/rust/lsp/src/outline.rs +23 -7
- data/rust/lsp/src/rpc.rs +17 -4
- data/rust/parser/Cargo.toml +36 -0
- data/rust/parser/clippy.toml +4 -0
- data/rust/parser/src/build/closed_sets.rs +6 -1
- data/rust/parser/src/build/identity.rs +4 -3
- data/rust/parser/src/build/mod.rs +1 -1
- data/rust/parser/src/build/named_pattern.rs +4 -1
- data/rust/parser/src/build/naming.rs +2 -80
- data/rust/parser/src/build/pattern_subset.rs +6 -8
- data/rust/parser/src/build/query_derive.rs +3 -1
- data/rust/parser/src/build/query_inference.rs +14 -5
- data/rust/parser/src/build/read_model.rs +4 -1
- data/rust/parser/src/build/references.rs +4 -3
- data/rust/parser/src/canonical.rs +34 -15
- data/rust/parser/src/emit.rs +187 -105
- data/rust/parser/src/expr/ast_json.rs +134 -42
- data/rust/parser/src/expr/evaluator.rs +27 -9
- data/rust/parser/src/expr/mod.rs +54 -13
- data/rust/parser/src/expr/resolver.rs +299 -92
- data/rust/parser/src/ir.rs +35 -3
- data/rust/parser/src/keywords.rs +7168 -516
- data/rust/parser/src/lex.rs +11 -25
- data/rust/parser/src/lib.rs +1 -0
- data/rust/parser/src/main.rs +65 -6
- data/rust/parser/src/parse/adapter.rs +157 -0
- data/rust/parser/src/parse/aggregate.rs +19 -10
- data/rust/parser/src/parse/chapter.rs +16 -17
- data/rust/parser/src/parse/command.rs +35 -16
- data/rust/parser/src/parse/domain_port.rs +4 -7
- data/rust/parser/src/parse/entity.rs +21 -11
- data/rust/parser/src/parse/hecksagon.rs +10 -4
- data/rust/parser/src/parse/lifecycle.rs +9 -5
- data/rust/parser/src/parse/mod.rs +22 -91
- data/rust/parser/src/parse/needs.rs +21 -12
- data/rust/parser/src/parse/policy.rs +2 -2
- data/rust/parser/src/parse/process_manager.rs +9 -11
- data/rust/parser/src/parse/query.rs +8 -4
- data/rust/parser/src/parse/read_model.rs +57 -16
- data/rust/parser/src/parse/value_object.rs +7 -2
- data/rust/parser/src/parse/world.rs +322 -0
- data/rust/parser/src/ruby_value.rs +7 -6
- data/rust/src/exemplar/json.rs +4 -4
- data/rust/src/exemplar/mutations.rs +18 -0
- data/rust/src/exemplar/read_models.rs +1 -10
- data/rust/src/kernel/attribute_shapes/composite.rs +54 -14
- data/rust/src/kernel/attribute_shapes/list.rs +2 -2
- data/rust/src/kernel/attribute_shapes/mod.rs +8 -8
- data/rust/src/kernel/attribute_shapes/scalar.rs +2 -2
- data/rust/src/kernel/cli.rs +244 -114
- data/rust/src/kernel/dispatch.rs +323 -115
- data/rust/src/kernel/dispatch_guard_tests.rs +327 -65
- data/rust/src/kernel/expr.rs +203 -86
- data/rust/src/kernel/expression_operators/arithmetic.rs +138 -25
- data/rust/src/kernel/expression_operators/comparison.rs +87 -2
- data/rust/src/kernel/expression_operators/enumeration.rs +106 -22
- data/rust/src/kernel/expression_operators/logical.rs +9 -3
- data/rust/src/kernel/expression_operators/membership.rs +3 -1
- data/rust/src/kernel/expression_operators/mod.rs +34 -21
- data/rust/src/kernel/expression_operators/pattern_match.rs +46 -11
- data/rust/src/kernel/expression_operators/positional.rs +84 -30
- data/rust/src/kernel/expression_operators/presence.rs +52 -13
- data/rust/src/kernel/expression_operators/sign_test.rs +3 -1
- data/rust/src/kernel/expression_operators/sized.rs +24 -8
- data/rust/src/kernel/expression_operators/text.rs +120 -27
- data/rust/src/kernel/expression_operators/to_string.rs +10 -4
- data/rust/src/kernel/json.rs +244 -148
- data/rust/src/kernel/mod.rs +47 -33
- data/rust/src/kernel/named_query.rs +63 -15
- data/rust/src/kernel/needs.rs +112 -25
- data/rust/src/kernel/orchestrate.rs +980 -581
- data/rust/src/kernel/orchestrate_guard_tests.rs +538 -140
- data/rust/src/kernel/pattern.rs +107 -34
- data/rust/src/kernel/present.rs +261 -0
- data/rust/src/kernel/query_comparators.rs +118 -41
- data/rust/src/kernel/query_ordering.rs +13 -5
- data/rust/src/kernel/read_model.rs +479 -181
- data/rust/src/kernel/reference_lookup.rs +122 -26
- data/rust/src/kernel/repository.rs +293 -66
- data/rust/src/kernel/routing.rs +58 -42
- data/rust/src/kernel/ruby_float.rs +139 -0
- data/rust/src/kernel/vocab/aggregate_dispatch_order.rs +57 -57
- data/rust/src/kernel/vocab/entity_dispatch_order.rs +54 -54
- data/rust/src/kernel/vocab/field_hint.rs +19 -25
- data/rust/src/kernel/vocab/mod.rs +6 -6
- data/rust/src/kernel/vocab/query_comparator.rs +23 -24
- data/rust/src/kernel/vocab/refusal_template.rs +501 -297
- data/rust/src/lib.rs +50 -0
- data/rust/src/main.rs +4 -1
- data/rust/web/Cargo.toml +36 -0
- data/rust/web/clippy.toml +4 -0
- metadata +85 -15
- data/examples/pizzas/bluebook/hecksagon/mock_stripe_payment_adapter.rb +0 -36
- data/lib/hecks/adapters/driven/google_authentication.adapter +0 -3
- data/lib/hecks/adapters/driven/google_authentication.rb +0 -99
- data/lib/hecks/adapters/driven/mock_stripe_adapter.adapter +0 -3
- data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +0 -32
- data/lib/hecks/ports/authentication.port +0 -6
- data/lib/hecks/ports/authentication.rb +0 -67
- data/lib/hecks/ports/persistence/plugins/era/translation/approval_file.rb +0 -255
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +0 -22
- data/rust/host/src/approval.rs +0 -652
data/rust/host/src/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,63 @@ 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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
)
|
|
34
|
-
if let Some(response) = web::render(&body, client, wasm_path, config, invoker).await {
|
|
28
|
+
///
|
|
29
|
+
/// `"bare": true` beside any of them (`verb` with `to`/`with` or `args`, `read`, `query`) answers
|
|
30
|
+
/// the same outcome with each single-field value object as its bare field
|
|
31
|
+
/// (`{"name": "Margherita"}`, not `{"name": {"value": "Margherita"}}`). What is stored is the
|
|
32
|
+
/// same either way; without `bare` the answer is the stored shape.
|
|
33
|
+
pub async fn dispatch_body(body: Value, host: Host<'_>) -> Result<Value, Error> {
|
|
34
|
+
if let Some(response) = web::render(&body, host).await {
|
|
35
35
|
return Ok(response);
|
|
36
36
|
}
|
|
37
|
+
let answer = kernel_answer(&body, host).await?;
|
|
38
|
+
if !wants_bare(&body) {
|
|
39
|
+
return Ok(answer);
|
|
40
|
+
}
|
|
41
|
+
Ok(dispatch::present(host.wasm_path, answer)
|
|
42
|
+
.await
|
|
43
|
+
.map_err(|e| format!("{e:#}"))?)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
fn wants_bare(body: &Value) -> bool {
|
|
47
|
+
body.get("bare").and_then(Value::as_bool) == Some(true)
|
|
48
|
+
}
|
|
37
49
|
|
|
38
|
-
|
|
50
|
+
// The kernel's outcome for a read, query or command body, in the stored shape.
|
|
51
|
+
async fn kernel_answer(body: &Value, host: Host<'_>) -> Result<Value, Error> {
|
|
52
|
+
let Host {
|
|
53
|
+
client,
|
|
54
|
+
wasm_path,
|
|
55
|
+
config,
|
|
56
|
+
..
|
|
57
|
+
} = host;
|
|
58
|
+
|
|
59
|
+
if body.get("read").and_then(serde_json::Value::as_bool) == Some(true) {
|
|
39
60
|
// `{e:#}`: anyhow's alternate Display walks `source()` to the real message,
|
|
40
61
|
// where a bare `tokio_postgres` error's Display is just "db error".
|
|
41
|
-
let result = dispatch::read(client, wasm_path)
|
|
62
|
+
let result = dispatch::read(client, wasm_path)
|
|
63
|
+
.await
|
|
64
|
+
.map_err(|e| format!("{e:#}"))?;
|
|
42
65
|
return Ok(result);
|
|
43
66
|
}
|
|
44
67
|
|
|
45
68
|
// A query step reads current state and writes nothing to the journal.
|
|
46
69
|
if body.get("query").is_some() {
|
|
47
|
-
return Ok(crate::query_step::answer(
|
|
70
|
+
return Ok(crate::query_step::answer(
|
|
71
|
+
body,
|
|
72
|
+
crate::ir::ir(),
|
|
73
|
+
&config.domain,
|
|
74
|
+
client,
|
|
75
|
+
wasm_path,
|
|
76
|
+
)
|
|
77
|
+
.await?);
|
|
48
78
|
}
|
|
49
79
|
|
|
50
80
|
let verb = body
|
|
@@ -52,30 +82,45 @@ pub async fn dispatch_body(
|
|
|
52
82
|
.and_then(|v| v.as_str())
|
|
53
83
|
.ok_or("event missing \"verb\"")?
|
|
54
84
|
.to_string();
|
|
55
|
-
let role = body
|
|
85
|
+
let role = body
|
|
86
|
+
.get("role")
|
|
87
|
+
.and_then(|v| v.as_str())
|
|
88
|
+
.map(std::string::ToString::to_string);
|
|
56
89
|
// Who the caller is, for Governance. Honored only where the internal protocol is: from this
|
|
57
90
|
// host's own peers (`trusts_internal_dispatch`), never from a public caller.
|
|
58
|
-
let actor_id = body
|
|
59
|
-
|
|
91
|
+
let actor_id = body
|
|
92
|
+
.get("actor_id")
|
|
93
|
+
.and_then(|v| v.as_str())
|
|
94
|
+
.map(std::string::ToString::to_string);
|
|
95
|
+
let caller = dispatch::Caller {
|
|
96
|
+
role: role.as_deref(),
|
|
97
|
+
actor_id: actor_id.as_deref(),
|
|
98
|
+
};
|
|
60
99
|
|
|
61
100
|
let outcome = if let Some(to) = body.get("to").cloned() {
|
|
62
101
|
if body.get("args").is_some() {
|
|
63
102
|
return Err("cannot combine to/with with legacy args".into());
|
|
64
103
|
}
|
|
65
|
-
let facts = body
|
|
66
|
-
|
|
104
|
+
let facts = body
|
|
105
|
+
.get("with")
|
|
106
|
+
.cloned()
|
|
107
|
+
.unwrap_or_else(|| serde_json::json!({}));
|
|
108
|
+
dispatch::handle_routed_as(host, &verb, to, facts, caller)
|
|
67
109
|
.await
|
|
68
110
|
.map_err(|e| format!("{e:#}"))?
|
|
69
111
|
} else if let Some(facts) = body.get("with").cloned() {
|
|
70
112
|
if body.get("args").is_some() {
|
|
71
113
|
return Err("cannot combine \"with\" with legacy args".into());
|
|
72
114
|
}
|
|
73
|
-
dispatch::handle_facts_as(
|
|
115
|
+
dispatch::handle_facts_as(host, &verb, facts, caller)
|
|
74
116
|
.await
|
|
75
117
|
.map_err(|e| format!("{e:#}"))?
|
|
76
118
|
} else {
|
|
77
|
-
let args = body
|
|
78
|
-
|
|
119
|
+
let args = body
|
|
120
|
+
.get("args")
|
|
121
|
+
.cloned()
|
|
122
|
+
.unwrap_or_else(|| serde_json::json!({}));
|
|
123
|
+
dispatch::handle_as(host, &verb, args, caller)
|
|
79
124
|
.await
|
|
80
125
|
.map_err(|e| format!("{e:#}"))?
|
|
81
126
|
};
|
|
@@ -93,10 +138,24 @@ pub struct ServerState {
|
|
|
93
138
|
pub limits: Arc<RateLimits>,
|
|
94
139
|
}
|
|
95
140
|
|
|
141
|
+
impl ServerState {
|
|
142
|
+
/// The shared pieces a dispatch needs, borrowed for one call.
|
|
143
|
+
pub fn host(&self) -> Host<'_> {
|
|
144
|
+
Host::shared(
|
|
145
|
+
&self.client,
|
|
146
|
+
&self.wasm_path,
|
|
147
|
+
&self.lineage_config,
|
|
148
|
+
&self.invoker,
|
|
149
|
+
)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
96
153
|
/// The document `GET /version` serves: era, IR hash, and build — all fixed for the life of
|
|
97
154
|
/// the process, so this is built once at boot. Carries no secrets.
|
|
98
155
|
pub fn version_body(era: &str, ir_hash: &str, build_env: Option<&str>) -> Value {
|
|
99
|
-
let build = build_env
|
|
156
|
+
let build = build_env
|
|
157
|
+
.filter(|v| !v.is_empty())
|
|
158
|
+
.unwrap_or(env!("CARGO_PKG_VERSION"));
|
|
100
159
|
serde_json::json!({ "era": era, "ir_hash": ir_hash, "build": build })
|
|
101
160
|
}
|
|
102
161
|
|
|
@@ -104,13 +163,19 @@ pub fn version_body(era: &str, ir_hash: &str, build_env: Option<&str>) -> Value
|
|
|
104
163
|
/// request answers a clone of the boot-time document, never touching auth or dispatch.
|
|
105
164
|
pub fn version_router(body: Value) -> Router {
|
|
106
165
|
let body = Arc::new(body);
|
|
107
|
-
Router::new().route(
|
|
166
|
+
Router::new().route(
|
|
167
|
+
"/version",
|
|
168
|
+
get(move || std::future::ready(axum::Json(body.as_ref().clone()))),
|
|
169
|
+
)
|
|
108
170
|
}
|
|
109
171
|
|
|
110
172
|
/// Compiles the domain's wasm module, then binds `$PORT` (falls back to 8080) and serves
|
|
111
173
|
/// forever. Nothing listens until the compile finishes, so the health check never lies.
|
|
112
174
|
pub async fn serve(state: ServerState, version: Value) -> Result<(), Error> {
|
|
113
|
-
let port: u16 = std::env::var("PORT")
|
|
175
|
+
let port: u16 = std::env::var("PORT")
|
|
176
|
+
.ok()
|
|
177
|
+
.and_then(|v| v.parse().ok())
|
|
178
|
+
.unwrap_or(8080);
|
|
114
179
|
|
|
115
180
|
warm_wasm(&state.wasm_path).await;
|
|
116
181
|
|
|
@@ -122,7 +187,11 @@ pub async fn serve(state: ServerState, version: Value) -> Result<(), Error> {
|
|
|
122
187
|
|
|
123
188
|
/// The router and serve loop over an already-bound `listener`, split out so a test can
|
|
124
189
|
/// run the real service on an ephemeral port.
|
|
125
|
-
async fn serve_on(
|
|
190
|
+
async fn serve_on(
|
|
191
|
+
listener: tokio::net::TcpListener,
|
|
192
|
+
state: ServerState,
|
|
193
|
+
version: Value,
|
|
194
|
+
) -> Result<(), Error> {
|
|
126
195
|
let app = Router::new()
|
|
127
196
|
.route("/", get(health))
|
|
128
197
|
.fallback(any(dispatch_route))
|
|
@@ -130,7 +199,11 @@ async fn serve_on(listener: tokio::net::TcpListener, state: ServerState, version
|
|
|
130
199
|
.merge(version_router(version));
|
|
131
200
|
|
|
132
201
|
// The connect info is the TCP peer, which `rate_limit` needs to tell a visitor from a proxy.
|
|
133
|
-
axum::serve(
|
|
202
|
+
axum::serve(
|
|
203
|
+
listener,
|
|
204
|
+
app.into_make_service_with_connect_info::<SocketAddr>(),
|
|
205
|
+
)
|
|
206
|
+
.await?;
|
|
134
207
|
Ok(())
|
|
135
208
|
}
|
|
136
209
|
|
|
@@ -143,8 +216,14 @@ async fn warm_wasm(wasm_path: &Arc<PathBuf>) {
|
|
|
143
216
|
Ok(Ok(())) => {
|
|
144
217
|
phase.end();
|
|
145
218
|
}
|
|
146
|
-
Ok(Err(e)) => log::error(
|
|
147
|
-
|
|
219
|
+
Ok(Err(e)) => log::error(
|
|
220
|
+
"wasm_warm_failed",
|
|
221
|
+
serde_json::json!({ "error": format!("{e:#}") }),
|
|
222
|
+
),
|
|
223
|
+
Err(e) => log::error(
|
|
224
|
+
"wasm_warm_failed",
|
|
225
|
+
serde_json::json!({ "error": format!("{e}") }),
|
|
226
|
+
),
|
|
148
227
|
}
|
|
149
228
|
}
|
|
150
229
|
|
|
@@ -179,30 +258,55 @@ async fn dispatch_route(
|
|
|
179
258
|
response
|
|
180
259
|
}
|
|
181
260
|
|
|
182
|
-
async fn route_request(
|
|
261
|
+
async fn route_request(
|
|
262
|
+
state: ServerState,
|
|
263
|
+
peer: SocketAddr,
|
|
264
|
+
method: Method,
|
|
265
|
+
uri: Uri,
|
|
266
|
+
headers: HeaderMap,
|
|
267
|
+
body: Bytes,
|
|
268
|
+
) -> Response {
|
|
183
269
|
let envelope = match admit(&state.limits, peer, &method, &uri, &headers, &body) {
|
|
184
270
|
Ok(envelope) => envelope,
|
|
185
271
|
Err(refusal) => return *refusal,
|
|
186
272
|
};
|
|
187
273
|
|
|
188
|
-
match dispatch_body(envelope,
|
|
274
|
+
match dispatch_body(envelope, state.host()).await {
|
|
189
275
|
Ok(value) => value_to_response(value),
|
|
190
276
|
Err(e) => {
|
|
191
|
-
log::error(
|
|
192
|
-
|
|
277
|
+
log::error(
|
|
278
|
+
"dispatch_failed",
|
|
279
|
+
serde_json::json!({ "error": format!("{e}") }),
|
|
280
|
+
);
|
|
281
|
+
(
|
|
282
|
+
StatusCode::INTERNAL_SERVER_ERROR,
|
|
283
|
+
axum::Json(serde_json::json!({ "error": format!("{e}") })),
|
|
284
|
+
)
|
|
285
|
+
.into_response()
|
|
193
286
|
}
|
|
194
287
|
}
|
|
195
288
|
}
|
|
196
289
|
|
|
197
290
|
/// Turns a raw HTTP request into `dispatch_body`'s envelope, or into the response that ends
|
|
198
291
|
/// it first: a `400` for non-JSON, a `429` for a caller over a public route's rate limit.
|
|
199
|
-
fn admit(
|
|
292
|
+
fn admit(
|
|
293
|
+
limits: &RateLimits,
|
|
294
|
+
peer: SocketAddr,
|
|
295
|
+
method: &Method,
|
|
296
|
+
uri: &Uri,
|
|
297
|
+
headers: &HeaderMap,
|
|
298
|
+
body: &Bytes,
|
|
299
|
+
) -> Result<Value, Box<Response>> {
|
|
200
300
|
let parsed: Value = if body.is_empty() {
|
|
201
301
|
serde_json::json!({})
|
|
202
302
|
} else {
|
|
203
303
|
match serde_json::from_slice(body) {
|
|
204
304
|
Ok(value) => value,
|
|
205
|
-
Err(e) =>
|
|
305
|
+
Err(e) => {
|
|
306
|
+
return Err(Box::new(
|
|
307
|
+
(StatusCode::BAD_REQUEST, format!("invalid JSON body: {e}")).into_response(),
|
|
308
|
+
))
|
|
309
|
+
}
|
|
206
310
|
}
|
|
207
311
|
};
|
|
208
312
|
|
|
@@ -213,14 +317,22 @@ fn admit(limits: &RateLimits, peer: SocketAddr, method: &Method, uri: &Uri, head
|
|
|
213
317
|
// they're honored only from a peer on this host (the task's own sidecar); everyone
|
|
214
318
|
// else — the load balancer, i.e. the public internet — gets an ordinary request
|
|
215
319
|
// envelope for the path it actually hit.
|
|
216
|
-
|
|
320
|
+
// A proxy on this host (an AwsBox's Caddy, which a tunnel also goes through) makes every
|
|
321
|
+
// public request arrive from loopback too; the forwarding headers it always adds are what
|
|
322
|
+
// tell its traffic apart from a sidecar's.
|
|
323
|
+
let envelope = if trusts_internal_dispatch(peer)
|
|
324
|
+
&& !came_through_a_proxy(headers)
|
|
325
|
+
&& is_internal_dispatch_shape(&parsed)
|
|
326
|
+
{
|
|
217
327
|
parsed
|
|
218
328
|
} else {
|
|
219
329
|
synthesize_function_url_envelope(method, uri, headers, body)
|
|
220
330
|
};
|
|
221
331
|
|
|
222
332
|
if let Some((route_method, route_path)) = envelope_route(&envelope) {
|
|
223
|
-
if let Verdict::Limited { retry_after_secs } =
|
|
333
|
+
if let Verdict::Limited { retry_after_secs } =
|
|
334
|
+
limits.check(route_method, route_path, headers, Some(peer.ip()))
|
|
335
|
+
{
|
|
224
336
|
return Err(Box::new(rate_limit::too_many_requests(retry_after_secs)));
|
|
225
337
|
}
|
|
226
338
|
}
|
|
@@ -230,7 +342,11 @@ fn admit(limits: &RateLimits, peer: SocketAddr, method: &Method, uri: &Uri, head
|
|
|
230
342
|
/// The method and path a Function-URL-shaped envelope names, or `None` for
|
|
231
343
|
/// the verb and read shapes, which carry no route.
|
|
232
344
|
fn envelope_route(envelope: &Value) -> Option<(&str, &str)> {
|
|
233
|
-
let method = envelope
|
|
345
|
+
let method = envelope
|
|
346
|
+
.get("requestContext")?
|
|
347
|
+
.get("http")?
|
|
348
|
+
.get("method")?
|
|
349
|
+
.as_str()?;
|
|
234
350
|
let path = envelope.get("rawPath")?.as_str()?;
|
|
235
351
|
Some((method, path))
|
|
236
352
|
}
|
|
@@ -240,10 +356,29 @@ fn envelope_route(envelope: &Value) -> Option<(&str, &str)> {
|
|
|
240
356
|
fn trusts_internal_dispatch(peer: SocketAddr) -> bool {
|
|
241
357
|
match peer.ip() {
|
|
242
358
|
std::net::IpAddr::V4(addr) => addr.is_loopback(),
|
|
243
|
-
std::net::IpAddr::V6(addr) =>
|
|
359
|
+
std::net::IpAddr::V6(addr) => {
|
|
360
|
+
addr.is_loopback()
|
|
361
|
+
|| addr
|
|
362
|
+
.to_ipv4_mapped()
|
|
363
|
+
.is_some_and(|mapped| mapped.is_loopback())
|
|
364
|
+
}
|
|
244
365
|
}
|
|
245
366
|
}
|
|
246
367
|
|
|
368
|
+
/// Whether a proxy forwarded this request: Caddy's `reverse_proxy` always sets the
|
|
369
|
+
/// `X-Forwarded-*` headers, and a client cannot make it drop them.
|
|
370
|
+
fn came_through_a_proxy(headers: &HeaderMap) -> bool {
|
|
371
|
+
[
|
|
372
|
+
"x-forwarded-for",
|
|
373
|
+
"x-forwarded-host",
|
|
374
|
+
"x-forwarded-proto",
|
|
375
|
+
"forwarded",
|
|
376
|
+
"via",
|
|
377
|
+
]
|
|
378
|
+
.iter()
|
|
379
|
+
.any(|name| headers.contains_key(*name))
|
|
380
|
+
}
|
|
381
|
+
|
|
247
382
|
/// Whether `value` already matches one of `dispatch_body`'s recognized shapes; anything
|
|
248
383
|
/// else (including a bare `{}`) is real REST traffic needing `synthesize_function_url_envelope`.
|
|
249
384
|
fn is_internal_dispatch_shape(value: &Value) -> bool {
|
|
@@ -255,11 +390,19 @@ fn is_internal_dispatch_shape(value: &Value) -> bool {
|
|
|
255
390
|
|
|
256
391
|
/// Rebuilds the envelope shape a real Lambda Function URL invocation already produces
|
|
257
392
|
/// automatically, from the raw axum request parts.
|
|
258
|
-
fn synthesize_function_url_envelope(
|
|
393
|
+
fn synthesize_function_url_envelope(
|
|
394
|
+
method: &Method,
|
|
395
|
+
uri: &Uri,
|
|
396
|
+
headers: &HeaderMap,
|
|
397
|
+
body: &Bytes,
|
|
398
|
+
) -> Value {
|
|
259
399
|
let mut header_map = serde_json::Map::new();
|
|
260
|
-
for (name, value) in headers
|
|
400
|
+
for (name, value) in headers {
|
|
261
401
|
if let Ok(value) = value.to_str() {
|
|
262
|
-
header_map.insert(
|
|
402
|
+
header_map.insert(
|
|
403
|
+
name.as_str().to_ascii_lowercase(),
|
|
404
|
+
Value::String(value.to_string()),
|
|
405
|
+
);
|
|
263
406
|
}
|
|
264
407
|
}
|
|
265
408
|
|
|
@@ -276,17 +419,25 @@ fn synthesize_function_url_envelope(method: &Method, uri: &Uri, headers: &Header
|
|
|
276
419
|
/// Translates `dispatch_body`'s JSON result into a real HTTP response: `web::render`'s
|
|
277
420
|
/// Function-URL envelope when `statusCode` is present, else the raw outcome as `200` JSON.
|
|
278
421
|
fn value_to_response(value: Value) -> Response {
|
|
279
|
-
let Some(status_code) = value.get("statusCode").and_then(
|
|
422
|
+
let Some(status_code) = value.get("statusCode").and_then(serde_json::Value::as_u64) else {
|
|
280
423
|
return (StatusCode::OK, axum::Json(value)).into_response();
|
|
281
424
|
};
|
|
282
425
|
// `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)
|
|
426
|
+
let status = u16::try_from(status_code)
|
|
427
|
+
.ok()
|
|
428
|
+
.and_then(|code| StatusCode::from_u16(code).ok())
|
|
429
|
+
.unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
|
|
284
430
|
|
|
285
431
|
let mut headers = HeaderMap::new();
|
|
286
432
|
if let Some(header_obj) = value.get("headers").and_then(|h| h.as_object()) {
|
|
287
433
|
for (name, header_value) in header_obj {
|
|
288
|
-
let Some(header_value) = header_value.as_str() else {
|
|
289
|
-
|
|
434
|
+
let Some(header_value) = header_value.as_str() else {
|
|
435
|
+
continue;
|
|
436
|
+
};
|
|
437
|
+
if let (Ok(name), Ok(header_value)) = (
|
|
438
|
+
HeaderName::from_bytes(name.as_bytes()),
|
|
439
|
+
HeaderValue::from_str(header_value),
|
|
440
|
+
) {
|
|
290
441
|
headers.insert(name, header_value);
|
|
291
442
|
}
|
|
292
443
|
}
|
|
@@ -304,7 +455,11 @@ fn value_to_response(value: Value) -> Response {
|
|
|
304
455
|
}
|
|
305
456
|
|
|
306
457
|
let raw_body = value.get("body").and_then(|v| v.as_str()).unwrap_or("");
|
|
307
|
-
let body_bytes = if value
|
|
458
|
+
let body_bytes = if value
|
|
459
|
+
.get("isBase64Encoded")
|
|
460
|
+
.and_then(serde_json::Value::as_bool)
|
|
461
|
+
== Some(true)
|
|
462
|
+
{
|
|
308
463
|
web::base64_decode(raw_body)
|
|
309
464
|
} else {
|
|
310
465
|
raw_body.as_bytes().to_vec()
|
|
@@ -319,29 +474,39 @@ mod tests {
|
|
|
319
474
|
|
|
320
475
|
#[test]
|
|
321
476
|
fn a_function_url_shaped_body_is_recognized_as_internal_dispatch() {
|
|
322
|
-
assert!(is_internal_dispatch_shape(
|
|
477
|
+
assert!(is_internal_dispatch_shape(
|
|
478
|
+
&serde_json::json!({"requestContext": {"http": {"method": "GET"}}})
|
|
479
|
+
));
|
|
323
480
|
}
|
|
324
481
|
|
|
325
482
|
#[test]
|
|
326
483
|
fn a_bare_read_body_is_recognized_as_internal_dispatch() {
|
|
327
|
-
assert!(is_internal_dispatch_shape(
|
|
484
|
+
assert!(is_internal_dispatch_shape(
|
|
485
|
+
&serde_json::json!({"read": true})
|
|
486
|
+
));
|
|
328
487
|
}
|
|
329
488
|
|
|
330
489
|
#[test]
|
|
331
490
|
fn a_declared_query_body_is_recognized_as_internal_dispatch() {
|
|
332
491
|
// `dispatch_body` answers `{"query": ...}`, and the generated editor's list and picture
|
|
333
492
|
// pickers ask it; without this a loopback caller was sent to the login page.
|
|
334
|
-
assert!(is_internal_dispatch_shape(
|
|
493
|
+
assert!(is_internal_dispatch_shape(
|
|
494
|
+
&serde_json::json!({"query": "Pictures::MediaItem.Pictures", "args": {}})
|
|
495
|
+
));
|
|
335
496
|
}
|
|
336
497
|
|
|
337
498
|
#[test]
|
|
338
499
|
fn a_verb_command_body_is_recognized_as_internal_dispatch() {
|
|
339
|
-
assert!(is_internal_dispatch_shape(
|
|
500
|
+
assert!(is_internal_dispatch_shape(
|
|
501
|
+
&serde_json::json!({"verb": "Register", "args": {}})
|
|
502
|
+
));
|
|
340
503
|
}
|
|
341
504
|
|
|
342
505
|
#[test]
|
|
343
506
|
fn a_plain_rest_payload_is_not_internal_dispatch() {
|
|
344
|
-
assert!(!is_internal_dispatch_shape(
|
|
507
|
+
assert!(!is_internal_dispatch_shape(
|
|
508
|
+
&serde_json::json!({"email": "a@example.com"})
|
|
509
|
+
));
|
|
345
510
|
}
|
|
346
511
|
|
|
347
512
|
#[test]
|
|
@@ -355,21 +520,43 @@ mod tests {
|
|
|
355
520
|
|
|
356
521
|
#[test]
|
|
357
522
|
fn only_a_peer_on_this_host_is_trusted_with_the_internal_protocol() {
|
|
358
|
-
for trusted in [
|
|
359
|
-
|
|
523
|
+
for trusted in [
|
|
524
|
+
"127.0.0.1:5000",
|
|
525
|
+
"127.4.5.6:5000",
|
|
526
|
+
"[::1]:5000",
|
|
527
|
+
"[::ffff:127.0.0.1]:5000",
|
|
528
|
+
] {
|
|
529
|
+
assert!(
|
|
530
|
+
trusts_internal_dispatch(peer(trusted)),
|
|
531
|
+
"{trusted} is this host"
|
|
532
|
+
);
|
|
360
533
|
}
|
|
361
|
-
for outside in [
|
|
362
|
-
|
|
534
|
+
for outside in [
|
|
535
|
+
"10.0.0.5:5000",
|
|
536
|
+
"172.31.4.9:5000",
|
|
537
|
+
"203.0.113.7:5000",
|
|
538
|
+
"[2001:db8::1]:5000",
|
|
539
|
+
"[::ffff:10.0.0.5]:5000",
|
|
540
|
+
] {
|
|
541
|
+
assert!(
|
|
542
|
+
!trusts_internal_dispatch(peer(outside)),
|
|
543
|
+
"{outside} is not this host"
|
|
544
|
+
);
|
|
363
545
|
}
|
|
364
546
|
}
|
|
365
547
|
|
|
366
548
|
fn admitted(from: &str, path: &str, body: &str) -> Value {
|
|
367
549
|
let uri: Uri = path.parse().unwrap();
|
|
368
550
|
let body = Bytes::from(body.to_string());
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
551
|
+
admit(
|
|
552
|
+
&limits_from(&[]),
|
|
553
|
+
peer(from),
|
|
554
|
+
&Method::POST,
|
|
555
|
+
&uri,
|
|
556
|
+
&HeaderMap::new(),
|
|
557
|
+
&body,
|
|
558
|
+
)
|
|
559
|
+
.unwrap_or_else(|_| panic!("{from} POST {path} was refused before dispatch"))
|
|
373
560
|
}
|
|
374
561
|
|
|
375
562
|
// The gap this closes: a public POST to any path the load balancer forwards carried
|
|
@@ -377,11 +564,17 @@ mod tests {
|
|
|
377
564
|
// caller's own word for its role and no session check.
|
|
378
565
|
#[test]
|
|
379
566
|
fn a_verb_or_read_body_from_outside_this_host_is_an_ordinary_request_not_a_command() {
|
|
380
|
-
for body in [
|
|
567
|
+
for body in [
|
|
568
|
+
r#"{"verb":"Approve","role":"admin"}"#,
|
|
569
|
+
r#"{"read":true}"#,
|
|
570
|
+
r#"{"query":"Pictures::MediaItem.Pictures","args":{}}"#,
|
|
571
|
+
] {
|
|
381
572
|
let envelope = admitted("10.0.0.5:5000", "/webhooks/x", body);
|
|
382
573
|
|
|
383
574
|
assert!(
|
|
384
|
-
envelope.get("verb").is_none()
|
|
575
|
+
envelope.get("verb").is_none()
|
|
576
|
+
&& envelope.get("read").is_none()
|
|
577
|
+
&& envelope.get("query").is_none(),
|
|
385
578
|
"{body} must not reach dispatch as a command"
|
|
386
579
|
);
|
|
387
580
|
assert_eq!(envelope["requestContext"]["http"]["method"], "POST");
|
|
@@ -397,18 +590,58 @@ mod tests {
|
|
|
397
590
|
|
|
398
591
|
assert_eq!(envelope["rawPath"], "/webhooks/x");
|
|
399
592
|
assert_eq!(envelope["requestContext"]["http"]["method"], "POST");
|
|
400
|
-
assert!(
|
|
593
|
+
assert!(
|
|
594
|
+
envelope["headers"].get("cookie").is_none(),
|
|
595
|
+
"headers come from the real request, not the body"
|
|
596
|
+
);
|
|
401
597
|
}
|
|
402
598
|
|
|
403
599
|
#[test]
|
|
404
600
|
fn a_verb_or_read_body_from_this_host_still_reaches_dispatch_unchanged() {
|
|
405
|
-
for body in [
|
|
601
|
+
for body in [
|
|
602
|
+
r#"{"verb":"Register","to":"r1","with":{}}"#,
|
|
603
|
+
r#"{"read":true}"#,
|
|
604
|
+
r#"{"query":"Banking::Customer.Suspended","args":{}}"#,
|
|
605
|
+
] {
|
|
406
606
|
let envelope = admitted("127.0.0.1:5000", "/anything", body);
|
|
407
607
|
|
|
408
608
|
assert_eq!(envelope, serde_json::from_str::<Value>(body).unwrap());
|
|
409
609
|
}
|
|
410
610
|
}
|
|
411
611
|
|
|
612
|
+
// The gap this closes: on an AwsBox, Caddy proxies the public internet to 127.0.0.1, so
|
|
613
|
+
// a public `{"read": true}` or `{"verb": ..., "role": "Admin"}` arrived from loopback and
|
|
614
|
+
// reached the kernel as the internal protocol.
|
|
615
|
+
#[test]
|
|
616
|
+
fn a_verb_or_read_body_a_proxy_on_this_host_forwarded_is_an_ordinary_request() {
|
|
617
|
+
for header in [
|
|
618
|
+
"x-forwarded-for",
|
|
619
|
+
"x-forwarded-host",
|
|
620
|
+
"x-forwarded-proto",
|
|
621
|
+
"forwarded",
|
|
622
|
+
"via",
|
|
623
|
+
] {
|
|
624
|
+
let mut headers = HeaderMap::new();
|
|
625
|
+
headers.insert(header, "203.0.113.7".parse().unwrap());
|
|
626
|
+
let body = Bytes::from(r#"{"verb":"Approve","role":"Admin"}"#);
|
|
627
|
+
let envelope = admit(
|
|
628
|
+
&limits_from(&[]),
|
|
629
|
+
peer("127.0.0.1:5000"),
|
|
630
|
+
&Method::POST,
|
|
631
|
+
&"/anything".parse().unwrap(),
|
|
632
|
+
&headers,
|
|
633
|
+
&body,
|
|
634
|
+
)
|
|
635
|
+
.unwrap_or_else(|_| panic!("a request carrying {header} was refused"));
|
|
636
|
+
|
|
637
|
+
assert!(
|
|
638
|
+
envelope.get("verb").is_none(),
|
|
639
|
+
"a body forwarded with {header} must not reach dispatch as a command"
|
|
640
|
+
);
|
|
641
|
+
assert_eq!(envelope["rawPath"], "/anything");
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
412
645
|
// Pins a real REST client's plain POST, with no Function-URL wrapping: it must
|
|
413
646
|
// synthesize the same shape `web::render`/`checkout_route` read, not error on a
|
|
414
647
|
// missing "verb".
|
|
@@ -417,8 +650,14 @@ mod tests {
|
|
|
417
650
|
let method = Method::POST;
|
|
418
651
|
let uri: Uri = "/registrations?utm_source=test".parse().unwrap();
|
|
419
652
|
let mut headers = HeaderMap::new();
|
|
420
|
-
headers.insert(
|
|
421
|
-
|
|
653
|
+
headers.insert(
|
|
654
|
+
HeaderName::from_static("stripe-signature"),
|
|
655
|
+
HeaderValue::from_static("t=1,v1=abc"),
|
|
656
|
+
);
|
|
657
|
+
headers.insert(
|
|
658
|
+
HeaderName::from_static("content-type"),
|
|
659
|
+
HeaderValue::from_static("application/json"),
|
|
660
|
+
);
|
|
422
661
|
let body = Bytes::from_static(br#"{"event_id":"ci-verification-test"}"#);
|
|
423
662
|
|
|
424
663
|
let envelope = synthesize_function_url_envelope(&method, &uri, &headers, &body);
|
|
@@ -430,7 +669,10 @@ mod tests {
|
|
|
430
669
|
assert_eq!(envelope["headers"]["content-type"], "application/json");
|
|
431
670
|
assert_eq!(envelope["body"], r#"{"event_id":"ci-verification-test"}"#);
|
|
432
671
|
assert_eq!(envelope["isBase64Encoded"], false);
|
|
433
|
-
assert!(
|
|
672
|
+
assert!(
|
|
673
|
+
is_internal_dispatch_shape(&envelope),
|
|
674
|
+
"the synthesized envelope must itself be recognized on a second pass"
|
|
675
|
+
);
|
|
434
676
|
}
|
|
435
677
|
|
|
436
678
|
#[test]
|
|
@@ -453,7 +695,9 @@ mod tests {
|
|
|
453
695
|
// Serves `version_router` on an ephemeral port, the way the host serves
|
|
454
696
|
// it, so a request travels the real HTTP stack.
|
|
455
697
|
async fn serve_version(body: Value) -> std::net::SocketAddr {
|
|
456
|
-
let listener = tokio::net::TcpListener::bind(("127.0.0.1", 0))
|
|
698
|
+
let listener = tokio::net::TcpListener::bind(("127.0.0.1", 0))
|
|
699
|
+
.await
|
|
700
|
+
.unwrap();
|
|
457
701
|
let addr = listener.local_addr().unwrap();
|
|
458
702
|
tokio::spawn(async move { axum::serve(listener, version_router(body)).await });
|
|
459
703
|
addr
|
|
@@ -462,31 +706,56 @@ mod tests {
|
|
|
462
706
|
#[tokio::test]
|
|
463
707
|
async fn version_answers_200_json_with_no_session() {
|
|
464
708
|
let addr = serve_version(sample_version()).await;
|
|
465
|
-
let response = reqwest::get(format!("http://{addr}/version"))
|
|
709
|
+
let response = reqwest::get(format!("http://{addr}/version"))
|
|
710
|
+
.await
|
|
711
|
+
.unwrap();
|
|
466
712
|
assert_eq!(response.status(), 200);
|
|
467
|
-
let content_type = response.headers()["content-type"]
|
|
468
|
-
|
|
713
|
+
let content_type = response.headers()["content-type"]
|
|
714
|
+
.to_str()
|
|
715
|
+
.unwrap()
|
|
716
|
+
.to_string();
|
|
717
|
+
assert!(
|
|
718
|
+
content_type.starts_with("application/json"),
|
|
719
|
+
"got {content_type}"
|
|
720
|
+
);
|
|
469
721
|
let body: Value = response.json().await.unwrap();
|
|
470
|
-
assert_eq!(
|
|
722
|
+
assert_eq!(
|
|
723
|
+
body,
|
|
724
|
+
serde_json::json!({ "era": "199b08", "ir_hash": "a".repeat(64), "build": "build-42" })
|
|
725
|
+
);
|
|
471
726
|
}
|
|
472
727
|
|
|
473
728
|
#[tokio::test]
|
|
474
729
|
async fn version_era_is_a_non_empty_string() {
|
|
475
730
|
let addr = serve_version(sample_version()).await;
|
|
476
|
-
let body: Value = reqwest::get(format!("http://{addr}/version"))
|
|
731
|
+
let body: Value = reqwest::get(format!("http://{addr}/version"))
|
|
732
|
+
.await
|
|
733
|
+
.unwrap()
|
|
734
|
+
.json()
|
|
735
|
+
.await
|
|
736
|
+
.unwrap();
|
|
477
737
|
assert!(body["era"].as_str().is_some_and(|era| !era.is_empty()));
|
|
478
738
|
}
|
|
479
739
|
|
|
480
740
|
#[tokio::test]
|
|
481
741
|
async fn version_is_get_only() {
|
|
482
742
|
let addr = serve_version(sample_version()).await;
|
|
483
|
-
let response = reqwest::Client::new()
|
|
743
|
+
let response = reqwest::Client::new()
|
|
744
|
+
.post(format!("http://{addr}/version"))
|
|
745
|
+
.send()
|
|
746
|
+
.await
|
|
747
|
+
.unwrap();
|
|
484
748
|
assert_eq!(response.status(), 405);
|
|
485
749
|
}
|
|
486
750
|
|
|
487
751
|
fn limits_from(pairs: &[(&str, &str)]) -> Arc<RateLimits> {
|
|
488
|
-
let map: std::collections::HashMap<String, String> = pairs
|
|
489
|
-
|
|
752
|
+
let map: std::collections::HashMap<String, String> = pairs
|
|
753
|
+
.iter()
|
|
754
|
+
.map(|(k, v)| (k.to_string(), v.to_string()))
|
|
755
|
+
.collect();
|
|
756
|
+
Arc::new(RateLimits::new(
|
|
757
|
+
crate::rate_limit::Config::from_lookup(|name| map.get(name).cloned()).0,
|
|
758
|
+
))
|
|
490
759
|
}
|
|
491
760
|
|
|
492
761
|
async fn gate_only(
|
|
@@ -506,15 +775,30 @@ mod tests {
|
|
|
506
775
|
// A disposable server on an ephemeral port: the real `admit` gate reading the real connection
|
|
507
776
|
// peer, with an acknowledgement standing in for dispatch, which needs Postgres and wasm.
|
|
508
777
|
async fn gated_server(limits: Arc<RateLimits>) -> SocketAddr {
|
|
509
|
-
let listener = tokio::net::TcpListener::bind(("127.0.0.1", 0))
|
|
778
|
+
let listener = tokio::net::TcpListener::bind(("127.0.0.1", 0))
|
|
779
|
+
.await
|
|
780
|
+
.unwrap();
|
|
510
781
|
let addr = listener.local_addr().unwrap();
|
|
511
782
|
let app = Router::new().fallback(any(gate_only)).with_state(limits);
|
|
512
|
-
tokio::spawn(async move {
|
|
783
|
+
tokio::spawn(async move {
|
|
784
|
+
axum::serve(
|
|
785
|
+
listener,
|
|
786
|
+
app.into_make_service_with_connect_info::<SocketAddr>(),
|
|
787
|
+
)
|
|
788
|
+
.await
|
|
789
|
+
});
|
|
513
790
|
addr
|
|
514
791
|
}
|
|
515
792
|
|
|
516
|
-
async fn post(
|
|
517
|
-
|
|
793
|
+
async fn post(
|
|
794
|
+
addr: SocketAddr,
|
|
795
|
+
path: &str,
|
|
796
|
+
forwarded_for: Option<&str>,
|
|
797
|
+
body: &str,
|
|
798
|
+
) -> reqwest::Response {
|
|
799
|
+
let mut request = reqwest::Client::new()
|
|
800
|
+
.post(format!("http://{addr}{path}"))
|
|
801
|
+
.body(body.to_string());
|
|
518
802
|
if let Some(forwarded_for) = forwarded_for {
|
|
519
803
|
request = request.header("x-forwarded-for", forwarded_for);
|
|
520
804
|
}
|
|
@@ -527,37 +811,84 @@ mod tests {
|
|
|
527
811
|
async fn a_burst_on_a_public_write_route_gets_429_with_retry_after_and_a_json_error() {
|
|
528
812
|
let addr = gated_server(limits_from(&[("HECKS_RATE_LIMIT_SUBSCRIBE", "3")])).await;
|
|
529
813
|
for attempt in 1..=3 {
|
|
530
|
-
assert_eq!(
|
|
814
|
+
assert_eq!(
|
|
815
|
+
post(addr, SUBSCRIBE, None, r#"{"email":"a@example.com"}"#)
|
|
816
|
+
.await
|
|
817
|
+
.status(),
|
|
818
|
+
200,
|
|
819
|
+
"request {attempt}"
|
|
820
|
+
);
|
|
531
821
|
}
|
|
532
822
|
|
|
533
823
|
let refused = post(addr, SUBSCRIBE, None, r#"{"email":"a@example.com"}"#).await;
|
|
534
824
|
assert_eq!(refused.status(), 429);
|
|
535
|
-
let retry_after: u64 = refused.headers()["retry-after"]
|
|
825
|
+
let retry_after: u64 = refused.headers()["retry-after"]
|
|
826
|
+
.to_str()
|
|
827
|
+
.unwrap()
|
|
828
|
+
.parse()
|
|
829
|
+
.unwrap();
|
|
536
830
|
assert!((1..=3600).contains(&retry_after), "got {retry_after}");
|
|
537
|
-
assert!(refused.headers()["content-type"]
|
|
831
|
+
assert!(refused.headers()["content-type"]
|
|
832
|
+
.to_str()
|
|
833
|
+
.unwrap()
|
|
834
|
+
.starts_with("application/json"));
|
|
538
835
|
let body: Value = refused.json().await.unwrap();
|
|
539
|
-
assert_eq!(
|
|
836
|
+
assert_eq!(
|
|
837
|
+
body,
|
|
838
|
+
serde_json::json!({ "error": "too many requests, please try again later" })
|
|
839
|
+
);
|
|
540
840
|
}
|
|
541
841
|
|
|
542
842
|
#[tokio::test]
|
|
543
843
|
async fn registrations_are_limited_apart_from_subscribes() {
|
|
544
|
-
let addr = gated_server(limits_from(&[
|
|
844
|
+
let addr = gated_server(limits_from(&[
|
|
845
|
+
("HECKS_RATE_LIMIT_SUBSCRIBE", "1"),
|
|
846
|
+
("HECKS_RATE_LIMIT_REGISTER", "2"),
|
|
847
|
+
]))
|
|
848
|
+
.await;
|
|
545
849
|
assert_eq!(post(addr, SUBSCRIBE, None, "{}").await.status(), 200);
|
|
546
850
|
assert_eq!(post(addr, SUBSCRIBE, None, "{}").await.status(), 429);
|
|
547
851
|
|
|
548
|
-
assert_eq!(
|
|
852
|
+
assert_eq!(
|
|
853
|
+
post(addr, "/registrations", None, "{}").await.status(),
|
|
854
|
+
200,
|
|
855
|
+
"subscribe being spent leaves register alone"
|
|
856
|
+
);
|
|
549
857
|
assert_eq!(post(addr, "/registrations", None, "{}").await.status(), 200);
|
|
550
858
|
assert_eq!(post(addr, "/registrations", None, "{}").await.status(), 429);
|
|
551
859
|
}
|
|
552
860
|
|
|
553
861
|
#[tokio::test]
|
|
554
862
|
async fn reads_and_other_routes_are_never_limited() {
|
|
555
|
-
let addr = gated_server(limits_from(&[
|
|
863
|
+
let addr = gated_server(limits_from(&[
|
|
864
|
+
("HECKS_RATE_LIMIT_SUBSCRIBE", "1"),
|
|
865
|
+
("HECKS_RATE_LIMIT_REGISTER", "1"),
|
|
866
|
+
]))
|
|
867
|
+
.await;
|
|
556
868
|
let client = reqwest::Client::new();
|
|
557
869
|
for _ in 0..5 {
|
|
558
|
-
assert_eq!(
|
|
559
|
-
|
|
560
|
-
|
|
870
|
+
assert_eq!(
|
|
871
|
+
client
|
|
872
|
+
.get(format!("http://{addr}{SUBSCRIBE}"))
|
|
873
|
+
.send()
|
|
874
|
+
.await
|
|
875
|
+
.unwrap()
|
|
876
|
+
.status(),
|
|
877
|
+
200
|
|
878
|
+
);
|
|
879
|
+
assert_eq!(
|
|
880
|
+
client
|
|
881
|
+
.get(format!("http://{addr}/registrations/REG-1"))
|
|
882
|
+
.send()
|
|
883
|
+
.await
|
|
884
|
+
.unwrap()
|
|
885
|
+
.status(),
|
|
886
|
+
200
|
|
887
|
+
);
|
|
888
|
+
assert_eq!(
|
|
889
|
+
post(addr, "/webhooks/stripe", None, "{}").await.status(),
|
|
890
|
+
200
|
|
891
|
+
);
|
|
561
892
|
assert_eq!(post(addr, "/members", None, "{}").await.status(), 200);
|
|
562
893
|
}
|
|
563
894
|
}
|
|
@@ -567,18 +898,46 @@ mod tests {
|
|
|
567
898
|
// The test client is the peer at 127.0.0.1 and nothing trusts it, so every header is
|
|
568
899
|
// typed by the caller and the count stays with the connection.
|
|
569
900
|
let addr = gated_server(limits_from(&[("HECKS_RATE_LIMIT_SUBSCRIBE", "1")])).await;
|
|
570
|
-
assert_eq!(
|
|
901
|
+
assert_eq!(
|
|
902
|
+
post(addr, SUBSCRIBE, Some("1.1.1.1"), "{}").await.status(),
|
|
903
|
+
200
|
|
904
|
+
);
|
|
571
905
|
for spoof in ["2.2.2.2", "3.3.3.3", "4.4.4.4, 5.5.5.5"] {
|
|
572
|
-
assert_eq!(
|
|
906
|
+
assert_eq!(
|
|
907
|
+
post(addr, SUBSCRIBE, Some(spoof), "{}").await.status(),
|
|
908
|
+
429,
|
|
909
|
+
"{spoof}"
|
|
910
|
+
);
|
|
573
911
|
}
|
|
574
912
|
}
|
|
575
913
|
|
|
576
914
|
#[tokio::test]
|
|
577
915
|
async fn behind_a_trusted_proxy_each_visitor_has_their_own_allowance() {
|
|
578
|
-
let addr = gated_server(limits_from(&[
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
916
|
+
let addr = gated_server(limits_from(&[
|
|
917
|
+
("HECKS_RATE_LIMIT_SUBSCRIBE", "1"),
|
|
918
|
+
("HECKS_TRUSTED_PROXIES", "127.0.0.1"),
|
|
919
|
+
]))
|
|
920
|
+
.await;
|
|
921
|
+
assert_eq!(
|
|
922
|
+
post(addr, SUBSCRIBE, Some("1.1.1.1, 203.0.113.7"), "{}")
|
|
923
|
+
.await
|
|
924
|
+
.status(),
|
|
925
|
+
200
|
|
926
|
+
);
|
|
927
|
+
assert_eq!(
|
|
928
|
+
post(addr, SUBSCRIBE, Some("2.2.2.2, 203.0.113.7"), "{}")
|
|
929
|
+
.await
|
|
930
|
+
.status(),
|
|
931
|
+
429,
|
|
932
|
+
"same visitor, new typed entry"
|
|
933
|
+
);
|
|
934
|
+
assert_eq!(
|
|
935
|
+
post(addr, SUBSCRIBE, Some("2.2.2.2, 198.51.100.4"), "{}")
|
|
936
|
+
.await
|
|
937
|
+
.status(),
|
|
938
|
+
200,
|
|
939
|
+
"a different visitor"
|
|
940
|
+
);
|
|
582
941
|
}
|
|
583
942
|
|
|
584
943
|
#[tokio::test]
|
|
@@ -587,7 +946,11 @@ mod tests {
|
|
|
587
946
|
let envelope = r#"{"requestContext":{"http":{"method":"POST"}},"rawPath":"/registrations","body":"{}"}"#;
|
|
588
947
|
assert_eq!(post(addr, "/anything", None, envelope).await.status(), 200);
|
|
589
948
|
assert_eq!(post(addr, "/anything", None, envelope).await.status(), 429);
|
|
590
|
-
assert_eq!(
|
|
949
|
+
assert_eq!(
|
|
950
|
+
post(addr, "/registrations", None, "{}").await.status(),
|
|
951
|
+
429,
|
|
952
|
+
"the same budget as the real path"
|
|
953
|
+
);
|
|
591
954
|
}
|
|
592
955
|
|
|
593
956
|
#[tokio::test]
|
|
@@ -601,7 +964,11 @@ mod tests {
|
|
|
601
964
|
|
|
602
965
|
#[tokio::test]
|
|
603
966
|
async fn turning_the_limits_off_lets_every_request_through() {
|
|
604
|
-
let addr = gated_server(limits_from(&[
|
|
967
|
+
let addr = gated_server(limits_from(&[
|
|
968
|
+
("HECKS_RATE_LIMIT", "off"),
|
|
969
|
+
("HECKS_RATE_LIMIT_SUBSCRIBE", "1"),
|
|
970
|
+
]))
|
|
971
|
+
.await;
|
|
605
972
|
for _ in 0..10 {
|
|
606
973
|
assert_eq!(post(addr, SUBSCRIBE, None, "{}").await.status(), 200);
|
|
607
974
|
}
|
|
@@ -611,16 +978,28 @@ mod tests {
|
|
|
611
978
|
// checkout fixture's wasm, so the first request is a real compile.
|
|
612
979
|
async fn fixture_host(database: &str, copy_name: &str) -> (SocketAddr, Arc<PathBuf>) {
|
|
613
980
|
let client = crate::dispatch::tests::scratch_db(database).await;
|
|
614
|
-
crate::dispatch::tests::provision_lineage(
|
|
981
|
+
crate::dispatch::tests::provision_lineage(
|
|
982
|
+
&*client.lock().await,
|
|
983
|
+
"CheckoutFixture",
|
|
984
|
+
1,
|
|
985
|
+
&["Event"],
|
|
986
|
+
)
|
|
987
|
+
.await;
|
|
615
988
|
let wasm_path = Arc::new(crate::wasm_runner::tests::cold_copy_of_fixture(copy_name));
|
|
616
989
|
let state = ServerState {
|
|
617
990
|
client: Arc::new(client),
|
|
618
991
|
wasm_path: Arc::clone(&wasm_path),
|
|
619
|
-
lineage_config: Arc::new(LineageConfig {
|
|
992
|
+
lineage_config: Arc::new(LineageConfig {
|
|
993
|
+
domain: "CheckoutFixture".to_string(),
|
|
994
|
+
era: Some(1),
|
|
995
|
+
mirrored: None,
|
|
996
|
+
}),
|
|
620
997
|
invoker: Arc::new(crate::lambda_client::NeverInvoker),
|
|
621
998
|
limits: limits_from(&[]),
|
|
622
999
|
};
|
|
623
|
-
let listener = tokio::net::TcpListener::bind(("127.0.0.1", 0))
|
|
1000
|
+
let listener = tokio::net::TcpListener::bind(("127.0.0.1", 0))
|
|
1001
|
+
.await
|
|
1002
|
+
.unwrap();
|
|
624
1003
|
let addr = listener.local_addr().unwrap();
|
|
625
1004
|
tokio::spawn(async move { serve_on(listener, state, sample_version()).await });
|
|
626
1005
|
(addr, wasm_path)
|
|
@@ -631,7 +1010,12 @@ mod tests {
|
|
|
631
1010
|
let requests: Vec<_> = (0..burst)
|
|
632
1011
|
.map(|_| {
|
|
633
1012
|
tokio::spawn(async move {
|
|
634
|
-
let response = reqwest::Client::new()
|
|
1013
|
+
let response = reqwest::Client::new()
|
|
1014
|
+
.post(format!("http://{addr}/dispatch"))
|
|
1015
|
+
.body(r#"{"read": true}"#)
|
|
1016
|
+
.send()
|
|
1017
|
+
.await
|
|
1018
|
+
.unwrap();
|
|
635
1019
|
let status = StatusCode::from_u16(response.status().as_u16()).unwrap();
|
|
636
1020
|
(status, response.json::<Value>().await.unwrap())
|
|
637
1021
|
})
|
|
@@ -639,7 +1023,12 @@ mod tests {
|
|
|
639
1023
|
.collect();
|
|
640
1024
|
let mut answers = Vec::new();
|
|
641
1025
|
for request in requests {
|
|
642
|
-
answers.push(
|
|
1026
|
+
answers.push(
|
|
1027
|
+
tokio::time::timeout(std::time::Duration::from_secs(120), request)
|
|
1028
|
+
.await
|
|
1029
|
+
.expect("a read was never answered")
|
|
1030
|
+
.unwrap(),
|
|
1031
|
+
);
|
|
643
1032
|
}
|
|
644
1033
|
answers
|
|
645
1034
|
}
|
|
@@ -665,7 +1054,63 @@ mod tests {
|
|
|
665
1054
|
let answers = read_burst(addr, 1).await;
|
|
666
1055
|
assert_eq!(answers[0].0, StatusCode::OK, "{}", answers[0].1);
|
|
667
1056
|
assert_eq!(crate::wasm_runner::compile_count(&wasm_path), 1);
|
|
668
|
-
assert!(
|
|
1057
|
+
assert!(
|
|
1058
|
+
started.elapsed() < std::time::Duration::from_secs(1),
|
|
1059
|
+
"a warmed read took {:?}",
|
|
1060
|
+
started.elapsed()
|
|
1061
|
+
);
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
// The checkout fixture's `Event`: `slug`, `name` and `capacity` hold one `value` field and
|
|
1065
|
+
// `price` one `cents` field, so each answers as a bare scalar when asked to.
|
|
1066
|
+
#[tokio::test]
|
|
1067
|
+
async fn a_bare_body_answers_single_field_value_objects_bare_and_the_default_stays_wrapped() {
|
|
1068
|
+
let client = crate::dispatch::tests::scratch_db("rust_host_serve_bare").await;
|
|
1069
|
+
crate::dispatch::tests::provision_lineage(
|
|
1070
|
+
&*client.lock().await,
|
|
1071
|
+
"CheckoutFixture",
|
|
1072
|
+
1,
|
|
1073
|
+
&["Event"],
|
|
1074
|
+
)
|
|
1075
|
+
.await;
|
|
1076
|
+
let config = LineageConfig {
|
|
1077
|
+
domain: "CheckoutFixture".to_string(),
|
|
1078
|
+
era: Some(1),
|
|
1079
|
+
mirrored: None,
|
|
1080
|
+
};
|
|
1081
|
+
let wasm = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../dist/checkout_fixture.wasm");
|
|
1082
|
+
let host = Host::new(&client, &wasm, &config, &crate::lambda_client::NeverInvoker);
|
|
1083
|
+
let schedule = serde_json::json!({
|
|
1084
|
+
"verb": "CheckoutFixture::Event.Schedule",
|
|
1085
|
+
"role": "Organizer",
|
|
1086
|
+
"with": {"slug": "intro", "name": "Intro", "price": 1500, "capacity": 10},
|
|
1087
|
+
"bare": true,
|
|
1088
|
+
});
|
|
1089
|
+
|
|
1090
|
+
let scheduled = dispatch_body(schedule, host).await.unwrap();
|
|
1091
|
+
let wrapped = dispatch_body(serde_json::json!({"read": true}), host)
|
|
1092
|
+
.await
|
|
1093
|
+
.unwrap();
|
|
1094
|
+
let bare = dispatch_body(serde_json::json!({"read": true, "bare": true}), host)
|
|
1095
|
+
.await
|
|
1096
|
+
.unwrap();
|
|
1097
|
+
|
|
1098
|
+
let event = "CheckoutFixture::Event#intro";
|
|
1099
|
+
assert_eq!(
|
|
1100
|
+
scheduled["instances"][event]["name"], "Intro",
|
|
1101
|
+
"{scheduled}"
|
|
1102
|
+
);
|
|
1103
|
+
assert_eq!(
|
|
1104
|
+
scheduled["events"][0]["payload"]["price"], 1500,
|
|
1105
|
+
"{scheduled}"
|
|
1106
|
+
);
|
|
1107
|
+
assert_eq!(
|
|
1108
|
+
wrapped["instances"][event]["name"],
|
|
1109
|
+
serde_json::json!({"value": "Intro"}),
|
|
1110
|
+
"{wrapped}"
|
|
1111
|
+
);
|
|
1112
|
+
assert_eq!(bare["instances"][event]["price"], 1500, "{bare}");
|
|
1113
|
+
assert_eq!(bare["instances"][event]["capacity"], 10, "{bare}");
|
|
669
1114
|
}
|
|
670
1115
|
|
|
671
1116
|
#[test]
|
|
@@ -675,8 +1120,14 @@ mod tests {
|
|
|
675
1120
|
|
|
676
1121
|
#[test]
|
|
677
1122
|
fn build_falls_back_to_the_crate_version_when_unset_or_empty() {
|
|
678
|
-
assert_eq!(
|
|
679
|
-
|
|
1123
|
+
assert_eq!(
|
|
1124
|
+
version_body("e", "h", None)["build"],
|
|
1125
|
+
env!("CARGO_PKG_VERSION")
|
|
1126
|
+
);
|
|
1127
|
+
assert_eq!(
|
|
1128
|
+
version_body("e", "h", Some(""))["build"],
|
|
1129
|
+
env!("CARGO_PKG_VERSION")
|
|
1130
|
+
);
|
|
680
1131
|
}
|
|
681
1132
|
}
|
|
682
1133
|
|