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
|
@@ -21,8 +21,20 @@ pub struct StoredKeys {
|
|
|
21
21
|
|
|
22
22
|
impl StoredKeys {
|
|
23
23
|
fn from_json(value: &Value) -> Self {
|
|
24
|
-
let text = |field: &str|
|
|
25
|
-
|
|
24
|
+
let text = |field: &str| {
|
|
25
|
+
value
|
|
26
|
+
.get(field)
|
|
27
|
+
.and_then(|v| v.as_str())
|
|
28
|
+
.unwrap_or_default()
|
|
29
|
+
.to_string()
|
|
30
|
+
};
|
|
31
|
+
Self {
|
|
32
|
+
secret_key: text("secret_key"),
|
|
33
|
+
publishable_key: text("publishable_key"),
|
|
34
|
+
webhook_secret: text("webhook_secret"),
|
|
35
|
+
webhook_endpoint_id: text("webhook_endpoint_id"),
|
|
36
|
+
saved_at: text("saved_at"),
|
|
37
|
+
}
|
|
26
38
|
}
|
|
27
39
|
|
|
28
40
|
fn to_json(&self) -> Value {
|
|
@@ -45,8 +57,16 @@ pub struct StoredDocument {
|
|
|
45
57
|
|
|
46
58
|
impl StoredDocument {
|
|
47
59
|
fn from_json(value: &Value) -> Self {
|
|
48
|
-
let entry = |mode: &str|
|
|
49
|
-
|
|
60
|
+
let entry = |mode: &str| {
|
|
61
|
+
value
|
|
62
|
+
.get(mode)
|
|
63
|
+
.filter(|v| v.is_object())
|
|
64
|
+
.map(StoredKeys::from_json)
|
|
65
|
+
};
|
|
66
|
+
Self {
|
|
67
|
+
test: entry("test"),
|
|
68
|
+
live: entry("live"),
|
|
69
|
+
}
|
|
50
70
|
}
|
|
51
71
|
|
|
52
72
|
fn to_json(&self) -> Value {
|
|
@@ -61,17 +81,29 @@ impl StoredDocument {
|
|
|
61
81
|
|
|
62
82
|
/// The entry saved for `mode` ("test" or "live").
|
|
63
83
|
pub fn mode(&self, mode: &str) -> Option<&StoredKeys> {
|
|
64
|
-
if mode == "live" {
|
|
84
|
+
if mode == "live" {
|
|
85
|
+
self.live.as_ref()
|
|
86
|
+
} else {
|
|
87
|
+
self.test.as_ref()
|
|
88
|
+
}
|
|
65
89
|
}
|
|
66
90
|
|
|
67
91
|
/// Replaces (or with `None` removes) the entry for `mode`.
|
|
68
92
|
pub fn set(&mut self, mode: &str, keys: Option<StoredKeys>) {
|
|
69
|
-
if mode == "live" {
|
|
93
|
+
if mode == "live" {
|
|
94
|
+
self.live = keys;
|
|
95
|
+
} else {
|
|
96
|
+
self.test = keys;
|
|
97
|
+
}
|
|
70
98
|
}
|
|
71
99
|
|
|
72
100
|
/// Every webhook signing secret saved, for either mode.
|
|
73
101
|
pub fn webhook_secrets(&self) -> impl Iterator<Item = &str> {
|
|
74
|
-
[self.test.as_ref(), self.live.as_ref()]
|
|
102
|
+
[self.test.as_ref(), self.live.as_ref()]
|
|
103
|
+
.into_iter()
|
|
104
|
+
.flatten()
|
|
105
|
+
.map(|k| k.webhook_secret.as_str())
|
|
106
|
+
.filter(|s| !s.is_empty())
|
|
75
107
|
}
|
|
76
108
|
}
|
|
77
109
|
|
|
@@ -94,7 +126,10 @@ pub struct AwsRawStore {
|
|
|
94
126
|
impl AwsRawStore {
|
|
95
127
|
/// A store over the secret named `secret_id`; nothing is fetched until the first use.
|
|
96
128
|
pub fn new(secret_id: String) -> Self {
|
|
97
|
-
Self {
|
|
129
|
+
Self {
|
|
130
|
+
secret_id,
|
|
131
|
+
fetcher: tokio::sync::OnceCell::new(),
|
|
132
|
+
}
|
|
98
133
|
}
|
|
99
134
|
|
|
100
135
|
async fn fetcher(&self) -> &AwsSecretFetcher {
|
|
@@ -105,11 +140,17 @@ impl AwsRawStore {
|
|
|
105
140
|
#[async_trait::async_trait]
|
|
106
141
|
impl RawStore for AwsRawStore {
|
|
107
142
|
async fn read(&self) -> anyhow::Result<Option<String>> {
|
|
108
|
-
self.fetcher()
|
|
143
|
+
self.fetcher()
|
|
144
|
+
.await
|
|
145
|
+
.fetch_secret_string_if_present(&self.secret_id)
|
|
146
|
+
.await
|
|
109
147
|
}
|
|
110
148
|
|
|
111
149
|
async fn write(&self, document: &str) -> anyhow::Result<()> {
|
|
112
|
-
self.fetcher()
|
|
150
|
+
self.fetcher()
|
|
151
|
+
.await
|
|
152
|
+
.put_secret_string(&self.secret_id, document)
|
|
153
|
+
.await
|
|
113
154
|
}
|
|
114
155
|
}
|
|
115
156
|
|
|
@@ -123,12 +164,21 @@ pub struct KeyStore {
|
|
|
123
164
|
impl KeyStore {
|
|
124
165
|
/// A cached view over `raw`, starting with an empty cache.
|
|
125
166
|
pub fn new(raw: Arc<dyn RawStore>) -> Self {
|
|
126
|
-
Self {
|
|
167
|
+
Self {
|
|
168
|
+
raw,
|
|
169
|
+
cache: Mutex::new(None),
|
|
170
|
+
}
|
|
127
171
|
}
|
|
128
172
|
|
|
129
173
|
fn cached(&self) -> Option<Arc<StoredDocument>> {
|
|
130
|
-
let cache = self
|
|
131
|
-
|
|
174
|
+
let cache = self
|
|
175
|
+
.cache
|
|
176
|
+
.lock()
|
|
177
|
+
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
|
178
|
+
cache
|
|
179
|
+
.as_ref()
|
|
180
|
+
.filter(|(at, _)| at.elapsed() < CACHE_TTL)
|
|
181
|
+
.map(|(_, document)| document.clone())
|
|
132
182
|
}
|
|
133
183
|
|
|
134
184
|
/// The document, from the cache when it is under a minute old.
|
|
@@ -143,11 +193,18 @@ impl KeyStore {
|
|
|
143
193
|
pub async fn refresh(&self) -> anyhow::Result<Arc<StoredDocument>> {
|
|
144
194
|
let text = self.raw.read().await?;
|
|
145
195
|
let document = match text {
|
|
146
|
-
Some(text) => StoredDocument::from_json(
|
|
196
|
+
Some(text) => StoredDocument::from_json(
|
|
197
|
+
&serde_json::from_str::<Value>(&text)
|
|
198
|
+
.map_err(|_| anyhow::anyhow!("the stored payment keys are not valid JSON"))?,
|
|
199
|
+
),
|
|
147
200
|
None => StoredDocument::default(),
|
|
148
201
|
};
|
|
149
202
|
let document = Arc::new(document);
|
|
150
|
-
*self
|
|
203
|
+
*self
|
|
204
|
+
.cache
|
|
205
|
+
.lock()
|
|
206
|
+
.unwrap_or_else(std::sync::PoisonError::into_inner) =
|
|
207
|
+
Some((Instant::now(), document.clone()));
|
|
151
208
|
Ok(document)
|
|
152
209
|
}
|
|
153
210
|
|
|
@@ -165,40 +222,59 @@ impl KeyStore {
|
|
|
165
222
|
|
|
166
223
|
/// Forgets the cached document.
|
|
167
224
|
pub fn invalidate(&self) {
|
|
168
|
-
*self
|
|
225
|
+
*self
|
|
226
|
+
.cache
|
|
227
|
+
.lock()
|
|
228
|
+
.unwrap_or_else(std::sync::PoisonError::into_inner) = None;
|
|
169
229
|
}
|
|
170
230
|
}
|
|
171
231
|
|
|
172
232
|
/// The secret's name from a raw `PAYMENTS_ACCOUNT_SECRET_ID` value, or `None`
|
|
173
233
|
/// when it is unset or blank.
|
|
174
234
|
fn configured_secret_id(configured: Option<&str>) -> Option<String> {
|
|
175
|
-
configured
|
|
235
|
+
configured
|
|
236
|
+
.map(str::trim)
|
|
237
|
+
.filter(|id| !id.is_empty())
|
|
238
|
+
.map(String::from)
|
|
176
239
|
}
|
|
177
240
|
|
|
178
241
|
/// Whether this process runs as a container task or a Lambda function on AWS.
|
|
179
242
|
fn on_aws() -> bool {
|
|
180
|
-
["ECS_CONTAINER_METADATA_URI_V4", "AWS_LAMBDA_FUNCTION_NAME"]
|
|
243
|
+
["ECS_CONTAINER_METADATA_URI_V4", "AWS_LAMBDA_FUNCTION_NAME"]
|
|
244
|
+
.iter()
|
|
245
|
+
.any(|name| std::env::var(name).is_ok_and(|v| !v.is_empty()))
|
|
181
246
|
}
|
|
182
247
|
|
|
183
248
|
/// The message that stops the boot when checkout is enabled on AWS without a
|
|
184
249
|
/// secret to keep the business's keys in; `None` when the boot may go on.
|
|
185
|
-
pub fn refusal_at_boot(
|
|
250
|
+
pub fn refusal_at_boot(
|
|
251
|
+
checkout_enabled: bool,
|
|
252
|
+
on_aws: bool,
|
|
253
|
+
configured: Option<&str>,
|
|
254
|
+
) -> Option<String> {
|
|
186
255
|
let missing = checkout_enabled && on_aws && configured_secret_id(configured).is_none();
|
|
187
256
|
missing.then(|| format!("{SECRET_ID_VARIABLE} is required on AWS when checkout is enabled: name the Secrets Manager secret that holds the business's payment keys"))
|
|
188
257
|
}
|
|
189
258
|
|
|
190
259
|
/// `refusal_at_boot` for this process's environment.
|
|
191
260
|
pub fn check_boot(checkout_enabled: bool) -> Result<(), String> {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
261
|
+
refusal_at_boot(
|
|
262
|
+
checkout_enabled,
|
|
263
|
+
on_aws(),
|
|
264
|
+
std::env::var(SECRET_ID_VARIABLE).ok().as_deref(),
|
|
265
|
+
)
|
|
266
|
+
.map_or(Ok(()), Err)
|
|
196
267
|
}
|
|
197
268
|
|
|
198
269
|
/// The process-wide store, built once so its cache is shared by every request.
|
|
199
270
|
pub fn default_store() -> Option<Arc<KeyStore>> {
|
|
200
271
|
static STORE: OnceLock<Option<Arc<KeyStore>>> = OnceLock::new();
|
|
201
|
-
STORE
|
|
272
|
+
STORE
|
|
273
|
+
.get_or_init(|| {
|
|
274
|
+
configured_secret_id(std::env::var(SECRET_ID_VARIABLE).ok().as_deref())
|
|
275
|
+
.map(|id| Arc::new(KeyStore::new(Arc::new(AwsRawStore::new(id)))))
|
|
276
|
+
})
|
|
277
|
+
.clone()
|
|
202
278
|
}
|
|
203
279
|
|
|
204
280
|
/// `YYYY-MM-DDTHH:MM:SSZ` for a Unix time in seconds (proleptic Gregorian,
|
|
@@ -208,18 +284,33 @@ pub fn utc_timestamp(seconds: u64) -> String {
|
|
|
208
284
|
let z = days + 719_468;
|
|
209
285
|
let era = z.div_euclid(146_097);
|
|
210
286
|
let day_of_era = z.rem_euclid(146_097);
|
|
211
|
-
let year_of_era =
|
|
287
|
+
let year_of_era =
|
|
288
|
+
(day_of_era - day_of_era / 1_460 + day_of_era / 36_524 - day_of_era / 146_096) / 365;
|
|
212
289
|
let day_of_year = day_of_era - (365 * year_of_era + year_of_era / 4 - year_of_era / 100);
|
|
213
290
|
let month_shifted = (5 * day_of_year + 2) / 153;
|
|
214
291
|
let day = day_of_year - (153 * month_shifted + 2) / 5 + 1;
|
|
215
|
-
let month = if month_shifted < 10 {
|
|
292
|
+
let month = if month_shifted < 10 {
|
|
293
|
+
month_shifted + 3
|
|
294
|
+
} else {
|
|
295
|
+
month_shifted - 9
|
|
296
|
+
};
|
|
216
297
|
let year = year_of_era + era * 400 + i64::from(month <= 2);
|
|
217
|
-
format!(
|
|
298
|
+
format!(
|
|
299
|
+
"{year:04}-{month:02}-{day:02}T{:02}:{:02}:{:02}Z",
|
|
300
|
+
rest / 3_600,
|
|
301
|
+
rest % 3_600 / 60,
|
|
302
|
+
rest % 60
|
|
303
|
+
)
|
|
218
304
|
}
|
|
219
305
|
|
|
220
306
|
/// The current time as `utc_timestamp` writes it.
|
|
221
307
|
pub fn now_timestamp() -> String {
|
|
222
|
-
utc_timestamp(
|
|
308
|
+
utc_timestamp(
|
|
309
|
+
std::time::SystemTime::now()
|
|
310
|
+
.duration_since(std::time::UNIX_EPOCH)
|
|
311
|
+
.unwrap_or_default()
|
|
312
|
+
.as_secs(),
|
|
313
|
+
)
|
|
223
314
|
}
|
|
224
315
|
|
|
225
316
|
/// An in-memory stand-in for Secrets Manager that counts its calls, for the
|
|
@@ -239,15 +330,23 @@ pub(crate) struct MemoryStore {
|
|
|
239
330
|
impl RawStore for MemoryStore {
|
|
240
331
|
async fn read(&self) -> anyhow::Result<Option<String>> {
|
|
241
332
|
self.reads.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
|
|
242
|
-
Ok(self
|
|
333
|
+
Ok(self
|
|
334
|
+
.document
|
|
335
|
+
.lock()
|
|
336
|
+
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
|
337
|
+
.clone())
|
|
243
338
|
}
|
|
244
339
|
|
|
245
340
|
async fn write(&self, document: &str) -> anyhow::Result<()> {
|
|
246
|
-
self.writes
|
|
341
|
+
self.writes
|
|
342
|
+
.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
|
|
247
343
|
if self.fail_writes.load(std::sync::atomic::Ordering::SeqCst) {
|
|
248
344
|
return Err(anyhow::anyhow!("writing the secret failed"));
|
|
249
345
|
}
|
|
250
|
-
*self
|
|
346
|
+
*self
|
|
347
|
+
.document
|
|
348
|
+
.lock()
|
|
349
|
+
.unwrap_or_else(std::sync::PoisonError::into_inner) = Some(document.to_string());
|
|
251
350
|
Ok(())
|
|
252
351
|
}
|
|
253
352
|
}
|
|
@@ -258,7 +357,13 @@ mod tests {
|
|
|
258
357
|
use std::sync::atomic::Ordering;
|
|
259
358
|
|
|
260
359
|
fn keys(secret: &str) -> StoredKeys {
|
|
261
|
-
StoredKeys {
|
|
360
|
+
StoredKeys {
|
|
361
|
+
secret_key: secret.into(),
|
|
362
|
+
publishable_key: "pk_test_x".into(),
|
|
363
|
+
webhook_secret: "whsec_x".into(),
|
|
364
|
+
webhook_endpoint_id: "we_1".into(),
|
|
365
|
+
saved_at: "2026-09-26T05:00:00Z".into(),
|
|
366
|
+
}
|
|
262
367
|
}
|
|
263
368
|
|
|
264
369
|
#[tokio::test]
|
|
@@ -267,12 +372,23 @@ mod tests {
|
|
|
267
372
|
let store = KeyStore::new(raw.clone());
|
|
268
373
|
assert!(store.document().await.unwrap().test.is_none());
|
|
269
374
|
assert!(store.document().await.unwrap().test.is_none());
|
|
270
|
-
assert_eq!(
|
|
271
|
-
|
|
272
|
-
|
|
375
|
+
assert_eq!(
|
|
376
|
+
raw.reads.load(Ordering::SeqCst),
|
|
377
|
+
1,
|
|
378
|
+
"the second read was served from the cache"
|
|
379
|
+
);
|
|
380
|
+
|
|
381
|
+
store
|
|
382
|
+
.update(|doc| doc.set("test", Some(keys("rk_test_one"))))
|
|
383
|
+
.await
|
|
384
|
+
.unwrap();
|
|
273
385
|
let reads_after_update = raw.reads.load(Ordering::SeqCst);
|
|
274
386
|
let document = store.document().await.unwrap();
|
|
275
|
-
assert_eq!(
|
|
387
|
+
assert_eq!(
|
|
388
|
+
document.test.as_ref().unwrap().secret_key,
|
|
389
|
+
"rk_test_one",
|
|
390
|
+
"the write invalidated the cache"
|
|
391
|
+
);
|
|
276
392
|
assert_eq!(raw.reads.load(Ordering::SeqCst), reads_after_update + 1);
|
|
277
393
|
assert_eq!(raw.writes.load(Ordering::SeqCst), 1);
|
|
278
394
|
}
|
|
@@ -281,20 +397,38 @@ mod tests {
|
|
|
281
397
|
async fn the_stored_json_has_exactly_the_agreed_shape() {
|
|
282
398
|
let raw = Arc::new(MemoryStore::default());
|
|
283
399
|
let store = KeyStore::new(raw.clone());
|
|
284
|
-
store
|
|
285
|
-
|
|
400
|
+
store
|
|
401
|
+
.update(|doc| doc.set("live", Some(keys("rk_live_one"))))
|
|
402
|
+
.await
|
|
403
|
+
.unwrap();
|
|
404
|
+
let stored: serde_json::Value =
|
|
405
|
+
serde_json::from_str(raw.document.lock().unwrap().as_ref().unwrap()).unwrap();
|
|
286
406
|
assert!(stored.get("test").is_none(), "an unsaved mode has no entry");
|
|
287
407
|
let live = stored["live"].as_object().unwrap();
|
|
288
408
|
let mut names: Vec<&str> = live.keys().map(String::as_str).collect();
|
|
289
409
|
names.sort_unstable();
|
|
290
|
-
assert_eq!(
|
|
410
|
+
assert_eq!(
|
|
411
|
+
names,
|
|
412
|
+
[
|
|
413
|
+
"publishable_key",
|
|
414
|
+
"saved_at",
|
|
415
|
+
"secret_key",
|
|
416
|
+
"webhook_endpoint_id",
|
|
417
|
+
"webhook_secret"
|
|
418
|
+
]
|
|
419
|
+
);
|
|
291
420
|
}
|
|
292
421
|
|
|
293
422
|
#[tokio::test]
|
|
294
423
|
async fn an_unreadable_document_is_an_error_that_does_not_quote_it() {
|
|
295
424
|
let raw = Arc::new(MemoryStore::default());
|
|
296
425
|
*raw.document.lock().unwrap() = Some("rk_live_SECRET not json".to_string());
|
|
297
|
-
let error = KeyStore::new(raw)
|
|
426
|
+
let error = KeyStore::new(raw)
|
|
427
|
+
.document()
|
|
428
|
+
.await
|
|
429
|
+
.err()
|
|
430
|
+
.expect("an error")
|
|
431
|
+
.to_string();
|
|
298
432
|
assert!(!error.contains("rk_live_SECRET"), "{error}");
|
|
299
433
|
}
|
|
300
434
|
|
|
@@ -303,21 +437,39 @@ mod tests {
|
|
|
303
437
|
assert_eq!(configured_secret_id(None), None);
|
|
304
438
|
assert_eq!(configured_secret_id(Some("")), None);
|
|
305
439
|
assert_eq!(configured_secret_id(Some(" ")), None);
|
|
306
|
-
assert_eq!(
|
|
440
|
+
assert_eq!(
|
|
441
|
+
configured_secret_id(Some(" payments-keys ")),
|
|
442
|
+
Some("payments-keys".to_string())
|
|
443
|
+
);
|
|
307
444
|
}
|
|
308
445
|
|
|
309
446
|
#[test]
|
|
310
447
|
fn checkout_on_aws_without_a_secret_id_refuses_the_boot_and_names_the_variable() {
|
|
311
448
|
let message = refusal_at_boot(true, true, None).expect("refused");
|
|
312
449
|
assert!(message.contains("PAYMENTS_ACCOUNT_SECRET_ID"), "{message}");
|
|
313
|
-
assert!(
|
|
450
|
+
assert!(
|
|
451
|
+
refusal_at_boot(true, true, Some(" ")).is_some(),
|
|
452
|
+
"a blank value is unset"
|
|
453
|
+
);
|
|
314
454
|
}
|
|
315
455
|
|
|
316
456
|
#[test]
|
|
317
457
|
fn every_other_combination_boots() {
|
|
318
|
-
assert_eq!(
|
|
319
|
-
|
|
320
|
-
|
|
458
|
+
assert_eq!(
|
|
459
|
+
refusal_at_boot(true, true, Some("payments-keys")),
|
|
460
|
+
None,
|
|
461
|
+
"set: unchanged"
|
|
462
|
+
);
|
|
463
|
+
assert_eq!(
|
|
464
|
+
refusal_at_boot(true, false, None),
|
|
465
|
+
None,
|
|
466
|
+
"off AWS: only environment keys"
|
|
467
|
+
);
|
|
468
|
+
assert_eq!(
|
|
469
|
+
refusal_at_boot(false, true, None),
|
|
470
|
+
None,
|
|
471
|
+
"checkout off: payments are not served"
|
|
472
|
+
);
|
|
321
473
|
assert_eq!(refusal_at_boot(false, false, None), None);
|
|
322
474
|
}
|
|
323
475
|
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
//! Which signed-in roles may manage a site's payments: a per-site setting, read once at boot.
|
|
2
|
+
//!
|
|
3
|
+
//! `HECKS_PAYMENTS_MANAGER_ROLES` is a comma-separated list of exact role names. Unset means
|
|
4
|
+
//! `Owner`, which is what the host did before the setting existed. A set value must be a
|
|
5
|
+
//! plausible list or the boot is refused; a refusal names the variable and the kind of mistake,
|
|
6
|
+
//! never the value. A caller manages payments when their role is in the list, compared exactly:
|
|
7
|
+
//! no case folding, no hierarchy, no wildcard.
|
|
8
|
+
|
|
9
|
+
use std::sync::OnceLock;
|
|
10
|
+
|
|
11
|
+
/// The variable that names the roles allowed to manage payments.
|
|
12
|
+
pub const VARIABLE: &str = "HECKS_PAYMENTS_MANAGER_ROLES";
|
|
13
|
+
/// The role that manages payments when the variable is unset.
|
|
14
|
+
pub const DEFAULT_ROLE: &str = "Owner";
|
|
15
|
+
/// The most roles one list may name.
|
|
16
|
+
pub const MAX_ENTRIES: usize = 8;
|
|
17
|
+
/// The longest role name, after trimming.
|
|
18
|
+
pub const MAX_ENTRY_LENGTH: usize = 32;
|
|
19
|
+
|
|
20
|
+
/// The roles that manage payments when nothing is configured.
|
|
21
|
+
pub fn default_roles() -> Vec<String> {
|
|
22
|
+
vec![DEFAULT_ROLE.to_string()]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/// Parses the variable's value; `None` (unset) gives the default list.
|
|
26
|
+
pub fn parse(raw: Option<&str>) -> Result<Vec<String>, String> {
|
|
27
|
+
let Some(raw) = raw else {
|
|
28
|
+
return Ok(default_roles());
|
|
29
|
+
};
|
|
30
|
+
let refuse = |why: &str| Err(format!("{VARIABLE} is invalid: {why}"));
|
|
31
|
+
let entries: Vec<&str> = raw.split(',').map(str::trim).collect();
|
|
32
|
+
if entries.len() > MAX_ENTRIES {
|
|
33
|
+
return refuse(&format!("it names more than {MAX_ENTRIES} roles"));
|
|
34
|
+
}
|
|
35
|
+
for (index, entry) in entries.iter().enumerate() {
|
|
36
|
+
let position = index + 1;
|
|
37
|
+
if entry.is_empty() {
|
|
38
|
+
return refuse(&format!(
|
|
39
|
+
"entry {position} is empty; name at least one role and no blank entries"
|
|
40
|
+
));
|
|
41
|
+
}
|
|
42
|
+
if entry.len() > MAX_ENTRY_LENGTH {
|
|
43
|
+
return refuse(&format!(
|
|
44
|
+
"entry {position} is longer than {MAX_ENTRY_LENGTH} characters"
|
|
45
|
+
));
|
|
46
|
+
}
|
|
47
|
+
let starts_well = entry.starts_with(|c: char| c.is_ascii_alphanumeric());
|
|
48
|
+
if !starts_well
|
|
49
|
+
|| !entry
|
|
50
|
+
.chars()
|
|
51
|
+
.all(|c| c.is_ascii_alphanumeric() || c == ' ' || c == '-')
|
|
52
|
+
{
|
|
53
|
+
return refuse(&format!("entry {position} is not a role name: letters, digits, spaces and hyphens only, starting with a letter or digit"));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
Ok(entries.into_iter().map(String::from).collect())
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/// Reads the setting into `cell` the first time and never again, whatever the environment
|
|
60
|
+
/// does afterwards. `read` stands for the environment lookup.
|
|
61
|
+
pub fn read_once(
|
|
62
|
+
cell: &OnceLock<Result<Vec<String>, String>>,
|
|
63
|
+
read: impl FnOnce() -> Option<String>,
|
|
64
|
+
) -> &Result<Vec<String>, String> {
|
|
65
|
+
cell.get_or_init(|| parse(read().as_deref()))
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/// This process's setting, read from the environment on first use (the boot check is the first).
|
|
69
|
+
pub fn configured() -> &'static Result<Vec<String>, String> {
|
|
70
|
+
static CELL: OnceLock<Result<Vec<String>, String>> = OnceLock::new();
|
|
71
|
+
read_once(&CELL, || std::env::var(VARIABLE).ok())
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/// Refuses the boot when this process's setting is invalid.
|
|
75
|
+
pub fn check_boot() -> Result<(), String> {
|
|
76
|
+
configured().as_ref().map(|_| ()).map_err(Clone::clone)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/// The roles for a request. An invalid setting never gets here (the boot was refused); should it
|
|
80
|
+
/// ever, only the default role manages, so a mistake narrows access instead of widening it.
|
|
81
|
+
pub fn manager_roles() -> Vec<String> {
|
|
82
|
+
configured().clone().unwrap_or_else(|_| default_roles())
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/// Whether `role` is one of `roles`, by exact string match.
|
|
86
|
+
pub fn manages(roles: &[String], role: &str) -> bool {
|
|
87
|
+
roles.iter().any(|allowed| allowed == role)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
#[cfg(test)]
|
|
91
|
+
mod tests {
|
|
92
|
+
use super::*;
|
|
93
|
+
|
|
94
|
+
fn roles(raw: Option<&str>) -> Vec<String> {
|
|
95
|
+
parse(raw).unwrap()
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
#[test]
|
|
99
|
+
fn unset_means_owner_only() {
|
|
100
|
+
let list = roles(None);
|
|
101
|
+
assert_eq!(list, ["Owner"]);
|
|
102
|
+
assert!(manages(&list, "Owner"));
|
|
103
|
+
assert!(!manages(&list, "Admin"));
|
|
104
|
+
assert!(!manages(&list, "Member"));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
#[test]
|
|
108
|
+
fn a_configured_list_admits_exactly_its_roles() {
|
|
109
|
+
let list = roles(Some("Owner,Admin"));
|
|
110
|
+
assert!(manages(&list, "Owner") && manages(&list, "Admin"));
|
|
111
|
+
assert!(!manages(&list, "Editor"));
|
|
112
|
+
assert!(!manages(&list, ""));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
#[test]
|
|
116
|
+
fn whitespace_is_trimmed_and_case_is_exact() {
|
|
117
|
+
let list = roles(Some(" Owner , Account Admin "));
|
|
118
|
+
assert_eq!(list, ["Owner", "Account Admin"]);
|
|
119
|
+
let lower = roles(Some("admin"));
|
|
120
|
+
assert!(manages(&lower, "admin"));
|
|
121
|
+
assert!(!manages(&lower, "Admin"), "no case folding");
|
|
122
|
+
assert!(!manages(&roles(Some("Admin")), "admin"));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
#[test]
|
|
126
|
+
fn no_wildcard_and_no_hierarchy() {
|
|
127
|
+
assert!(parse(Some("*")).is_err());
|
|
128
|
+
assert!(
|
|
129
|
+
!manages(&roles(Some("Admin")), "Owner"),
|
|
130
|
+
"naming Admin does not admit Owner"
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
#[test]
|
|
135
|
+
fn an_invalid_setting_is_refused_naming_the_variable() {
|
|
136
|
+
let too_many = ["Role"; MAX_ENTRIES + 1].join(",");
|
|
137
|
+
let too_long = "A".repeat(MAX_ENTRY_LENGTH + 1);
|
|
138
|
+
for bad in [
|
|
139
|
+
"",
|
|
140
|
+
" ",
|
|
141
|
+
",",
|
|
142
|
+
"Owner,",
|
|
143
|
+
",Owner",
|
|
144
|
+
"Owner,,Admin",
|
|
145
|
+
"Own/er",
|
|
146
|
+
"Owner\nAdmin",
|
|
147
|
+
"Own\u{7}er",
|
|
148
|
+
"Admin;Owner",
|
|
149
|
+
"-Admin",
|
|
150
|
+
"Ad_min",
|
|
151
|
+
"Régisseur",
|
|
152
|
+
too_many.as_str(),
|
|
153
|
+
too_long.as_str(),
|
|
154
|
+
] {
|
|
155
|
+
let message = parse(Some(bad)).expect_err(bad);
|
|
156
|
+
assert!(message.starts_with(VARIABLE), "{message}");
|
|
157
|
+
}
|
|
158
|
+
assert!(parse(Some(&"A".repeat(MAX_ENTRY_LENGTH))).is_ok());
|
|
159
|
+
assert!(parse(Some(&["Role"; MAX_ENTRIES].join(","))).is_ok());
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
#[test]
|
|
163
|
+
fn a_refusal_never_echoes_the_value() {
|
|
164
|
+
let message = parse(Some("sk_live_SECRET/value")).unwrap_err();
|
|
165
|
+
assert!(!message.contains("sk_live"), "{message}");
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
#[test]
|
|
169
|
+
fn the_setting_is_read_once() {
|
|
170
|
+
let cell = OnceLock::new();
|
|
171
|
+
let reads = std::cell::Cell::new(0);
|
|
172
|
+
let first = read_once(&cell, || {
|
|
173
|
+
reads.set(reads.get() + 1);
|
|
174
|
+
Some("Owner,Admin".to_string())
|
|
175
|
+
});
|
|
176
|
+
assert_eq!(first.as_ref().unwrap(), &["Owner", "Admin"]);
|
|
177
|
+
let second = read_once(&cell, || {
|
|
178
|
+
reads.set(reads.get() + 1);
|
|
179
|
+
Some("Editor".to_string())
|
|
180
|
+
});
|
|
181
|
+
assert_eq!(second.as_ref().unwrap(), &["Owner", "Admin"]);
|
|
182
|
+
assert_eq!(reads.get(), 1);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
#[test]
|
|
186
|
+
fn an_invalid_value_stays_refused_once_read() {
|
|
187
|
+
let cell = OnceLock::new();
|
|
188
|
+
assert!(read_once(&cell, || Some("a/b".to_string())).is_err());
|
|
189
|
+
assert!(read_once(&cell, || None).is_err());
|
|
190
|
+
}
|
|
191
|
+
}
|