hecks 2.9.0 → 3.0.1
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/exe/hecks +51 -6
- data/lib/hecks/adapters/driven/heki/journal.rb +1 -1
- data/lib/hecks/adapters/driven/lambda/client.rb +1 -1
- data/lib/hecks/adapters/driven/lambda.rb +1 -1
- data/lib/hecks/adapters/driven/memory.rb +16 -3
- data/lib/hecks/adapters/driven/postgres/reconnect.rb +6 -2
- data/lib/hecks/adapters/driven/postgres/shared_connection.rb +180 -0
- data/lib/hecks/adapters/driven/postgres.rb +13 -4
- data/lib/hecks/adapters/driven/prism.rb +41 -8
- data/lib/hecks/adapters/driven/tenant_provisioner.rb +73 -16
- data/lib/hecks/behaviors/expectations.rb +1 -1
- data/lib/hecks/behaviors.rb +1 -1
- data/lib/hecks/bench/cli.rb +62 -0
- data/lib/hecks/bench/environment.rb +71 -0
- data/lib/hecks/bench/postgres_probe.rb +30 -0
- data/lib/hecks/bench/report.rb +76 -0
- data/lib/hecks/bench/ruby_runner.rb +93 -0
- data/lib/hecks/bench/run.rb +55 -0
- data/lib/hecks/bench/rust_runner.rb +101 -0
- data/lib/hecks/bench/stats.rb +46 -0
- data/lib/hecks/bench/suite.rb +98 -0
- data/lib/hecks/bench/workload.rb +89 -0
- data/lib/hecks/bench.rb +17 -0
- data/lib/hecks/bluebook/assembly/contracts.rb +2 -0
- data/lib/hecks/bluebook/attribute.rb +1 -1
- data/lib/hecks/bluebook/behaviour/aggregate.rb +30 -1
- data/lib/hecks/bluebook/behaviour/domain_port.rb +7 -0
- data/lib/hecks/bluebook/chapter.rb +6 -2
- data/lib/hecks/bluebook/domain_port.rb +36 -5
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +10 -0
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +8 -0
- data/lib/hecks/bluebook/dsl/bootstrap_table.rb +2 -1
- data/lib/hecks/bluebook/dsl/domain_port_builder.rb +31 -3
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +122 -3
- data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +1 -1
- data/lib/hecks/bluebook/dsl/query_builder.rb +27 -1
- data/lib/hecks/bluebook/dsl/rule_reference.rb +1 -1
- data/lib/hecks/bluebook/dsl/world_builder.rb +29 -2
- data/lib/hecks/bluebook/expression/canonical_form.rb +1 -1
- data/lib/hecks/bluebook/expression/resolver.rb +3 -2
- data/lib/hecks/bluebook/hexagon.rb +13 -5
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +1 -0
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +4 -1
- data/lib/hecks/bluebook/meta_validator/verdict_cache.rb +246 -0
- data/lib/hecks/bluebook/meta_validator.rb +9 -4
- data/lib/hecks/bluebook/model_check.rb +23 -0
- data/lib/hecks/bluebook/pattern_subset.rb +38 -0
- data/lib/hecks/bluebook/query.rb +20 -5
- data/lib/hecks/bluebook/smoke_test.rb +1 -1
- data/lib/hecks/canonical_json.rb +30 -0
- data/lib/hecks/chapters.rb +102 -0
- data/lib/hecks/cli/behaviors.rb +61 -0
- data/lib/hecks/cli/canonicalise.rb +25 -0
- data/lib/hecks/cli/check_engine_agreement.rb +32 -0
- data/lib/hecks/cli/check_era.rb +88 -0
- data/lib/hecks/cli/console.rb +66 -0
- data/lib/hecks/cli/deploy_template_diff.rb +40 -0
- data/lib/hecks/cli/doc_coverage.rb +32 -0
- data/lib/hecks/cli/document.rb +2 -2
- data/lib/hecks/cli/expression_projection.rb +41 -0
- data/lib/hecks/cli/follow.rb +88 -0
- data/lib/hecks/cli/history.rb +44 -0
- data/lib/hecks/cli/ir.rb +1 -1
- data/lib/hecks/cli/mcp.rb +3 -3
- data/lib/hecks/cli/model_check.rb +15 -8
- data/lib/hecks/cli/pattern_cases.rb +47 -0
- data/lib/hecks/cli/present.rb +39 -0
- data/lib/hecks/cli/project_cli.rb +224 -32
- data/lib/hecks/cli/project_diagrams.rb +4 -3
- data/lib/hecks/cli/project_glossary.rb +44 -0
- data/lib/hecks/cli/reference.rb +46 -0
- data/lib/hecks/cli/refresh_projections.rb +44 -0
- data/lib/hecks/cli/refresh_rspec_runtime_baseline.rb +150 -0
- data/lib/hecks/cli/release.rb +70 -0
- data/lib/hecks/cli/release_gem.rb +59 -0
- data/lib/hecks/cli/rspec_io_parallel_files.rb +137 -0
- data/lib/hecks/cli/rspec_shard_files.rb +66 -0
- data/lib/hecks/cli/run.rb +3 -3
- data/lib/hecks/cli/seed_semantics_corpus.rb +71 -0
- data/lib/hecks/cli/serve_query_ir_mcp.rb +30 -0
- data/lib/hecks/cli/shape.rb +70 -0
- data/lib/hecks/cli/smoke_http.rb +230 -0
- data/lib/hecks/cli/smoke_test.rb +5 -3
- data/lib/hecks/cli/spec_example.rb +27 -0
- data/lib/hecks/cli/statements.rb +32 -0
- data/lib/hecks/cli/stores.rb +4 -5
- data/lib/hecks/cli/stress_concurrency_specs.rb +177 -0
- data/lib/hecks/cli.rb +201 -52
- data/lib/hecks/codemod.rb +324 -0
- data/lib/hecks/corpus.rb +423 -0
- data/lib/hecks/deploy/bluebook/deploy.bluebook +591 -23
- data/lib/hecks/deploy/bluebook/deploy.hecksagon +41 -4
- data/lib/hecks/deploy/oidc.json +82 -2
- data/lib/hecks/doc/reference.rb +351 -0
- data/lib/hecks/doors/cli_door.rb +196 -0
- data/lib/hecks/doors/cli_runner.rb +297 -0
- data/lib/hecks/{facade → doors}/command_request.rb +1 -1
- data/lib/hecks/{facade → doors}/handle.rb +2 -2
- data/lib/hecks/{facade → doors}/json_door.rb +4 -4
- data/lib/hecks/doors/launcher_options.rb +129 -0
- data/lib/hecks/{cli → doors}/mcp_door.rb +7 -7
- data/lib/hecks/doors/mcp_door_scope.rb +97 -0
- data/lib/hecks/{facade/surface → doors/ruby_door}/aggregate_door.rb +8 -3
- data/lib/hecks/{facade/surface → doors/ruby_door}/chapter.rb +3 -3
- data/lib/hecks/{facade/surface.rb → doors/ruby_door.rb} +36 -5
- data/lib/hecks/doors.rb +61 -0
- data/lib/hecks/embryonaut_bluebook/vendor_cli.rb +1 -1
- data/lib/hecks/embryonaut_bluebook.rb +1 -1
- data/lib/hecks/engine_agreement.rb +156 -0
- data/lib/hecks/facade.rb +24 -37
- data/lib/hecks/forms/app.rb +6 -6
- data/lib/hecks/forms/banking_presentation.rb +50 -0
- data/lib/hecks/forms/field_shape.rb +2 -1
- data/lib/hecks/forms/port_argument.rb +1 -1
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +393 -0
- data/lib/hecks/fuzzing/combination_miner.rb +86 -0
- data/lib/hecks/fuzzing/concurrent_dispatch.rb +214 -0
- data/lib/hecks/fuzzing/coverage_campaign.rb +143 -0
- data/lib/hecks/fuzzing/differential.rb +156 -0
- data/lib/hecks/fuzzing/domain_generator.rb +615 -0
- data/lib/hecks/fuzzing/era_boundary.rb +71 -0
- data/lib/hecks/fuzzing/form_census.rb +136 -0
- data/lib/hecks/fuzzing/generated_domain_check.rb +93 -0
- data/lib/hecks/fuzzing/invalid_value_generator.rb +64 -0
- data/lib/hecks/fuzzing/isolated_boot.rb +301 -0
- data/lib/hecks/fuzzing/nondeterministic.rb +51 -0
- data/lib/hecks/fuzzing/outside_world.rb +45 -0
- data/lib/hecks/fuzzing/persistence_parity.rb +102 -0
- data/lib/hecks/fuzzing/properties/corrections.rb +88 -0
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +314 -0
- data/lib/hecks/fuzzing/properties/guards.rb +190 -0
- data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +300 -0
- data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +93 -0
- data/lib/hecks/fuzzing/properties/outbox.rb +89 -0
- data/lib/hecks/fuzzing/properties/querying.rb +175 -0
- data/lib/hecks/fuzzing/properties.rb +162 -0
- data/lib/hecks/fuzzing/qa_settings.rb +123 -0
- data/lib/hecks/fuzzing/replay.rb +505 -0
- data/lib/hecks/fuzzing/rotation_priority.rb +48 -0
- data/lib/hecks/fuzzing/rust_gap_manifest.rb +108 -0
- data/lib/hecks/fuzzing/self_consistency.rb +339 -0
- data/lib/hecks/fuzzing/sequence_generator/adversary.rb +385 -0
- data/lib/hecks/fuzzing/sequence_generator/catalog.rb +132 -0
- data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +95 -0
- data/lib/hecks/fuzzing/sequence_generator/picker.rb +77 -0
- data/lib/hecks/fuzzing/sequence_generator/query_binding.rb +109 -0
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +237 -0
- data/lib/hecks/fuzzing/sequence_generator.rb +189 -0
- data/lib/hecks/fuzzing/shrinker.rb +166 -0
- data/lib/hecks/fuzzing/structural_skips.rb +33 -0
- data/lib/hecks/fuzzing/sweep_depth.rb +43 -0
- data/lib/hecks/fuzzing/target_capabilities.rb +163 -0
- data/lib/hecks/fuzzing/value_generator.rb +216 -0
- data/lib/hecks/fuzzing.rb +23 -0
- data/lib/hecks/grammar/evolve.rb +336 -0
- data/lib/hecks/hecks/adapters/codebase/codemods.rb +45 -0
- data/lib/hecks/hecks/adapters/codebase/conformance.rb +71 -0
- data/lib/hecks/hecks/adapters/codebase/corpus_tasks.rb +202 -0
- data/lib/hecks/hecks/adapters/codebase/gem_registry.rb +80 -0
- data/lib/hecks/hecks/adapters/codebase/kernel_tables.rb +54 -0
- data/lib/hecks/hecks/adapters/codebase/language.rb +240 -0
- data/lib/hecks/hecks/adapters/codebase/npm_registry.rb +102 -0
- data/lib/hecks/hecks/adapters/codebase/publishing.rb +142 -0
- data/lib/hecks/hecks/adapters/codebase/regeneration.rb +67 -0
- data/lib/hecks/hecks/adapters/codebase/release_facts.rb +90 -0
- data/lib/hecks/hecks/adapters/codebase/ruby_child.rb +83 -0
- data/lib/hecks/hecks/adapters/codebase/secret_vault.rb +36 -0
- data/lib/hecks/hecks/adapters/codebase/source_tree.adapter +7 -0
- data/lib/hecks/hecks/adapters/codebase/source_tree.rb +189 -0
- data/lib/hecks/hecks/adapters/codebase/sqlite_fixture.rb +71 -0
- data/lib/hecks/hecks/adapters/codebase/style.rb +171 -0
- data/lib/hecks/hecks/adapters/codebase/test_runner.rb +55 -0
- data/lib/hecks/hecks/adapters/codebase/test_suite.rb +165 -0
- data/lib/hecks/hecks/adapters/codebase/tree.rb +111 -0
- data/lib/hecks/hecks/adapters/console_capture.rb +79 -0
- data/lib/hecks/hecks/adapters/deploy_toolchain.adapter +7 -0
- data/lib/hecks/hecks/adapters/deploy_toolchain.rb +108 -0
- data/lib/hecks/hecks/adapters/git.adapter +7 -0
- data/lib/hecks/hecks/adapters/git.rb +102 -0
- data/lib/hecks/hecks/adapters/host_http.adapter +7 -0
- data/lib/hecks/hecks/adapters/host_http.rb +65 -0
- data/lib/hecks/hecks/adapters/in_process_boot.adapter +7 -0
- data/lib/hecks/hecks/adapters/in_process_boot.rb +197 -0
- data/lib/hecks/hecks/adapters/in_process_operations.rb +202 -0
- data/lib/hecks/hecks/adapters/journal_store/changes.rb +143 -0
- data/lib/hecks/hecks/adapters/journal_store/compaction.rb +145 -0
- data/lib/hecks/hecks/adapters/journal_store/edge_audit.rb +132 -0
- data/lib/hecks/hecks/adapters/journal_store/examination.rb +135 -0
- data/lib/hecks/hecks/adapters/journal_store/held_domain.rb +163 -0
- data/lib/hecks/hecks/adapters/journal_store/readings.rb +187 -0
- data/lib/hecks/hecks/adapters/journal_store.adapter +7 -0
- data/lib/hecks/hecks/adapters/journal_store.rb +64 -0
- data/lib/hecks/hecks/adapters/local_files.adapter +7 -0
- data/lib/hecks/hecks/adapters/local_files.rb +46 -0
- data/lib/hecks/hecks/adapters/pg_admin.adapter +7 -0
- data/lib/hecks/hecks/adapters/pg_admin.rb +245 -0
- data/lib/hecks/hecks/adapters/process_pool.adapter +7 -0
- data/lib/hecks/hecks/adapters/process_pool.rb +119 -0
- data/lib/hecks/hecks/adapters/rust_toolchain.adapter +7 -0
- data/lib/hecks/hecks/adapters/rust_toolchain.rb +179 -0
- data/lib/hecks/hecks/adapters/rust_workspace.rb +150 -0
- data/lib/hecks/hecks/adapters/shell.adapter +7 -0
- data/lib/hecks/hecks/adapters/shell.rb +49 -0
- data/lib/hecks/hecks/adapters/terminal.adapter +7 -0
- data/lib/hecks/hecks/adapters/terminal.rb +74 -0
- data/lib/hecks/hecks/codebase.behaviors +172 -0
- data/lib/hecks/hecks/codebase.bluebook +2453 -0
- data/lib/hecks/hecks/context_map.hecksagon +13 -0
- data/lib/hecks/hecks/custodian.behaviors +179 -0
- data/lib/hecks/hecks/custodian.bluebook +1847 -0
- data/lib/hecks/hecks/deploy.behaviors +74 -0
- data/lib/hecks/hecks/deploy.world +12 -0
- data/lib/hecks/hecks/environments/memory.world +10 -0
- data/lib/hecks/hecks/governance.world +7 -0
- data/lib/hecks/hecks/hecks.behaviors +62 -0
- data/lib/hecks/hecks/hecks.bluebook +124 -0
- data/lib/hecks/hecks/hecks.hecksagon +390 -0
- data/lib/hecks/hecks/hecks.world +42 -0
- data/lib/hecks/kernel_capabilities.rb +194 -0
- data/lib/hecks/language/bluebook/bluebook.bluebook +11 -0
- data/lib/hecks/language/bluebook/query.bluebook +7 -0
- data/lib/hecks/language/bluebook/vocabulary.bluebook +4 -4
- data/lib/hecks/language/hecksagon/domain_port.bluebook +10 -0
- data/lib/hecks/language/hecksagon/hecksagon.bluebook +20 -0
- data/lib/hecks/mcp_stdio_guard.rb +2 -2
- data/lib/hecks/persistence_legacy_fixture/regenerate.rb +161 -0
- data/lib/hecks/persistence_legacy_fixture.rb +113 -0
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +8 -5
- data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +4 -1
- data/lib/hecks/ports/persistence/plugins/era/expected_era.rb +15 -2
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +32 -15
- data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +15 -5
- data/lib/hecks/ports/persistence/plugins/era/translation/approval_file.rb +227 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +43 -24
- data/lib/hecks/ports/persistence/plugins/era/translation.rb +1 -0
- data/lib/hecks/ports/persistence/state_codec.rb +24 -5
- data/lib/hecks/projection_files.rb +285 -0
- data/lib/hecks/projections/bootstrap_table.rb +1 -1
- data/lib/hecks/projections/deploy/fargate.rb +5 -5
- data/lib/hecks/projections/deploy/lambda.rb +43 -32
- data/lib/hecks/projections/deploy/preview/preview.sh.erb +1 -1
- data/lib/hecks/projections/deploy/preview/template.rb +1 -1
- data/lib/hecks/projections/deploy/scripts/deploy-service.sh.tmpl +1 -1
- data/lib/hecks/projections/deploy/scripts/expected-era.header +1 -1
- data/lib/hecks/projections/deploy/scripts/hosting.mk.tmpl +3 -3
- data/lib/hecks/projections/deploy/scripts/smoke-after-deploy.sh.tmpl +1 -1
- data/lib/hecks/projections/deploy/shared.rb +1 -1
- data/lib/hecks/projections/diagrams.rb +3 -3
- data/lib/hecks/projections/model/deviations.rb +1 -1
- data/lib/hecks/projections/parser_table.rb +4 -4
- data/lib/hecks/projections/reference.rb +2 -3
- data/lib/hecks/projections/rust_vocabulary.rb +1 -1
- data/lib/hecks/projector/cli_projector.rb +106 -37
- data/lib/hecks/projector.rb +1 -1
- data/lib/hecks/quality_control/adapters/agent.adapter +7 -0
- data/lib/hecks/quality_control/adapters/agent.rb +58 -0
- data/lib/hecks/quality_control/adapters/angle_tools.adapter +7 -0
- data/lib/hecks/quality_control/adapters/angle_tools.rb +15 -0
- data/lib/hecks/quality_control/adapters/clearance_recorder.rb +36 -0
- data/lib/hecks/quality_control/adapters/clearance_tools.adapter +7 -0
- data/lib/hecks/quality_control/adapters/clearance_tools.rb +17 -0
- data/lib/hecks/quality_control/adapters/git_pr.adapter +7 -0
- data/lib/hecks/quality_control/adapters/git_pr.rb +226 -0
- data/lib/hecks/quality_control/adapters/github_checks.adapter +9 -0
- data/lib/hecks/quality_control/adapters/github_checks.rb +83 -0
- data/lib/hecks/quality_control/adapters/github_ci_webhook.rb +56 -0
- data/lib/hecks/quality_control/adapters/github_issues.adapter +7 -0
- data/lib/hecks/quality_control/adapters/github_issues.rb +49 -0
- data/lib/hecks/quality_control/adapters/qa_tool.rb +59 -0
- data/lib/hecks/quality_control/adapters/sweep_tools.adapter +7 -0
- data/lib/hecks/quality_control/adapters/sweep_tools.rb +51 -0
- data/lib/hecks/quality_control/adapters/target_tools.adapter +7 -0
- data/lib/hecks/quality_control/adapters/target_tools.rb +43 -0
- data/lib/hecks/quality_control/cli/child.rb +45 -0
- data/lib/hecks/quality_control/cli/qa_concurrency_racer.rb +45 -0
- data/lib/hecks/quality_control/cli/qa_discover_external_domains.rb +248 -0
- data/lib/hecks/quality_control/cli/qa_domain_novelty.rb +163 -0
- data/lib/hecks/quality_control/cli/qa_generated_domains.rb +450 -0
- data/lib/hecks/quality_control/cli/qa_log_bug.rb +195 -0
- data/lib/hecks/quality_control/cli/qa_mine_combinations.rb +220 -0
- data/lib/hecks/quality_control/cli/qa_open_pr.rb +305 -0
- data/lib/hecks/quality_control/cli/qa_postgres_migrate.rb +154 -0
- data/lib/hecks/quality_control/cli/qa_postgres_role.rb +159 -0
- data/lib/hecks/quality_control/cli/qa_pr_check.rb +232 -0
- data/lib/hecks/quality_control/cli/qa_seed_angles.rb +142 -0
- data/lib/hecks/quality_control/cli/qa_seed_targets.rb +69 -0
- data/lib/hecks/quality_control/cli/qa_sweep/all_mode.rb +280 -0
- data/lib/hecks/quality_control/cli/qa_sweep/checks.rb +329 -0
- data/lib/hecks/quality_control/cli/qa_sweep/dials.rb +92 -0
- data/lib/hecks/quality_control/cli/qa_sweep/finding_report.rb +117 -0
- data/lib/hecks/quality_control/cli/qa_sweep/parsing.rb +175 -0
- data/lib/hecks/quality_control/cli/qa_sweep/release_mode.rb +60 -0
- data/lib/hecks/quality_control/cli/qa_sweep.rb +611 -0
- data/lib/hecks/quality_control/cli/qa_tick.rb +274 -0
- data/lib/hecks/quality_control/quality_control.bluebook +2224 -0
- data/lib/hecks/quality_control/quality_control.hecksagon +13 -0
- data/lib/hecks/quality_control/quality_control.ports.hecksagon +54 -0
- data/lib/hecks/query_ir.rb +346 -0
- data/lib/hecks/query_ir_mcp.rb +198 -0
- data/lib/hecks/release/runner/ci_publisher.rb +2 -2
- data/lib/hecks/release/runner/clean_tree.rb +42 -0
- data/lib/hecks/release/runner/console.rb +6 -0
- data/lib/hecks/release/runner/gem_publisher.rb +14 -7
- data/lib/hecks/release/runner/npm_publisher.rb +6 -36
- data/lib/hecks/release/runner/preflight.rb +26 -2
- data/lib/hecks/release/runner/published.rb +8 -26
- data/lib/hecks/release/runner/tagger.rb +30 -15
- data/lib/hecks/release/runner.rb +57 -6
- data/lib/hecks/router/namespace_installer.rb +3 -3
- data/lib/hecks/runtime/adapter_lookup.rb +124 -0
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +4 -0
- data/lib/hecks/runtime/dependency_planning.rb +20 -2
- data/lib/hecks/runtime/dispatcher.rb +53 -3
- data/lib/hecks/runtime/interpreting.rb +3 -0
- data/lib/hecks/runtime/loader.rb +65 -12
- data/lib/hecks/runtime/outbox.rb +1 -1
- data/lib/hecks/runtime/policy_interpreter.rb +5 -2
- data/lib/hecks/runtime/port_operation_interpreter.rb +2 -10
- data/lib/hecks/runtime/query_interpreter.rb +77 -0
- data/lib/hecks/runtime/reaction_outcome.rb +89 -0
- data/lib/hecks/runtime/refusal_wording.rb +23 -1
- data/lib/hecks/runtime/registry/verification.rb +163 -4
- data/lib/hecks/runtime/registry.rb +57 -3
- data/lib/hecks/runtime/remote_dispatcher.rb +31 -2
- data/lib/hecks/runtime/value/coercion.rb +23 -135
- data/lib/hecks/runtime/value/field_checks.rb +172 -0
- data/lib/hecks/runtime/value.rb +4 -1
- data/lib/hecks/runtime.rb +20 -6
- data/lib/hecks/rust_build/conformance.rb +158 -0
- data/lib/hecks/rust_build/conformance_fuzz.rb +71 -0
- data/lib/hecks/rust_build/coverage.rb +218 -0
- data/lib/hecks/rust_build/native_build.rb +124 -0
- data/lib/hecks/rust_build/project_rust/root_mod.rb +120 -0
- data/lib/hecks/rust_build/project_rust.rb +332 -0
- data/lib/hecks/rust_build/wasm.rb +105 -0
- data/lib/hecks/rust_build/wasm_browser.rb +113 -0
- data/lib/hecks/rust_build.rb +144 -0
- data/lib/hecks/storehouse.rb +16 -16
- data/lib/hecks/tenancy/bluebook/tenancy.bluebook +2 -2
- data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +1 -1
- data/lib/hecks/three_zero/forms.yml +127 -0
- data/lib/hecks/three_zero.rb +12 -0
- data/lib/hecks/tools/argument_gate_matrix.rb +316 -0
- data/lib/hecks/tools/comment_style.rb +1070 -0
- data/lib/hecks/tools/corpus_report.rb +87 -0
- data/lib/hecks/tools/deploy_recipe.rb +255 -0
- data/lib/hecks/tools/deploy_recipe_lint.rb +282 -0
- data/lib/hecks/tools/drop_implicit_append_fields.rb +95 -0
- data/lib/hecks/tools/era_projection_backfill.rb +94 -0
- data/lib/hecks/tools/era_reattest.rb +146 -0
- data/lib/hecks/tools/evolve_run.rb +189 -0
- data/lib/hecks/tools/fuzz_sweep.rb +373 -0
- data/lib/hecks/tools/heki_compaction.rb +100 -0
- data/lib/hecks/tools/hoist_local_givens.rb +246 -0
- data/lib/hecks/tools/journal_compaction.rb +89 -0
- data/lib/hecks/tools/oidc_manifests.rb +70 -0
- data/lib/hecks/tools/query_ir_run.rb +81 -0
- data/lib/hecks/tools/regeneration_run.rb +166 -0
- data/lib/hecks/tools/rust_comment_style.rb +642 -0
- data/lib/hecks/tools/sequence_script.rb +62 -0
- data/lib/hecks/tools/tail_merge.rb +117 -0
- data/lib/hecks/tools/tenant_provisioning.rb +142 -0
- data/lib/hecks/tools/translation_audit.rb +208 -0
- data/lib/hecks/tools/translation_scaffold.rb +131 -0
- data/lib/hecks/tools.rb +86 -0
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks.rb +32 -15
- data/qa/settings.yml +66 -0
- data/rust/.cargo/config.toml +6 -0
- data/rust/.gitignore +2 -0
- data/rust/Cargo.lock +54 -0
- data/rust/Cargo.toml +61 -0
- data/rust/build/.cargo/config.toml +3 -0
- data/rust/build/.gitignore +1 -0
- data/rust/build/Cargo.lock +7 -0
- data/rust/build/Cargo.toml +19 -0
- data/rust/build/src/build_artifact.rs +47 -0
- data/rust/build/src/cargo_sync.rs +248 -0
- data/rust/build/src/json.rs +421 -0
- data/rust/build/src/lineage_pass.rs +285 -0
- data/rust/build/src/main.rs +51 -0
- data/rust/build/src/optional_pass.rs +150 -0
- data/rust/build/src/pipeline.rs +264 -0
- data/rust/build/src/reserved_names.rs +76 -0
- data/rust/build/src/resolve.rs +124 -0
- data/rust/build/src/root.rs +25 -0
- data/rust/build/src/sidecars.rs +73 -0
- data/rust/build/src/subprocess.rs +51 -0
- data/rust/build/src/tmp.rs +31 -0
- data/rust/codegen/.cargo/config.toml +3 -0
- data/rust/codegen/.gitignore +1 -0
- data/rust/codegen/Cargo.lock +7 -0
- data/rust/codegen/Cargo.toml +20 -0
- data/rust/codegen/src/attr.rs +32 -0
- data/rust/codegen/src/bridging.rs +481 -0
- data/rust/codegen/src/commands.rs +1226 -0
- data/rust/codegen/src/constraints.rs +109 -0
- data/rust/codegen/src/dependency_planning.rs +209 -0
- data/rust/codegen/src/domain_generator.rs +1288 -0
- data/rust/codegen/src/exemplar.rs +230 -0
- data/rust/codegen/src/expr_emitter.rs +87 -0
- data/rust/codegen/src/fielded.rs +192 -0
- data/rust/codegen/src/hecks_naming.rs +33 -0
- data/rust/codegen/src/json.rs +335 -0
- data/rust/codegen/src/json_codec.rs +697 -0
- data/rust/codegen/src/literal.rs +207 -0
- data/rust/codegen/src/main.rs +290 -0
- data/rust/codegen/src/manifest.rs +227 -0
- data/rust/codegen/src/mutations.rs +784 -0
- data/rust/codegen/src/naming.rs +286 -0
- data/rust/codegen/src/ports.rs +208 -0
- data/rust/codegen/src/prelude.rs +116 -0
- data/rust/codegen/src/queries.rs +786 -0
- data/rust/codegen/src/reactions.rs +532 -0
- data/rust/codegen/src/read_models.rs +697 -0
- data/rust/codegen/src/reference_specs.rs +40 -0
- data/rust/codegen/src/registry.rs +966 -0
- data/rust/codegen/src/reserved_names.rs +76 -0
- data/rust/codegen/src/shared.rs +93 -0
- data/rust/codegen/src/skip_reason.rs +25 -0
- data/rust/codegen/src/types.rs +362 -0
- data/rust/host/.cargo/config.toml +3 -0
- data/rust/host/.gitignore +1 -0
- data/rust/host/Cargo.lock +4297 -0
- data/rust/host/Cargo.toml +61 -0
- data/rust/host/HECKS_RELEASE +1 -0
- data/rust/host/rds-ca-bundle.pem +2736 -0
- data/rust/host/src/api.rs +1596 -0
- data/rust/host/src/approval.rs +652 -0
- data/rust/host/src/auth.rs +1287 -0
- data/rust/host/src/bin/expr_harness.rs +46 -0
- data/rust/host/src/bin/lineage_harness.rs +119 -0
- data/rust/host/src/bin/mint_harness.rs +135 -0
- data/rust/host/src/checkout.rs +265 -0
- data/rust/host/src/dispatch.rs +1443 -0
- data/rust/host/src/expr_json.rs +1117 -0
- data/rust/host/src/field_hints.rs +52 -0
- data/rust/host/src/ir.rs +733 -0
- data/rust/host/src/journal.rs +874 -0
- data/rust/host/src/lambda_client.rs +378 -0
- data/rust/host/src/log.rs +147 -0
- data/rust/host/src/main.rs +474 -0
- data/rust/host/src/mint.rs +1865 -0
- data/rust/host/src/payments/keystore.rs +330 -0
- data/rust/host/src/payments/tests.rs +1400 -0
- data/rust/host/src/payments.rs +767 -0
- data/rust/host/src/presentation.rs +698 -0
- data/rust/host/src/presentation_write.rs +1431 -0
- data/rust/host/src/query_step.rs +149 -0
- data/rust/host/src/rate_limit.rs +904 -0
- data/rust/host/src/reference_transform.rs +241 -0
- data/rust/host/src/reference_validate.rs +338 -0
- data/rust/host/src/resend.rs +296 -0
- data/rust/host/src/secrets.rs +106 -0
- data/rust/host/src/server.rs +663 -0
- data/rust/host/src/storage_shape.rs +175 -0
- data/rust/host/src/test_pg.rs +40 -0
- data/rust/host/src/ui_schema.rs +1351 -0
- data/rust/host/src/wasm_runner.rs +191 -0
- data/rust/host/src/web/newsletter/cooldown.rs +126 -0
- data/rust/host/src/web/newsletter/tests.rs +104 -0
- data/rust/host/src/web/newsletter.rs +310 -0
- data/rust/host/src/web/newsletter_send.rs +357 -0
- data/rust/host/src/web/registrations.rs +1505 -0
- data/rust/host/src/web.rs +2937 -0
- data/rust/lsp/.gitignore +1 -0
- data/rust/lsp/Cargo.lock +7 -0
- data/rust/lsp/Cargo.toml +16 -0
- data/rust/lsp/README.md +133 -0
- data/rust/lsp/src/diagnostics.rs +165 -0
- data/rust/lsp/src/json.rs +385 -0
- data/rust/lsp/src/main.rs +399 -0
- data/rust/lsp/src/outline.rs +177 -0
- data/rust/lsp/src/rpc.rs +67 -0
- data/rust/parser/.cargo/config.toml +5 -0
- data/rust/parser/.gitignore +1 -0
- data/rust/parser/Cargo.lock +7 -0
- data/rust/parser/Cargo.toml +21 -0
- data/rust/parser/src/build/closed_sets.rs +27 -0
- data/rust/parser/src/build/identity.rs +276 -0
- data/rust/parser/src/build/mod.rs +12 -0
- data/rust/parser/src/build/naming.rs +141 -0
- data/rust/parser/src/build/pattern_subset.rs +161 -0
- data/rust/parser/src/build/query_derive.rs +72 -0
- data/rust/parser/src/build/query_inference.rs +269 -0
- data/rust/parser/src/build/query_options.rs +100 -0
- data/rust/parser/src/build/read_model.rs +94 -0
- data/rust/parser/src/build/references.rs +32 -0
- data/rust/parser/src/canonical.rs +114 -0
- data/rust/parser/src/diag.rs +55 -0
- data/rust/parser/src/emit.rs +953 -0
- data/rust/parser/src/expr/ast_json.rs +147 -0
- data/rust/parser/src/expr/evaluator.rs +81 -0
- data/rust/parser/src/expr/mod.rs +84 -0
- data/rust/parser/src/expr/resolver.rs +497 -0
- data/rust/parser/src/ir.rs +387 -0
- data/rust/parser/src/keywords.rs +585 -0
- data/rust/parser/src/lex.rs +662 -0
- data/rust/parser/src/main.rs +252 -0
- data/rust/parser/src/parse/aggregate.rs +612 -0
- data/rust/parser/src/parse/chapter.rs +517 -0
- data/rust/parser/src/parse/command.rs +637 -0
- data/rust/parser/src/parse/domain_port.rs +127 -0
- data/rust/parser/src/parse/entity.rs +527 -0
- data/rust/parser/src/parse/file.rs +67 -0
- data/rust/parser/src/parse/hecksagon.rs +231 -0
- data/rust/parser/src/parse/lifecycle.rs +162 -0
- data/rust/parser/src/parse/mod.rs +1262 -0
- data/rust/parser/src/parse/policy.rs +87 -0
- data/rust/parser/src/parse/process_manager.rs +305 -0
- data/rust/parser/src/parse/query.rs +100 -0
- data/rust/parser/src/parse/read_model.rs +214 -0
- data/rust/parser/src/parse/value_object.rs +250 -0
- data/rust/parser/src/ruby_value.rs +422 -0
- data/rust/project/bridging.rb +257 -0
- data/rust/project/commands.rb +877 -0
- data/rust/project/constraints.rb +86 -0
- data/rust/project/dependency_planning.rb +178 -0
- data/rust/project/domain_generator.rb +980 -0
- data/rust/project/exemplar.rb +148 -0
- data/rust/project/expr_emitter.rb +72 -0
- data/rust/project/fielded.rb +141 -0
- data/rust/project/json_codec.rb +641 -0
- data/rust/project/mutations.rb +721 -0
- data/rust/project/naming.rb +205 -0
- data/rust/project/ports.rb +65 -0
- data/rust/project/queries.rb +493 -0
- data/rust/project/reactions.rb +317 -0
- data/rust/project/read_models.rb +690 -0
- data/rust/project/reference_specs.rb +27 -0
- data/rust/project/registry.rb +396 -0
- data/rust/project/skip_reason.rb +29 -0
- data/rust/project/types.rb +225 -0
- data/rust/project/write_if_changed.rb +83 -0
- data/rust/project.rb +30 -0
- data/rust/project_rust_pipeline.rb +383 -0
- data/rust/src/exemplar/commands.rs +279 -0
- data/rust/src/exemplar/constraints.rs +18 -0
- data/rust/src/exemplar/fielded.rs +280 -0
- data/rust/src/exemplar/json.rs +277 -0
- data/rust/src/exemplar/mod.rs +17 -0
- data/rust/src/exemplar/mutations.rs +80 -0
- data/rust/src/exemplar/queries.rs +33 -0
- data/rust/src/exemplar/reactions.rs +88 -0
- data/rust/src/exemplar/read_models.rs +48 -0
- data/rust/src/exemplar/registry.rs +166 -0
- data/rust/src/exemplar/types.rs +44 -0
- data/rust/src/kernel/attribute_shapes/composite.rs +88 -0
- data/rust/src/kernel/attribute_shapes/list.rs +18 -0
- data/rust/src/kernel/attribute_shapes/mod.rs +44 -0
- data/rust/src/kernel/attribute_shapes/optional.rs +9 -0
- data/rust/src/kernel/attribute_shapes/scalar.rs +39 -0
- data/rust/src/kernel/cli.rs +585 -0
- data/rust/src/kernel/dispatch.rs +1090 -0
- data/rust/src/kernel/expr.rs +458 -0
- data/rust/src/kernel/expression_operators/arithmetic.rs +128 -0
- data/rust/src/kernel/expression_operators/comparison.rs +48 -0
- data/rust/src/kernel/expression_operators/enumeration.rs +232 -0
- data/rust/src/kernel/expression_operators/logical.rs +13 -0
- data/rust/src/kernel/expression_operators/membership.rs +18 -0
- data/rust/src/kernel/expression_operators/mod.rs +68 -0
- data/rust/src/kernel/expression_operators/pattern_match.rs +73 -0
- data/rust/src/kernel/expression_operators/positional.rs +126 -0
- data/rust/src/kernel/expression_operators/presence.rs +101 -0
- data/rust/src/kernel/expression_operators/sign_test.rs +16 -0
- data/rust/src/kernel/expression_operators/sized.rs +35 -0
- data/rust/src/kernel/expression_operators/text.rs +79 -0
- data/rust/src/kernel/expression_operators/to_string.rs +18 -0
- data/rust/src/kernel/json.rs +779 -0
- data/rust/src/kernel/mod.rs +133 -0
- data/rust/src/kernel/named_query.rs +219 -0
- data/rust/src/kernel/orchestrate.rs +1810 -0
- data/rust/src/kernel/pattern.rs +307 -0
- data/rust/src/kernel/query_comparators.rs +206 -0
- data/rust/src/kernel/query_ordering.rs +103 -0
- data/rust/src/kernel/read_model.rs +914 -0
- data/rust/src/kernel/reference_lookup.rs +149 -0
- data/rust/src/kernel/repository.rs +468 -0
- data/rust/src/kernel/routing.rs +612 -0
- data/rust/src/kernel/vocab/aggregate_dispatch_order.rs +118 -0
- data/rust/src/kernel/vocab/entity_dispatch_order.rs +114 -0
- data/rust/src/kernel/vocab/field_hint.rs +70 -0
- data/rust/src/kernel/vocab/mod.rs +20 -0
- data/rust/src/kernel/vocab/query_comparator.rs +65 -0
- data/rust/src/kernel/vocab/refusal_template.rs +1592 -0
- data/rust/src/lib.rs +8 -0
- data/rust/src/main.rs +16 -0
- data/rust/web/.gitignore +1 -0
- data/rust/web/Cargo.lock +119 -0
- data/rust/web/Cargo.toml +18 -0
- data/rust/web/src/lib.rs +8 -0
- metadata +477 -12
- data/lib/hecks/facade/cli_door.rb +0 -96
- data/lib/hecks/facade/cli_runner.rb +0 -127
- data/lib/hecks/mcp_door_scope.rb +0 -95
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
//! Rust side of the expression differential: interprets each `{ast, instance}` case on stdin
|
|
2
|
+
//! through the same `expr_json` the mint-time invariant checker uses, and prints one result each.
|
|
3
|
+
//! `spec/rust_host_expr_json_conformance_spec.rb` diffs those against Ruby's own interpreters.
|
|
4
|
+
|
|
5
|
+
#[allow(dead_code)]
|
|
6
|
+
#[path = "../expr_json.rs"]
|
|
7
|
+
mod expr_json;
|
|
8
|
+
|
|
9
|
+
use expr_json::Value;
|
|
10
|
+
use serde_json::{json, Value as Json};
|
|
11
|
+
use std::io::Read;
|
|
12
|
+
|
|
13
|
+
// Usage: `expr_harness < {"cases": [{"ast": <ast_json>, "instance": <json>}, ...]}`.
|
|
14
|
+
// Prints `{"results": [{"ok": <value>} | {"error": <message>}, ...]}`, one per case, in order.
|
|
15
|
+
fn main() -> anyhow::Result<()> {
|
|
16
|
+
let mut input = String::new();
|
|
17
|
+
std::io::stdin().read_to_string(&mut input)?;
|
|
18
|
+
let request: Json = serde_json::from_str(&input)?;
|
|
19
|
+
let cases = request.get("cases").and_then(Json::as_array).ok_or_else(|| anyhow::anyhow!("no \"cases\" array on stdin"))?;
|
|
20
|
+
|
|
21
|
+
let results: Vec<Json> = cases
|
|
22
|
+
.iter()
|
|
23
|
+
.map(|case| {
|
|
24
|
+
let outcome = expr_json::parse(&case["ast"]).and_then(|expr| expr_json::interpret(&expr, &case["instance"]));
|
|
25
|
+
match outcome {
|
|
26
|
+
Ok(value) => json!({ "ok": to_json(&value) }),
|
|
27
|
+
Err(error) => json!({ "error": error }),
|
|
28
|
+
}
|
|
29
|
+
})
|
|
30
|
+
.collect();
|
|
31
|
+
|
|
32
|
+
println!("{}", json!({ "results": results }));
|
|
33
|
+
Ok(())
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
fn to_json(value: &Value) -> Json {
|
|
37
|
+
match value {
|
|
38
|
+
Value::Int(i) => json!(i),
|
|
39
|
+
Value::Float(f) => serde_json::Number::from_f64(*f).map_or(Json::Null, Json::Number),
|
|
40
|
+
Value::Str(s) => json!(s),
|
|
41
|
+
Value::Bool(b) => json!(b),
|
|
42
|
+
Value::Nil => Json::Null,
|
|
43
|
+
Value::Array(items) => Json::Array(items.iter().map(to_json).collect()),
|
|
44
|
+
Value::Object(object) => object.clone(),
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
//! Rust side of the differential-parity harness: generic lineage reads and writes on Postgres.
|
|
2
|
+
//! Usage: `lineage_harness <db_name> <app_role> <domain> <era>`, JSON on stdin and stdout.
|
|
3
|
+
//!
|
|
4
|
+
//! stdin is `{"operations": [...]}`, each one of:
|
|
5
|
+
//! {"op": "read_all", "storage_name": "order"}
|
|
6
|
+
//! {"op": "read_by_id", "storage_name": "order", "id": "order-1"}
|
|
7
|
+
//! {"op": "write", "aggregate": "Pizzas::Order", "id": "order-9", "state": {...}}
|
|
8
|
+
//! stdout is `{"results": [...]}`, one entry per operation in order. A per-operation Postgres
|
|
9
|
+
//! error (such as an RLS refusal) lands in that entry; only a failed connection exits nonzero.
|
|
10
|
+
|
|
11
|
+
// journal.rs has no `crate::` references, so it is shared with `bootstrap` by path, not a lib
|
|
12
|
+
// split.
|
|
13
|
+
// The flat-journal functions it also carries are unused here.
|
|
14
|
+
#[allow(dead_code)]
|
|
15
|
+
#[path = "../journal.rs"]
|
|
16
|
+
mod journal;
|
|
17
|
+
|
|
18
|
+
// journal.rs's Postgres tests connect through `crate::test_pg`, so the harness's own test build
|
|
19
|
+
// carries the same connection-string helpers.
|
|
20
|
+
#[cfg(test)]
|
|
21
|
+
#[path = "../test_pg.rs"]
|
|
22
|
+
mod test_pg;
|
|
23
|
+
|
|
24
|
+
use serde_json::{json, Value};
|
|
25
|
+
use std::io::Read;
|
|
26
|
+
use tokio_postgres::NoTls;
|
|
27
|
+
|
|
28
|
+
#[tokio::main]
|
|
29
|
+
async fn main() -> anyhow::Result<()> {
|
|
30
|
+
let args: Vec<String> = std::env::args().collect();
|
|
31
|
+
let [_, db_name, app_role, domain, era] = args.as_slice() else {
|
|
32
|
+
anyhow::bail!("usage: lineage_harness <db_name> <app_role> <domain> <era>");
|
|
33
|
+
};
|
|
34
|
+
let era: i32 = era.parse().map_err(|_| anyhow::anyhow!("era must be an integer, got {era:?}"))?;
|
|
35
|
+
|
|
36
|
+
let mut input = String::new();
|
|
37
|
+
std::io::stdin().read_to_string(&mut input)?;
|
|
38
|
+
let request: Value = serde_json::from_str(&input)?;
|
|
39
|
+
let operations = request
|
|
40
|
+
.get("operations")
|
|
41
|
+
.and_then(|v| v.as_array())
|
|
42
|
+
.ok_or_else(|| anyhow::anyhow!("stdin JSON missing \"operations\" array"))?;
|
|
43
|
+
|
|
44
|
+
// tokio_postgres ignores libpq's `PGPASSWORD`, so pass it through when the server asks for one.
|
|
45
|
+
let password = std::env::var("PGPASSWORD").ok().filter(|p| !p.is_empty()).map(|p| format!(" password={p}")).unwrap_or_default();
|
|
46
|
+
let conn_string = format!("host=localhost dbname={db_name} user={app_role}{password}");
|
|
47
|
+
let (client, connection) = tokio_postgres::connect(&conn_string, NoTls).await?;
|
|
48
|
+
tokio::spawn(async move {
|
|
49
|
+
if let Err(err) = connection.await {
|
|
50
|
+
eprintln!("postgres connection error: {err:#}");
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
let config = journal::LineageConfig { domain: domain.clone(), era: Some(era), mirrored: None };
|
|
55
|
+
let mut results = Vec::with_capacity(operations.len());
|
|
56
|
+
|
|
57
|
+
for operation in operations {
|
|
58
|
+
results.push(run_one(&client, &config, operation).await);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
println!("{}", json!({ "results": results }));
|
|
62
|
+
Ok(())
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async fn run_one(client: &tokio_postgres::Client, config: &journal::LineageConfig, operation: &Value) -> Value {
|
|
66
|
+
let op = operation.get("op").and_then(|v| v.as_str()).unwrap_or("");
|
|
67
|
+
|
|
68
|
+
let outcome = match op {
|
|
69
|
+
"read_all" => read_all(client, config, operation).await,
|
|
70
|
+
"read_by_id" => read_by_id(client, config, operation).await,
|
|
71
|
+
"write" => write(client, config, operation).await,
|
|
72
|
+
other => Err(anyhow::anyhow!("unknown op {other:?}")),
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
match outcome {
|
|
76
|
+
Ok(mut value) => {
|
|
77
|
+
value["op"] = json!(op);
|
|
78
|
+
value["ok"] = json!(true);
|
|
79
|
+
value
|
|
80
|
+
}
|
|
81
|
+
Err(err) => json!({ "op": op, "ok": false, "error": format!("{err:#}") }),
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async fn read_all(client: &tokio_postgres::Client, config: &journal::LineageConfig, operation: &Value) -> anyhow::Result<Value> {
|
|
86
|
+
let storage_name = require_str(operation, "storage_name")?;
|
|
87
|
+
let rows = journal::read_lineage_head_all(client, &config.domain, storage_name).await?;
|
|
88
|
+
Ok(json!({ "rows": rows.into_iter().map(|(id, state)| json!([id, state])).collect::<Vec<_>>() }))
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async fn read_by_id(client: &tokio_postgres::Client, config: &journal::LineageConfig, operation: &Value) -> anyhow::Result<Value> {
|
|
92
|
+
let storage_name = require_str(operation, "storage_name")?;
|
|
93
|
+
let id = require_str(operation, "id")?;
|
|
94
|
+
let state = journal::read_lineage_head_by_id(client, &config.domain, storage_name, id).await?;
|
|
95
|
+
Ok(json!({ "state": state }))
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async fn write(client: &tokio_postgres::Client, config: &journal::LineageConfig, operation: &Value) -> anyhow::Result<Value> {
|
|
99
|
+
let aggregate = require_str(operation, "aggregate")?;
|
|
100
|
+
let id = require_str(operation, "id")?;
|
|
101
|
+
let state = operation
|
|
102
|
+
.get("state")
|
|
103
|
+
.ok_or_else(|| anyhow::anyhow!("write operation missing \"state\": {operation}"))?;
|
|
104
|
+
|
|
105
|
+
journal::append_lineage_mutation(
|
|
106
|
+
client,
|
|
107
|
+
config,
|
|
108
|
+
&journal::Mutation { aggregate, id, operation: "save", state },
|
|
109
|
+
)
|
|
110
|
+
.await?;
|
|
111
|
+
Ok(json!({}))
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
fn require_str<'a>(operation: &'a Value, field: &str) -> anyhow::Result<&'a str> {
|
|
115
|
+
operation
|
|
116
|
+
.get(field)
|
|
117
|
+
.and_then(|v| v.as_str())
|
|
118
|
+
.ok_or_else(|| anyhow::anyhow!("operation missing {field:?}: {operation}"))
|
|
119
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
//! Rust-mints-it-too side of the differential-parity harness: one boot decision per invocation.
|
|
2
|
+
//! Lets the spec mint an edge with no Ruby process so the two `_head` views can be diffed.
|
|
3
|
+
|
|
4
|
+
// Modules are shared with `bootstrap` by path, not a lib split, so `crate::` in mint.rs resolves
|
|
5
|
+
// here.
|
|
6
|
+
#[allow(dead_code)]
|
|
7
|
+
#[path = "../log.rs"]
|
|
8
|
+
mod log;
|
|
9
|
+
#[allow(dead_code)]
|
|
10
|
+
#[path = "../journal.rs"]
|
|
11
|
+
mod journal;
|
|
12
|
+
#[allow(dead_code)]
|
|
13
|
+
#[path = "../storage_shape.rs"]
|
|
14
|
+
mod storage_shape;
|
|
15
|
+
#[allow(dead_code)]
|
|
16
|
+
#[path = "../reference_transform.rs"]
|
|
17
|
+
mod reference_transform;
|
|
18
|
+
#[allow(dead_code)]
|
|
19
|
+
#[path = "../expr_json.rs"]
|
|
20
|
+
mod expr_json;
|
|
21
|
+
#[allow(dead_code)]
|
|
22
|
+
#[path = "../reference_validate.rs"]
|
|
23
|
+
mod reference_validate;
|
|
24
|
+
#[allow(dead_code)]
|
|
25
|
+
#[path = "../approval.rs"]
|
|
26
|
+
mod approval;
|
|
27
|
+
#[allow(dead_code)]
|
|
28
|
+
#[path = "../mint.rs"]
|
|
29
|
+
mod mint;
|
|
30
|
+
|
|
31
|
+
// The shared modules' Postgres tests connect through `crate::test_pg`, so the harness's own test
|
|
32
|
+
// build carries the same connection-string helpers.
|
|
33
|
+
#[cfg(test)]
|
|
34
|
+
#[path = "../test_pg.rs"]
|
|
35
|
+
mod test_pg;
|
|
36
|
+
|
|
37
|
+
use serde_json::{json, Value};
|
|
38
|
+
use std::collections::HashMap;
|
|
39
|
+
use tokio_postgres::NoTls;
|
|
40
|
+
|
|
41
|
+
// Usage: `mint_harness <db_name> <owner_role> <domain> <ir_json_path>`, as the schema owner.
|
|
42
|
+
// `ir_json_path` is read like `ir::ir()` reads `HECKS_IR_PATH`, so two runs can boot two shapes.
|
|
43
|
+
// Prints `{"era": <ordinal>, "label": <label>}`; a refusal exits nonzero with the error on stderr.
|
|
44
|
+
#[tokio::main]
|
|
45
|
+
async fn main() -> anyhow::Result<()> {
|
|
46
|
+
let args: Vec<String> = std::env::args().collect();
|
|
47
|
+
let [_, db_name, owner_role, domain, ir_path] = args.as_slice() else {
|
|
48
|
+
anyhow::bail!("usage: mint_harness <db_name> <owner_role> <domain> <ir_json_path>");
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
let ir_text = std::fs::read_to_string(ir_path).map_err(|e| anyhow::anyhow!("reading {ir_path}: {e}"))?;
|
|
52
|
+
let ir: Value = serde_json::from_str(&ir_text).map_err(|e| anyhow::anyhow!("parsing {ir_path}: {e}"))?;
|
|
53
|
+
|
|
54
|
+
// A server that wants a password over TCP gets it from PGPASSWORD, as libpq would read it.
|
|
55
|
+
// Built field by field, not spliced into a connection string, so a password with a space,
|
|
56
|
+
// quote or backslash reaches the server as typed.
|
|
57
|
+
let mut config = tokio_postgres::Config::new();
|
|
58
|
+
config.host("localhost").dbname(db_name).user(owner_role);
|
|
59
|
+
if let Ok(password) = std::env::var("PGPASSWORD") {
|
|
60
|
+
config.password(password);
|
|
61
|
+
}
|
|
62
|
+
let (client, connection) = config.connect(NoTls).await?;
|
|
63
|
+
tokio::spawn(async move {
|
|
64
|
+
if let Err(err) = connection.await {
|
|
65
|
+
eprintln!("postgres connection error: {err:#}");
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
journal::ensure_schema(&client).await?;
|
|
70
|
+
mint::ensure_base(&client, domain).await?;
|
|
71
|
+
|
|
72
|
+
let my_hash = storage_shape::mint_hash(&ir);
|
|
73
|
+
let my_label = storage_shape::mint_label(&ir);
|
|
74
|
+
|
|
75
|
+
// Every fixture is fully lineage-capable, so the `lineage_capable_aggregates` filter is
|
|
76
|
+
// skipped.
|
|
77
|
+
let aggregates: Vec<mint::Aggregate> = ir
|
|
78
|
+
.get("aggregates")
|
|
79
|
+
.and_then(Value::as_array)
|
|
80
|
+
.cloned()
|
|
81
|
+
.unwrap_or_default()
|
|
82
|
+
.iter()
|
|
83
|
+
.map(|agg| {
|
|
84
|
+
let name = agg.get("name").and_then(Value::as_str).unwrap_or("").to_string();
|
|
85
|
+
let storage_name = journal::snake(&name);
|
|
86
|
+
mint::Aggregate { name, storage_name }
|
|
87
|
+
})
|
|
88
|
+
.collect();
|
|
89
|
+
|
|
90
|
+
let held = journal::held_eras(&client, domain).await?;
|
|
91
|
+
|
|
92
|
+
let era = match mint::decide_boot_action(&held, &my_label) {
|
|
93
|
+
mint::BootDecision::UseExisting { ordinal } => ordinal,
|
|
94
|
+
mint::BootDecision::HoldFirst => {
|
|
95
|
+
let source_text = ir.get("source_text").and_then(Value::as_str).unwrap_or("mint_harness fixture source");
|
|
96
|
+
mint::hold_first(&client, domain, source_text, &ir, &aggregates, None).await?;
|
|
97
|
+
1
|
|
98
|
+
}
|
|
99
|
+
mint::BootDecision::LatestUnnamed { ordinal } => {
|
|
100
|
+
anyhow::bail!("latest held era {ordinal} has no label -- mint_harness has no bluebook parser to name it with");
|
|
101
|
+
}
|
|
102
|
+
mint::BootDecision::Mint { ordinal, from_ordinal, from_label } => {
|
|
103
|
+
let mut labels: Vec<String> = held.iter().map(|h| h.label.clone().unwrap_or_default()).collect();
|
|
104
|
+
labels.push(my_label.clone());
|
|
105
|
+
let edges = mint::parse_edges(&ir, domain);
|
|
106
|
+
let chain = mint::edge_chain(&edges, &labels)
|
|
107
|
+
.map_err(|e| anyhow::anyhow!("drifted from era {from_ordinal} ({from_label}) to {my_label} with no covering edge -- {e}"))?;
|
|
108
|
+
|
|
109
|
+
// The same gate the host runs: a compute/rekey edge mints only on an approval, from the
|
|
110
|
+
// journal or committed in ir.json's `approvals`.
|
|
111
|
+
let raw_edges = ir.get("translations").and_then(Value::as_array).cloned().unwrap_or_default();
|
|
112
|
+
let committed_approvals = approval::committed(&ir);
|
|
113
|
+
for edge in &chain {
|
|
114
|
+
// An edge with no raw entry fails closed, as the host does, and never skips the gate.
|
|
115
|
+
let Some(raw_edge) = raw_edges.iter().find(|candidate| {
|
|
116
|
+
candidate.get("domain").and_then(Value::as_str) == Some(domain.as_str())
|
|
117
|
+
&& candidate.get("from").and_then(Value::as_str) == Some(edge.from.as_str())
|
|
118
|
+
&& candidate.get("to").and_then(Value::as_str) == Some(edge.to.as_str())
|
|
119
|
+
}) else {
|
|
120
|
+
anyhow::bail!("cannot boot: {domain}'s edge {} -> {} vanished between parsing and approval-checking it", edge.from, edge.to);
|
|
121
|
+
};
|
|
122
|
+
approval::check(&client, domain, raw_edge, ordinal, &committed_approvals).await?;
|
|
123
|
+
}
|
|
124
|
+
let watermarks: HashMap<i32, Option<i64>> = held.iter().map(|h| (h.ordinal, h.watermark)).collect();
|
|
125
|
+
mint::audit_before_mint(&client, domain, &ir, &aggregates, ordinal, &chain, &raw_edges, &watermarks).await?;
|
|
126
|
+
|
|
127
|
+
let held_text = ir.get("source_text").and_then(Value::as_str).unwrap_or("mint_harness fixture source");
|
|
128
|
+
mint::mint_era(&client, domain, ordinal, &my_hash, &my_label, held_text, &aggregates, &chain, None, &mint::lifecycle_defaults(&ir)).await?;
|
|
129
|
+
ordinal
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
println!("{}", json!({ "era": era, "label": my_label }));
|
|
134
|
+
Ok(())
|
|
135
|
+
}
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
//! Stripe checkout and webhook mechanics for the registration routes: HMAC
|
|
2
|
+
//! signature verification, an embedded Checkout Session, and a mock session.
|
|
3
|
+
|
|
4
|
+
use hmac::{Hmac, Mac};
|
|
5
|
+
use serde_json::Value;
|
|
6
|
+
use sha2::Sha256;
|
|
7
|
+
|
|
8
|
+
type HmacSha256 = Hmac<Sha256>;
|
|
9
|
+
|
|
10
|
+
// docs.stripe.com/webhooks#verify-events — 5 minutes, guards against a
|
|
11
|
+
// replayed old payload even against a leaked (not yet rotated) secret.
|
|
12
|
+
const TOLERANCE_SECONDS: i64 = 300;
|
|
13
|
+
|
|
14
|
+
pub struct SignatureError(pub String);
|
|
15
|
+
|
|
16
|
+
impl std::fmt::Display for SignatureError {
|
|
17
|
+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
18
|
+
write!(f, "{}", self.0)
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Signs `<timestamp>.<raw body>`, the exact bytes Stripe sent -- re-serializing
|
|
23
|
+
// the JSON would disagree on whitespace or key order and break every match.
|
|
24
|
+
// `now` is a parameter, not read internally, so a test can hold time fixed.
|
|
25
|
+
pub fn verify_signature(payload: &str, header: &str, secret: &str, now: i64) -> Result<(), SignatureError> {
|
|
26
|
+
let mut timestamp: Option<i64> = None;
|
|
27
|
+
let mut signatures: Vec<&str> = Vec::new();
|
|
28
|
+
for part in header.split(',') {
|
|
29
|
+
let Some((key, value)) = part.split_once('=') else { continue };
|
|
30
|
+
match key {
|
|
31
|
+
"t" => timestamp = value.parse().ok(),
|
|
32
|
+
"v1" => signatures.push(value),
|
|
33
|
+
_ => {}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
let Some(timestamp) = timestamp else {
|
|
37
|
+
return Err(SignatureError("Stripe-Signature header has no t= timestamp".to_string()));
|
|
38
|
+
};
|
|
39
|
+
if signatures.is_empty() {
|
|
40
|
+
return Err(SignatureError("Stripe-Signature header has no v1= signature".to_string()));
|
|
41
|
+
}
|
|
42
|
+
if (now - timestamp).abs() > TOLERANCE_SECONDS {
|
|
43
|
+
return Err(SignatureError(format!(
|
|
44
|
+
"timestamp {timestamp} is outside the {TOLERANCE_SECONDS}s tolerance (now: {now})"
|
|
45
|
+
)));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let signed_payload = format!("{timestamp}.{payload}");
|
|
49
|
+
let mut mac = HmacSha256::new_from_slice(secret.as_bytes())
|
|
50
|
+
.map_err(|e| SignatureError(format!("webhook secret unusable as an HMAC key: {e}")))?;
|
|
51
|
+
mac.update(signed_payload.as_bytes());
|
|
52
|
+
let expected_hex = hex_encode(&mac.finalize().into_bytes());
|
|
53
|
+
|
|
54
|
+
if signatures.iter().any(|sig| constant_time_eq(sig.as_bytes(), expected_hex.as_bytes())) {
|
|
55
|
+
Ok(())
|
|
56
|
+
} else {
|
|
57
|
+
Err(SignatureError("signature does not match — wrong secret, or the payload was tampered with in transit".to_string()))
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
fn hex_encode(bytes: &[u8]) -> String {
|
|
62
|
+
bytes.iter().map(|b| format!("{b:02x}")).collect()
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// A bare `==` short-circuits at the first differing byte, opening a timing
|
|
66
|
+
// side-channel on the signature comparison, so this compares every byte.
|
|
67
|
+
fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
|
|
68
|
+
if a.len() != b.len() {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
a.iter().zip(b.iter()).fold(0u8, |acc, (x, y)| acc | (x ^ y)) == 0
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Encodes success_url/cancel_url as query params, matching the Ruby local
|
|
75
|
+
// checkout adapter's own `URI.encode_www_form` output exactly.
|
|
76
|
+
pub fn mock_checkout_session(registration_id: &str, success_url: &str, cancel_url: &str, site_url: &str) -> String {
|
|
77
|
+
let mut url = reqwest::Url::parse(&format!("{site_url}/pay/{registration_id}.html"))
|
|
78
|
+
.unwrap_or_else(|_| reqwest::Url::parse("http://invalid.invalid/").unwrap());
|
|
79
|
+
url.query_pairs_mut().append_pair("success_url", success_url).append_pair("cancel_url", cancel_url);
|
|
80
|
+
url.to_string()
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Embedded, not Stripe-hosted, so Stripe returns a `client_secret` instead of
|
|
84
|
+
// a `url`. `ui_mode` must be `embedded_page`; Stripe rejects the older
|
|
85
|
+
// `embedded` value. No success_url/cancel_url: the browser stays on the site
|
|
86
|
+
// and learns of completion from Stripe.js, with the webhook settling payment.
|
|
87
|
+
|
|
88
|
+
// Pinned: `embedded_page` needs this version line, not whatever the account defaults to.
|
|
89
|
+
pub const STRIPE_API_VERSION: &str = "2026-04-22.dahlia";
|
|
90
|
+
|
|
91
|
+
pub struct StripeAuth<'a> {
|
|
92
|
+
pub api_key: &'a str,
|
|
93
|
+
pub base_url: &'a str,
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
pub struct EmbeddedSession {
|
|
97
|
+
pub session_id: String,
|
|
98
|
+
pub client_secret: String,
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// 31, not 30: Stripe requires an expiry at least 30 minutes out.
|
|
102
|
+
pub const SESSION_HOLD_SECONDS: i64 = 31 * 60;
|
|
103
|
+
|
|
104
|
+
pub fn session_expires_at(now: i64) -> i64 {
|
|
105
|
+
now + SESSION_HOLD_SECONDS
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Errors carry Stripe's message when it refuses, never a secret.
|
|
109
|
+
pub async fn create_checkout_session(
|
|
110
|
+
auth: &StripeAuth<'_>,
|
|
111
|
+
price_cents: i64,
|
|
112
|
+
product_name: &str,
|
|
113
|
+
registration_id: &str,
|
|
114
|
+
expires_at: i64,
|
|
115
|
+
) -> anyhow::Result<EmbeddedSession> {
|
|
116
|
+
let unit_amount = price_cents.to_string();
|
|
117
|
+
let expires_at = expires_at.to_string();
|
|
118
|
+
let params = [
|
|
119
|
+
("mode", "payment"),
|
|
120
|
+
("ui_mode", "embedded_page"),
|
|
121
|
+
("redirect_on_completion", "never"),
|
|
122
|
+
("expires_at", expires_at.as_str()),
|
|
123
|
+
("line_items[0][price_data][currency]", "usd"),
|
|
124
|
+
("line_items[0][price_data][unit_amount]", unit_amount.as_str()),
|
|
125
|
+
("line_items[0][price_data][product_data][name]", product_name),
|
|
126
|
+
("line_items[0][quantity]", "1"),
|
|
127
|
+
("metadata[registration_id]", registration_id),
|
|
128
|
+
];
|
|
129
|
+
|
|
130
|
+
let response = reqwest::Client::new()
|
|
131
|
+
.post(format!("{}/v1/checkout/sessions", auth.base_url))
|
|
132
|
+
.bearer_auth(auth.api_key)
|
|
133
|
+
.header("Stripe-Version", STRIPE_API_VERSION)
|
|
134
|
+
.form(¶ms)
|
|
135
|
+
.send()
|
|
136
|
+
.await?;
|
|
137
|
+
|
|
138
|
+
let status = response.status();
|
|
139
|
+
let body: Value = response.json().await?;
|
|
140
|
+
if !status.is_success() {
|
|
141
|
+
let message = body
|
|
142
|
+
.get("error")
|
|
143
|
+
.and_then(|e| e.get("message"))
|
|
144
|
+
.and_then(|v| v.as_str())
|
|
145
|
+
.unwrap_or("unknown Stripe error");
|
|
146
|
+
anyhow::bail!("Stripe checkout session creation failed ({status}): {message}");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
let text = |field: &str| body.get(field).and_then(|v| v.as_str()).map(String::from);
|
|
150
|
+
match (text("id"), text("client_secret")) {
|
|
151
|
+
(Some(session_id), Some(client_secret)) => Ok(EmbeddedSession { session_id, client_secret }),
|
|
152
|
+
// The body is left out of the message: it may carry the client secret.
|
|
153
|
+
_ => anyhow::bail!("Stripe's response carried no \"id\" and \"client_secret\" for the embedded session"),
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
#[cfg(test)]
|
|
158
|
+
mod tests {
|
|
159
|
+
use super::*;
|
|
160
|
+
|
|
161
|
+
fn sign(secret: &str, timestamp: i64, payload: &str) -> String {
|
|
162
|
+
let signed_payload = format!("{timestamp}.{payload}");
|
|
163
|
+
let mut mac = HmacSha256::new_from_slice(secret.as_bytes()).unwrap();
|
|
164
|
+
mac.update(signed_payload.as_bytes());
|
|
165
|
+
hex_encode(&mac.finalize().into_bytes())
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
#[test]
|
|
169
|
+
fn mock_checkout_session_matches_local_checkouts_own_pay_page_shape() {
|
|
170
|
+
// The Ruby local checkout adapter's own real output, same three parts: this
|
|
171
|
+
// site's own /pay/<registration_id>.html page, with success_url
|
|
172
|
+
// and cancel_url riding along as encoded query params.
|
|
173
|
+
let url = mock_checkout_session(
|
|
174
|
+
"REG-1",
|
|
175
|
+
"https://example.com/yoga.html?registered=1",
|
|
176
|
+
"https://example.com/yoga.html?registered=0",
|
|
177
|
+
"https://example.com",
|
|
178
|
+
);
|
|
179
|
+
assert_eq!(
|
|
180
|
+
url,
|
|
181
|
+
"https://example.com/pay/REG-1.html?success_url=https%3A%2F%2Fexample.com%2Fyoga.html%3Fregistered%3D1&cancel_url=https%3A%2F%2Fexample.com%2Fyoga.html%3Fregistered%3D0"
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
#[test]
|
|
186
|
+
fn mock_checkout_session_carries_the_registration_id_in_its_own_path_not_a_query_param() {
|
|
187
|
+
let url = mock_checkout_session("REG-2", "https://example.com/yoga.html", "https://example.com/yoga.html", "https://example.com");
|
|
188
|
+
assert!(url.starts_with("https://example.com/pay/REG-2.html?"));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
#[test]
|
|
192
|
+
fn a_correctly_signed_payload_verifies() {
|
|
193
|
+
let payload = r#"{"type":"checkout.session.completed"}"#;
|
|
194
|
+
let secret = "whsec_test";
|
|
195
|
+
let now = 1_700_000_000;
|
|
196
|
+
let header = format!("t={now},v1={}", sign(secret, now, payload));
|
|
197
|
+
|
|
198
|
+
assert!(verify_signature(payload, &header, secret, now).is_ok());
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
#[test]
|
|
202
|
+
fn a_payload_tampered_with_after_signing_is_rejected() {
|
|
203
|
+
let secret = "whsec_test";
|
|
204
|
+
let now = 1_700_000_000;
|
|
205
|
+
let header = format!("t={now},v1={}", sign(secret, now, r#"{"type":"checkout.session.completed"}"#));
|
|
206
|
+
|
|
207
|
+
// Same header, different body — exactly what an attacker
|
|
208
|
+
// intercepting and rewriting the request in transit would send.
|
|
209
|
+
let tampered = r#"{"type":"checkout.session.expired"}"#;
|
|
210
|
+
assert!(verify_signature(tampered, &header, secret, now).is_err());
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
#[test]
|
|
214
|
+
fn the_wrong_secret_is_rejected() {
|
|
215
|
+
let payload = r#"{"type":"checkout.session.completed"}"#;
|
|
216
|
+
let now = 1_700_000_000;
|
|
217
|
+
let header = format!("t={now},v1={}", sign("whsec_real", now, payload));
|
|
218
|
+
|
|
219
|
+
assert!(verify_signature(payload, &header, "whsec_wrong", now).is_err());
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
#[test]
|
|
223
|
+
fn a_timestamp_outside_the_tolerance_window_is_rejected_even_with_a_correct_signature() {
|
|
224
|
+
let payload = r#"{"type":"checkout.session.completed"}"#;
|
|
225
|
+
let secret = "whsec_test";
|
|
226
|
+
let signed_at = 1_700_000_000;
|
|
227
|
+
let header = format!("t={signed_at},v1={}", sign(secret, signed_at, payload));
|
|
228
|
+
|
|
229
|
+
// A replayed webhook -- the signature is genuinely correct for
|
|
230
|
+
// its own timestamp, which is exactly why the tolerance check has
|
|
231
|
+
// to be a separate, independent gate rather than folded into
|
|
232
|
+
// "does the signature verify at all."
|
|
233
|
+
let much_later = signed_at + TOLERANCE_SECONDS + 1;
|
|
234
|
+
assert!(verify_signature(payload, &header, secret, much_later).is_err());
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
#[test]
|
|
238
|
+
fn a_second_v1_during_a_secret_rotation_window_verifies_against_either() {
|
|
239
|
+
let payload = r#"{"type":"checkout.session.completed"}"#;
|
|
240
|
+
let old_secret = "whsec_old";
|
|
241
|
+
let new_secret = "whsec_new";
|
|
242
|
+
let now = 1_700_000_000;
|
|
243
|
+
let header = format!(
|
|
244
|
+
"t={now},v1={},v1={}",
|
|
245
|
+
sign(old_secret, now, payload),
|
|
246
|
+
sign(new_secret, now, payload)
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
assert!(verify_signature(payload, &header, old_secret, now).is_ok());
|
|
250
|
+
assert!(verify_signature(payload, &header, new_secret, now).is_ok());
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
#[test]
|
|
254
|
+
fn a_header_with_no_v1_at_all_is_rejected() {
|
|
255
|
+
let now = 1_700_000_000;
|
|
256
|
+
let header = format!("t={now}");
|
|
257
|
+
assert!(verify_signature("{}", &header, "whsec_test", now).is_err());
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
#[test]
|
|
261
|
+
fn a_header_with_no_timestamp_at_all_is_rejected() {
|
|
262
|
+
let header = "v1=deadbeef".to_string();
|
|
263
|
+
assert!(verify_signature("{}", &header, "whsec_test", 1_700_000_000).is_err());
|
|
264
|
+
}
|
|
265
|
+
}
|