hecks 5.0.0 → 7.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/bluebook/pizzas.bluebook +1 -0
- data/exe/hecks +31 -12
- data/lib/hecks/adapters/driven/claude_code.rb +3 -3
- data/lib/hecks/adapters/driven/d1.rb +2 -1
- data/lib/hecks/adapters/driven/folder.rb +44 -28
- data/lib/hecks/adapters/driven/folder_overlay.rb +66 -0
- data/lib/hecks/adapters/driven/google_authentication.adapter +3 -0
- data/lib/hecks/adapters/driven/google_authentication.rb +99 -0
- data/lib/hecks/adapters/driven/governance_authorization.rb +32 -36
- data/lib/hecks/adapters/driven/heki/journal.rb +45 -0
- data/lib/hecks/adapters/driven/heki.rb +1 -1
- data/lib/hecks/adapters/driven/identity_registry.rb +1 -1
- data/lib/hecks/adapters/driven/in_memory_ordering.rb +3 -2
- data/lib/hecks/adapters/driven/in_process_key_vault.rb +1 -1
- data/lib/hecks/adapters/driven/key_keeper.rb +42 -0
- data/lib/hecks/adapters/driven/lambda/client.rb +8 -2
- data/lib/hecks/adapters/driven/lambda/remote_dispatcher.rb +185 -0
- data/lib/hecks/adapters/driven/lambda.rb +14 -2
- data/lib/hecks/adapters/driven/memory/dispatch_claims.rb +25 -0
- data/lib/hecks/adapters/driven/memory/outbox_rows.rb +2 -2
- data/lib/hecks/adapters/driven/memory.rb +15 -4
- data/lib/hecks/adapters/driven/postgres/codec.rb +1 -1
- data/lib/hecks/adapters/driven/postgres/journal.rb +46 -0
- data/lib/hecks/adapters/driven/postgres/name_guard.rb +25 -0
- data/lib/hecks/adapters/driven/postgres/outbox.rb +33 -7
- data/lib/hecks/adapters/driven/postgres/projection.rb +43 -0
- data/lib/hecks/adapters/driven/postgres/repository.rb +2 -2
- data/lib/hecks/adapters/driven/postgres/schema_builder.rb +2 -1
- data/lib/hecks/adapters/driven/postgres/set_dialect.rb +44 -0
- data/lib/hecks/adapters/driven/postgres/shared_connection.rb +1 -1
- data/lib/hecks/adapters/driven/postgres.adapter +8 -0
- data/lib/hecks/adapters/driven/postgres.rb +11 -3
- data/lib/hecks/adapters/driven/postgres_dump/connection.rb +57 -0
- data/lib/hecks/adapters/driven/postgres_dump.rb +117 -0
- data/lib/hecks/adapters/driven/postgres_era/boot.rb +58 -0
- data/lib/hecks/{ports/persistence/plugins/era → adapters/driven}/postgres_era/class_methods.rb +28 -8
- data/lib/hecks/{ports/persistence/plugins/era → adapters/driven}/postgres_era/dialect.rb +11 -6
- data/lib/hecks/adapters/driven/postgres_era/field_cache_provisioner.rb +39 -0
- data/lib/hecks/{ports/persistence/plugins/era → adapters/driven}/postgres_era/journal.rb +52 -13
- data/lib/hecks/{ports/persistence/plugins/era → adapters/driven}/postgres_era/lineage/chain_sql.rb +37 -53
- data/lib/hecks/adapters/driven/postgres_era/lineage/direct_sql.rb +73 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage/direct_verified.json +10 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage/direct_verified.rb +65 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage/domain_rename.rb +143 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage/domain_rename_state.rb +108 -0
- data/lib/hecks/{ports/persistence/plugins/era → adapters/driven}/postgres_era/lineage/era_integrity.rb +13 -8
- data/lib/hecks/adapters/driven/postgres_era/lineage/era_store.rb +125 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage/layered_sql.rb +64 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage/ledger_reset.rb +150 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage/mint_provisioning.rb +85 -0
- data/lib/hecks/{ports/persistence/plugins/era → adapters/driven}/postgres_era/lineage/mint_transaction.rb +13 -9
- data/lib/hecks/{ports/persistence/plugins/era → adapters/driven}/postgres_era/lineage/provisioning.rb +11 -5
- data/lib/hecks/adapters/driven/postgres_era/lineage/split_sql.rb +76 -0
- data/lib/hecks/{ports/persistence/plugins/era → adapters/driven}/postgres_era/lineage/sql_templates.rb +15 -2
- data/lib/hecks/{ports/persistence/plugins/era → adapters/driven}/postgres_era/lineage/tail_merge.rb +1 -1
- data/lib/hecks/{ports/persistence/plugins/era → adapters/driven}/postgres_era/lineage.rb +11 -3
- data/lib/hecks/adapters/driven/postgres_era/lineage_manager/context.rb +68 -0
- data/lib/hecks/{ports/persistence/plugins/era → adapters/driven}/postgres_era/lineage_manager/coverage_check.rb +16 -12
- data/lib/hecks/adapters/driven/postgres_era/lineage_manager/disposable_ledger.rb +119 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage_manager/era_resolution.rb +219 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage_manager/era_resolver.rb +114 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage_manager/held_era.rb +68 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage_manager/ledger_change.rb +63 -0
- data/lib/hecks/{ports/persistence/plugins/era → adapters/driven}/postgres_era/lineage_manager/merge_coordinator.rb +3 -3
- data/lib/hecks/adapters/driven/postgres_era/lineage_manager/mint_decision.rb +163 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage_manager/mint_request.rb +24 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage_manager/minter.rb +143 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage_manager/refusals.rb +76 -0
- data/lib/hecks/{ports/persistence/plugins/era → adapters/driven}/postgres_era/lineage_manager.rb +46 -12
- data/lib/hecks/{ports/persistence/plugins/era → adapters/driven}/postgres_era/query_phases.rb +8 -2
- data/lib/hecks/adapters/driven/postgres_era/shared_tables.rb +32 -0
- data/lib/hecks/adapters/driven/postgres_era.adapter +19 -1
- data/lib/hecks/{ports/persistence/plugins/era → adapters/driven}/postgres_era.rb +39 -23
- data/lib/hecks/adapters/driven/prism.rb +4 -4
- data/lib/hecks/adapters/driven/projection/read_model_walk.rb +126 -0
- data/lib/hecks/adapters/driven/projection/row_decoding.rb +57 -0
- data/lib/hecks/adapters/driven/set_transform/assignments.rb +101 -0
- data/lib/hecks/adapters/driven/set_transform/expressions.rb +138 -0
- data/lib/hecks/adapters/driven/set_transform.rb +81 -0
- data/lib/hecks/adapters/driven/sql_keyset.rb +61 -0
- data/lib/hecks/adapters/driven/sql_query_builder.rb +29 -9
- data/lib/hecks/adapters/driven/sqlite/codec.rb +1 -1
- data/lib/hecks/adapters/driven/sqlite/ddl.rb +7 -0
- data/lib/hecks/adapters/driven/sqlite/dialect.rb +2 -1
- data/lib/hecks/adapters/driven/sqlite/dispatch_claims.rb +28 -0
- data/lib/hecks/adapters/driven/sqlite/journal.rb +46 -0
- data/lib/hecks/adapters/driven/sqlite/name_guard.rb +25 -0
- data/lib/hecks/adapters/driven/sqlite/outbox.rb +5 -4
- data/lib/hecks/adapters/driven/sqlite/projection.rb +9 -135
- data/lib/hecks/adapters/driven/sqlite/repository.rb +2 -2
- data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +36 -4
- data/lib/hecks/adapters/driven/sqlite/set_dialect.rb +39 -0
- data/lib/hecks/adapters/driven/sqlite.adapter +2 -0
- data/lib/hecks/adapters/driven/sqlite.rb +20 -3
- data/lib/hecks/adapters/driven/tenant_provisioner.rb +1 -1
- data/lib/hecks/adapters/driven/worktree_overlay.rb +117 -0
- data/lib/hecks/adapters/driven.rb +8 -3
- data/lib/hecks/adapters/driving/cli/shorthand.rb +5 -1
- data/lib/hecks/adapters/driving/cli.rb +2 -2
- data/lib/hecks/adapters/driving/cli_runner/answers.rb +96 -34
- data/lib/hecks/adapters/driving/cli_runner/bang_rule.rb +35 -0
- data/lib/hecks/adapters/driving/cli_runner/completion_words.rb +36 -0
- data/lib/hecks/adapters/driving/cli_runner/json_words.rb +55 -0
- data/lib/hecks/adapters/driving/cli_runner/name_index.rb +63 -0
- data/lib/hecks/adapters/driving/cli_runner/read_context.rb +32 -0
- data/lib/hecks/adapters/driving/cli_runner/renamed_names.rb +49 -0
- data/lib/hecks/adapters/driving/cli_runner/resolution.rb +66 -39
- data/lib/hecks/adapters/driving/cli_runner/resume_words.rb +63 -0
- data/lib/hecks/adapters/driving/cli_runner/run_reference.rb +66 -0
- data/lib/hecks/adapters/driving/cli_runner/run_replay.rb +118 -0
- data/lib/hecks/adapters/driving/cli_runner/search_words.rb +35 -0
- data/lib/hecks/adapters/driving/cli_runner/settling.rb +14 -9
- data/lib/hecks/adapters/driving/cli_runner/streaming.rb +12 -11
- data/lib/hecks/adapters/driving/cli_runner/suggestions.rb +16 -8
- data/lib/hecks/adapters/driving/cli_runner/tour_words.rb +77 -0
- data/lib/hecks/adapters/driving/cli_runner/verb_names.rb +81 -0
- data/lib/hecks/adapters/driving/cli_runner/verbosity.rb +75 -0
- data/lib/hecks/adapters/driving/cli_runner.rb +110 -9
- data/lib/hecks/adapters/driving/context_line.rb +85 -0
- data/lib/hecks/adapters/driving/exit_status.rb +31 -0
- data/lib/hecks/adapters/driving/github_webhook.rb +2 -1
- data/lib/hecks/adapters/driving/handle/verb_methods.rb +46 -0
- data/lib/hecks/adapters/driving/handle.rb +9 -17
- data/lib/hecks/adapters/driving/json.rb +11 -8
- data/lib/hecks/adapters/driving/launcher_options.rb +41 -34
- data/lib/hecks/adapters/driving/mcp/progress_sink.rb +45 -0
- data/lib/hecks/adapters/driving/mcp/tool_calls.rb +7 -5
- data/lib/hecks/adapters/driving/mcp.rb +25 -5
- data/lib/hecks/adapters/driving/mcp_scope/argument_policy.rb +40 -7
- data/lib/hecks/adapters/driving/mcp_scope/env_problems.rb +3 -1
- data/lib/hecks/adapters/driving/mcp_scope.rb +11 -9
- data/lib/hecks/adapters/driving/newcomer.rb +6 -5
- data/lib/hecks/adapters/driving/ruby/aggregate_module/hooks.rb +154 -0
- data/lib/hecks/adapters/driving/ruby/aggregate_module.rb +73 -101
- data/lib/hecks/adapters/driving/ruby/bulk_verbs.rb +127 -0
- data/lib/hecks/adapters/driving/ruby/{chapter.rb → declaration.rb} +27 -27
- data/lib/hecks/adapters/driving/ruby/query_rows.rb +27 -0
- data/lib/hecks/adapters/driving/ruby.rb +25 -24
- data/lib/hecks/adapters/driving/usage_cache.rb +14 -3
- data/lib/hecks/adapters/driving/usage_log.rb +146 -0
- data/lib/hecks/adapters/driving/write_guard.rb +55 -0
- data/lib/hecks/adapters/driving.rb +0 -1
- data/lib/hecks/assembly.rb +101 -0
- data/lib/hecks/behaviors/dsl.rb +18 -4
- data/lib/hecks/behaviors/expectations/boot.rb +7 -7
- data/lib/hecks/behaviors/expectations/checks.rb +1 -1
- data/lib/hecks/behaviors/expectations/dispatching.rb +10 -9
- data/lib/hecks/behaviors/expectations/qualify.rb +9 -9
- data/lib/hecks/behaviors/expectations.rb +24 -10
- data/lib/hecks/behaviors/ir.rb +4 -2
- data/lib/hecks/behaviors/rspec.rb +1 -1
- data/lib/hecks/behaviors/runner.rb +31 -4
- data/lib/hecks/bench/environment.rb +6 -2
- data/lib/hecks/bench/postgres_probe.rb +3 -1
- data/lib/hecks/bench/report.rb +4 -1
- data/lib/hecks/bench/suite.rb +9 -3
- data/lib/hecks/bluebook/acknowledgements.rb +81 -0
- data/lib/hecks/bluebook/aggregate.rb +29 -17
- data/lib/hecks/bluebook/ask_resolution.rb +17 -17
- data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +8 -5
- data/lib/hecks/bluebook/assembly/contracts/sagas.rb +2 -0
- data/lib/hecks/bluebook/assembly/contracts/structure.rb +24 -15
- data/lib/hecks/bluebook/assembly/marks.rb +10 -1
- data/lib/hecks/bluebook/assembly/scalars.rb +8 -2
- data/lib/hecks/bluebook/assembly/specializer.rb +2 -2
- data/lib/hecks/bluebook/assembly.rb +10 -10
- data/lib/hecks/bluebook/behaviour/aggregate.rb +29 -4
- data/lib/hecks/bluebook/behaviour/attribute.rb +12 -0
- data/lib/hecks/bluebook/behaviour/command.rb +1 -1
- data/lib/hecks/bluebook/behaviour/{chapter.rb → declaration.rb} +17 -17
- data/lib/hecks/bluebook/behaviour/domain_port.rb +1 -1
- data/lib/hecks/bluebook/behaviour/entity.rb +1 -0
- data/lib/hecks/bluebook/behaviour/hecksagon.rb +8 -1
- data/lib/hecks/bluebook/behaviour/process_manager.rb +4 -1
- data/lib/hecks/bluebook/behaviour/query.rb +5 -0
- data/lib/hecks/bluebook/behaviour/read_model.rb +4 -1
- data/lib/hecks/bluebook/behaviour/traits.rb +13 -0
- data/lib/hecks/bluebook/capabilities/attribute_values.rb +17 -16
- data/lib/hecks/bluebook/capabilities/package_declared.rb +65 -0
- data/lib/hecks/bluebook/capabilities.rb +32 -132
- data/lib/hecks/bluebook/command.rb +33 -7
- data/lib/hecks/bluebook/context_map.rb +104 -0
- data/lib/hecks/bluebook/{chapter.rb → declaration.rb} +45 -17
- data/lib/hecks/bluebook/domain_port.rb +7 -3
- data/lib/hecks/bluebook/dsl/adapter_builder.rb +28 -4
- data/lib/hecks/bluebook/dsl/aggregate_builder/draining.rb +2 -2
- data/lib/hecks/bluebook/dsl/aggregate_builder/one_of_reference.rb +54 -0
- data/lib/hecks/bluebook/dsl/aggregate_builder/query_field_sealing.rb +11 -2
- data/lib/hecks/bluebook/dsl/aggregate_builder/query_sealing.rb +15 -5
- data/lib/hecks/bluebook/dsl/aggregate_builder/relationships.rb +21 -5
- data/lib/hecks/bluebook/dsl/aggregate_builder/renaming.rb +25 -0
- data/lib/hecks/bluebook/dsl/aggregate_builder/rules.rb +22 -22
- data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +2 -2
- data/lib/hecks/bluebook/dsl/aggregate_builder.rb +30 -22
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +0 -1
- data/lib/hecks/bluebook/dsl/audience_word.rb +40 -0
- data/lib/hecks/bluebook/dsl/bluebook_builder/metadata.rb +56 -0
- data/lib/hecks/bluebook/dsl/bluebook_builder/modules.rb +80 -0
- data/lib/hecks/bluebook/dsl/bluebook_builder/pending_givens.rb +22 -22
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation/computed_sources.rb +27 -21
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation/correlation_keys.rb +54 -5
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation/event_shapes.rb +5 -4
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation/fan_out_rows.rb +3 -3
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation/hop_tails.rb +13 -5
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation/projected_fields.rb +4 -4
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation/provisions.rb +38 -84
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation/query_hops.rb +5 -5
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation/references.rb +6 -6
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation/with_projections.rb +6 -6
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +4 -3
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +75 -92
- data/lib/hecks/bluebook/dsl/bootstrap_table.rb +10 -0
- data/lib/hecks/bluebook/dsl/command_builder/folded_sets.rb +48 -0
- data/lib/hecks/bluebook/dsl/command_builder.rb +39 -16
- data/lib/hecks/bluebook/dsl/context_map_builder.rb +73 -0
- data/lib/hecks/bluebook/dsl/entity_builder/rules.rb +17 -17
- data/lib/hecks/bluebook/dsl/entity_builder/scope.rb +3 -3
- data/lib/hecks/bluebook/dsl/entity_builder/sealing.rb +2 -2
- data/lib/hecks/bluebook/dsl/entity_builder.rb +3 -5
- data/lib/hecks/bluebook/dsl/generic_dispatch.rb +6 -4
- data/lib/hecks/bluebook/dsl/hecksagon_builder/{chapter_scope.rb → bluebook_scope.rb} +14 -14
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +33 -27
- data/lib/hecks/bluebook/dsl/identity_declaration.rb +6 -2
- data/lib/hecks/bluebook/dsl/kinds.rb +53 -0
- data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +4 -1
- data/lib/hecks/bluebook/dsl/port_operation_builder.rb +9 -2
- data/lib/hecks/bluebook/dsl/process_manager_builder.rb +25 -15
- data/lib/hecks/bluebook/dsl/query_builder.rb +14 -8
- data/lib/hecks/bluebook/dsl/read_model_builder/sealing.rb +6 -11
- data/lib/hecks/bluebook/dsl/read_model_builder.rb +1 -2
- data/lib/hecks/bluebook/dsl/translation_builder.rb +16 -1
- data/lib/hecks/bluebook/dsl/word_gate.rb +3 -2
- data/lib/hecks/bluebook/dsl/world_builder.rb +2 -2
- data/lib/hecks/bluebook/dsl.rb +4 -2
- data/lib/hecks/bluebook/expression/ast_json/emitters.rb +4 -0
- data/lib/hecks/bluebook/expression/ast_json.rb +5 -3
- data/lib/hecks/bluebook/expression/ast_reader/blocks.rb +41 -0
- data/lib/hecks/bluebook/expression/ast_reader.rb +2 -10
- data/lib/hecks/bluebook/expression/canonical_form.rb +9 -4
- data/lib/hecks/bluebook/expression/evaluator/parser.rb +4 -1
- data/lib/hecks/bluebook/expression/evaluator.rb +13 -9
- data/lib/hecks/bluebook/expression/projection.json +6 -0
- data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +50 -2
- data/lib/hecks/bluebook/expression/resolver/lookups.rb +2 -1
- data/lib/hecks/bluebook/expression/resolver/operations.rb +1 -0
- data/lib/hecks/bluebook/expression/resolver/parsing.rb +21 -15
- data/lib/hecks/bluebook/expression/resolver/scanning.rb +4 -1
- data/lib/hecks/bluebook/expression.rb +1 -1
- data/lib/hecks/bluebook/hecksagon.rb +48 -23
- data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +4 -1
- data/lib/hecks/bluebook/meta_validator/judge/cells.rb +1 -1
- data/lib/hecks/bluebook/meta_validator/judge/identity.rb +11 -5
- data/lib/hecks/bluebook/meta_validator/modes.rb +27 -27
- data/lib/hecks/bluebook/meta_validator/plan/category.rb +10 -2
- data/lib/hecks/bluebook/meta_validator/plan/verbs.rb +9 -0
- data/lib/hecks/bluebook/meta_validator/plan.rb +7 -6
- data/lib/hecks/bluebook/meta_validator/readings/fields.rb +3 -3
- data/lib/hecks/bluebook/meta_validator/reconstruction/heads.rb +14 -4
- data/lib/hecks/bluebook/meta_validator/reconstruction/reading.rb +8 -1
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +24 -19
- data/lib/hecks/bluebook/meta_validator/shapes.rb +9 -1
- data/lib/hecks/bluebook/meta_validator/syntax_boot/disk_cache.rb +19 -16
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +21 -15
- data/lib/hecks/bluebook/meta_validator/verdict_cache/prebuilt.rb +4 -1
- data/lib/hecks/bluebook/meta_validator/verdict_cache.rb +11 -10
- data/lib/hecks/bluebook/meta_validator.rb +25 -22
- data/lib/hecks/bluebook/model_check/aggregate_checks.rb +95 -0
- data/lib/hecks/bluebook/model_check/ask_checks.rb +31 -5
- data/lib/hecks/bluebook/model_check/client_profile.rb +8 -42
- data/lib/hecks/bluebook/model_check/context_checks.rb +97 -0
- data/lib/hecks/bluebook/model_check/context_map_checks.rb +158 -0
- data/lib/hecks/bluebook/model_check/lifecycle_checks.rb +27 -5
- data/lib/hecks/bluebook/model_check/one_of_checks.rb +106 -0
- data/lib/hecks/bluebook/model_check/orphan_checks.rb +129 -0
- data/lib/hecks/bluebook/model_check/reaction_chains.rb +114 -0
- data/lib/hecks/bluebook/model_check/reaction_cycles.rb +53 -0
- data/lib/hecks/bluebook/model_check/reaction_depth_checks.rb +58 -0
- data/lib/hecks/bluebook/model_check/reaction_walk.rb +62 -0
- data/lib/hecks/bluebook/model_check/redundant_relationships.rb +38 -0
- data/lib/hecks/bluebook/model_check/reference_checks.rb +11 -1
- data/lib/hecks/bluebook/model_check/saga_checks.rb +8 -3
- data/lib/hecks/bluebook/model_check/verbs.rb +4 -4
- data/lib/hecks/bluebook/model_check.rb +33 -35
- data/lib/hecks/bluebook/named_pattern.rb +12 -1
- data/lib/hecks/bluebook/one_of_union.rb +35 -0
- data/lib/hecks/bluebook/pattern_subset.rb +12 -3
- data/lib/hecks/bluebook/process_manager.rb +20 -3
- data/lib/hecks/bluebook/project_loader.rb +3 -2
- data/lib/hecks/bluebook/project_register.rb +21 -5
- data/lib/hecks/bluebook/query.rb +4 -1
- data/lib/hecks/bluebook/read_model.rb +5 -5
- data/lib/hecks/bluebook/reference.rb +3 -3
- data/lib/hecks/bluebook/smoke_test.rb +15 -13
- data/lib/hecks/bluebook/synthesizer.rb +48 -13
- data/lib/hecks/bluebook/translation.rb +5 -3
- data/lib/hecks/bluebook/translation_expression/parser.rb +14 -5
- data/lib/hecks/bluebook/translation_plan.rb +9 -2
- data/lib/hecks/bluebook.rb +6 -3
- data/lib/hecks/bulk/bluebook/adapters/row_sweeper.adapter +7 -0
- data/lib/hecks/bulk/bluebook/bulk.behaviors +18 -0
- data/lib/hecks/bulk/bluebook/bulk.bluebook +126 -0
- data/lib/hecks/bulk/bluebook/bulk.hecksagon +11 -0
- data/lib/hecks/bulk/bluebook/bulk.ports.hecksagon +13 -0
- data/lib/hecks/bulk/plan.rb +60 -0
- data/lib/hecks/bulk/row_sweeper.rb +34 -0
- data/lib/hecks/bulk/set_arguments.rb +68 -0
- data/lib/hecks/bulk/set_eligibility.rb +50 -0
- data/lib/hecks/bulk/set_planner.rb +100 -0
- data/lib/hecks/bulk/set_sweep.rb +78 -0
- data/lib/hecks/bulk/sweep.rb +67 -0
- data/lib/hecks/ci_pipeline/adapters/ci_cache.adapter +7 -0
- data/lib/hecks/ci_pipeline/adapters/ci_cache.rb +39 -0
- data/lib/hecks/ci_pipeline/adapters/ci_runner.adapter +7 -0
- data/lib/hecks/ci_pipeline/adapters/ci_runner.rb +55 -0
- data/lib/hecks/ci_pipeline/ci_pipeline.behaviors +322 -0
- data/lib/hecks/ci_pipeline/ci_pipeline.bluebook +938 -0
- data/lib/hecks/ci_pipeline/ci_pipeline.hecksagon +12 -0
- data/lib/hecks/ci_pipeline/ci_pipeline.ports.hecksagon +16 -0
- data/lib/hecks/ci_speed/adapters/ci_reader.adapter +7 -0
- data/lib/hecks/ci_speed/adapters/ci_reader.rb +141 -0
- data/lib/hecks/ci_speed/adapters/local_runner/answer.rb +41 -0
- data/lib/hecks/ci_speed/adapters/local_runner/catalog.rb +89 -0
- data/lib/hecks/ci_speed/adapters/local_runner/checkouts.rb +89 -0
- data/lib/hecks/ci_speed/adapters/local_runner/launcher.rb +69 -0
- data/lib/hecks/ci_speed/adapters/local_runner/load_average.rb +38 -0
- data/lib/hecks/ci_speed/adapters/local_runner/refs.rb +39 -0
- data/lib/hecks/ci_speed/adapters/local_runner/refused.rb +11 -0
- data/lib/hecks/ci_speed/adapters/local_runner/verdict.rb +71 -0
- data/lib/hecks/ci_speed/adapters/local_runner.adapter +7 -0
- data/lib/hecks/ci_speed/adapters/local_runner.rb +168 -0
- data/lib/hecks/ci_speed/adapters/replayed_runner.rb +39 -0
- data/lib/hecks/ci_speed/ci_speed.behaviors +341 -0
- data/lib/hecks/ci_speed/ci_speed.bluebook +1073 -0
- data/lib/hecks/ci_speed/ci_speed.hecksagon +11 -0
- data/lib/hecks/ci_speed/ci_speed.ports.hecksagon +21 -0
- data/lib/hecks/ci_workflows/ci_workflows.behaviors +13 -0
- data/lib/hecks/ci_workflows/ci_workflows.bluebook +173 -0
- data/lib/hecks/ci_workflows/ci_workflows.hecksagon +15 -0
- data/lib/hecks/codemod.rb +3 -3
- data/lib/hecks/coder/coder.bluebook +385 -0
- data/lib/hecks/coder/coder.hecksagon +12 -0
- data/lib/hecks/command_names/check.rb +39 -0
- data/lib/hecks/command_names/extractor.rb +67 -0
- data/lib/hecks/command_names/index.rb +40 -0
- data/lib/hecks/command_names/resolution.rb +81 -0
- data/lib/hecks/command_names/scan.rb +54 -0
- data/lib/hecks/command_names/sources.rb +73 -0
- data/lib/hecks/command_names.rb +26 -0
- data/lib/hecks/composer/adapters/claude_composer.rb +3 -2
- data/lib/hecks/composer/agent.rb +1 -1
- data/lib/hecks/composer/assistant.rb +2 -2
- data/lib/hecks/composer/behavior_run.rb +7 -1
- data/lib/hecks/composer/bluebook/composer.bluebook +18 -25
- data/lib/hecks/composer/bluebook/composer.hecksagon +1 -1
- data/lib/hecks/composer/board.rb +6 -6
- data/lib/hecks/composer/board_edit/command_text.rb +4 -1
- data/lib/hecks/composer/board_edit/source.rb +18 -9
- data/lib/hecks/composer/board_edit.rb +2 -2
- data/lib/hecks/composer/check.rb +22 -22
- data/lib/hecks/composer/domains.rb +1 -1
- data/lib/hecks/composer/jobs.rb +1 -1
- data/lib/hecks/composer/live.rb +19 -13
- data/lib/hecks/composer/outline.rb +4 -1
- data/lib/hecks/composer/page.html +1 -1
- data/lib/hecks/composer/panels.rb +6 -3
- data/lib/hecks/composer/parse.rb +53 -36
- data/lib/hecks/composer/session.rb +22 -16
- data/lib/hecks/composer/toolbox.rb +17 -7
- data/lib/hecks/composer/web.rb +3 -2
- data/lib/hecks/composer/workbench.rb +12 -9
- data/lib/hecks/composer.rb +1 -1
- data/lib/hecks/construct.rb +7 -7
- data/lib/hecks/corpus/adapters.rb +2 -1
- data/lib/hecks/corpus/generated_key.rb +205 -0
- data/lib/hecks/corpus/listing.rb +34 -0
- data/lib/hecks/corpus/routes.rb +25 -5
- data/lib/hecks/corpus/rust.rb +86 -35
- data/lib/hecks/corpus/sources.rb +14 -12
- data/lib/hecks/corpus/worlds.rb +2 -1
- data/lib/hecks/corpus.rb +57 -54
- data/lib/hecks/deploy/bluebook/deploy.bluebook +304 -518
- data/lib/hecks/deprecation.rb +23 -0
- data/lib/hecks/doc/reference/harvester.rb +4 -1
- data/lib/hecks/doc/reference/readme.rb +11 -6
- data/lib/hecks/doc/reference/rendering.rb +12 -6
- data/lib/hecks/doc/reference.rb +8 -6
- data/lib/hecks/embryonaut_bluebook/lock.rb +5 -1
- data/lib/hecks/embryonaut_bluebook/manifest.rb +8 -2
- data/lib/hecks/embryonaut_bluebook/registry/changelog.rb +4 -1
- data/lib/hecks/embryonaut_bluebook/registry/versions.rb +13 -2
- data/lib/hecks/embryonaut_bluebook/registry.rb +5 -2
- data/lib/hecks/embryonaut_bluebook/shape.rb +1 -1
- data/lib/hecks/embryonaut_bluebook/vendor.rb +5 -2
- data/lib/hecks/embryonaut_bluebook/vendor_cli.rb +15 -1
- data/lib/hecks/embryonaut_bluebook.rb +26 -12
- data/lib/hecks/engine_agreement.rb +4 -1
- data/lib/hecks/fixer/fixer.bluebook +240 -0
- data/lib/hecks/fixer/fixer.hecksagon +17 -0
- data/lib/hecks/forms/app/commands.rb +35 -2
- data/lib/hecks/forms/app/records.rb +12 -12
- data/lib/hecks/forms/app/responses.rb +1 -1
- data/lib/hecks/forms/app.rb +22 -19
- data/lib/hecks/forms/command_form_renderer.rb +2 -2
- data/lib/hecks/forms/field_controls.rb +6 -1
- data/lib/hecks/forms/field_shape.rb +14 -6
- data/lib/hecks/forms/html.rb +8 -7
- data/lib/hecks/forms/html_pattern.rb +80 -0
- data/lib/hecks/forms/index_renderer.rb +11 -11
- data/lib/hecks/forms/params.rb +2 -1
- data/lib/hecks/forms/port_argument.rb +4 -1
- data/lib/hecks/forms/query_form_renderer.rb +1 -1
- data/lib/hecks/forms/record_renderer.rb +2 -2
- data/lib/hecks/forms/record_table.rb +1 -1
- data/lib/hecks/forms/reference_options.rb +2 -1
- data/lib/hecks/forms/text_shape.rb +4 -1
- data/lib/hecks/forms/value_object_shape.rb +1 -1
- data/lib/hecks/forms.rb +7 -7
- data/lib/hecks/fqn.rb +8 -2
- data/lib/hecks/framework/bluebook/change.bluebook +10 -15
- data/lib/hecks/framework/bluebook/change.hecksagon +1 -1
- data/lib/hecks/framework/bluebook/compliance.bluebook +27 -126
- data/lib/hecks/framework/bluebook/console_settings.bluebook +199 -114
- data/lib/hecks/framework/bluebook/framework.hecksagon +7 -0
- data/lib/hecks/framework/bluebook/governance.bluebook +36 -14
- data/lib/hecks/framework/bluebook/identity.bluebook +1 -1
- data/lib/hecks/framework/bluebook/privacy.bluebook +123 -19
- data/lib/hecks/framework/bluebook/privacy.key_keeper.adapter +7 -0
- data/lib/hecks/framework/bluebook/privacy.ports.hecksagon +19 -0
- data/lib/hecks/framework/compliance.oidc.json +6 -36
- data/lib/hecks/framework/console_settings.oidc.json +39 -24
- data/lib/hecks/framework/privacy.oidc.json +25 -0
- data/lib/hecks/framework.rb +24 -8
- data/lib/hecks/framework_bluebooks.rb +181 -0
- data/lib/hecks/freezer.rb +7 -1
- data/lib/hecks/fuzzing/combination_miner.rb +9 -6
- data/lib/hecks/fuzzing/concurrent_dispatch/world_copy.rb +4 -1
- data/lib/hecks/fuzzing/concurrent_dispatch.rb +2 -2
- data/lib/hecks/fuzzing/domain_generator.rb +8 -3
- data/lib/hecks/fuzzing/era_boundary.rb +1 -1
- data/lib/hecks/fuzzing/failure_triage.rb +13 -6
- data/lib/hecks/fuzzing/form_census/{chapter_forms.rb → bluebook_forms.rb} +12 -12
- data/lib/hecks/fuzzing/form_census.rb +14 -12
- data/lib/hecks/fuzzing/generated_domain_check.rb +2 -1
- data/lib/hecks/fuzzing/isolated_boot/postgres_scratch.rb +14 -15
- data/lib/hecks/fuzzing/isolated_boot/scratch_schema.rb +19 -9
- data/lib/hecks/fuzzing/isolated_boot/worlds.rb +36 -13
- data/lib/hecks/fuzzing/isolated_boot.rb +5 -6
- data/lib/hecks/fuzzing/mutation/checks.rb +5 -4
- data/lib/hecks/fuzzing/mutation/finders.rb +25 -8
- data/lib/hecks/fuzzing/mutation/observation.rb +3 -1
- data/lib/hecks/fuzzing/mutation/source_map.rb +3 -1
- data/lib/hecks/fuzzing/nondeterministic.rb +10 -4
- data/lib/hecks/fuzzing/outside_world.rb +1 -1
- data/lib/hecks/fuzzing/projection_agreement/binding.rb +63 -0
- data/lib/hecks/fuzzing/projection_agreement/comparison.rb +104 -0
- data/lib/hecks/fuzzing/projection_agreement.rb +68 -0
- data/lib/hecks/fuzzing/properties/aggregation_recompute.rb +1 -1
- data/lib/hecks/fuzzing/properties/bulk_parity.rb +118 -0
- data/lib/hecks/fuzzing/properties/catalog.rb +7 -0
- data/lib/hecks/fuzzing/properties/cursor_walks.rb +56 -0
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +39 -1
- data/lib/hecks/fuzzing/properties/group_by_recompute.rb +6 -2
- data/lib/hecks/fuzzing/properties/grouped_reduction_recompute.rb +88 -0
- data/lib/hecks/fuzzing/properties/guards.rb +4 -1
- data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +12 -2
- data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +2 -1
- data/lib/hecks/fuzzing/properties/mutation_recompute.rb +3 -2
- data/lib/hecks/fuzzing/properties/piece_invariants.rb +3 -2
- data/lib/hecks/fuzzing/properties/query_rows.rb +5 -3
- data/lib/hecks/fuzzing/properties/querying.rb +6 -3
- data/lib/hecks/fuzzing/properties/read_model_names.rb +8 -2
- data/lib/hecks/fuzzing/properties/read_models.rb +2 -1
- data/lib/hecks/fuzzing/properties/refusals.rb +35 -0
- data/lib/hecks/fuzzing/properties/role_checks.rb +3 -2
- data/lib/hecks/fuzzing/properties/set_transform_parity.rb +73 -0
- data/lib/hecks/fuzzing/properties/tenant_scope.rb +3 -2
- data/lib/hecks/fuzzing/properties.rb +6 -0
- data/lib/hecks/fuzzing/qa_settings.rb +4 -4
- data/lib/hecks/fuzzing/replay/fan_out.rb +4 -3
- data/lib/hecks/fuzzing/replay/filters.rb +2 -2
- data/lib/hecks/fuzzing/replay/guard_check.rb +2 -2
- data/lib/hecks/fuzzing/replay/mutation_trace.rb +23 -10
- data/lib/hecks/fuzzing/replay/observation.rb +11 -7
- data/lib/hecks/fuzzing/replay/read_only_steps.rb +40 -3
- data/lib/hecks/fuzzing/replay/refusal_trace.rb +44 -0
- data/lib/hecks/fuzzing/replay/role_check.rb +2 -2
- data/lib/hecks/fuzzing/replay/session.rb +38 -18
- data/lib/hecks/fuzzing/replay/validation_reports.rb +61 -0
- data/lib/hecks/fuzzing/replay.rb +2 -2
- data/lib/hecks/fuzzing/rust_gap_manifest.rb +3 -3
- data/lib/hecks/fuzzing/self_consistency/saga_redelivery.rb +18 -8
- data/lib/hecks/fuzzing/self_consistency/sagas.rb +2 -2
- data/lib/hecks/fuzzing/self_consistency/value_objects.rb +9 -2
- data/lib/hecks/fuzzing/self_consistency.rb +4 -8
- data/lib/hecks/fuzzing/sequence_generator/adversary/argument_mutations.rb +15 -5
- data/lib/hecks/fuzzing/sequence_generator/adversary/caller_draw.rb +14 -10
- data/lib/hecks/fuzzing/sequence_generator/adversary/precedence_mutations.rb +1 -1
- data/lib/hecks/fuzzing/sequence_generator/argument_drawing.rb +2 -1
- data/lib/hecks/fuzzing/sequence_generator/catalog.rb +21 -13
- data/lib/hecks/fuzzing/sequence_generator/picker.rb +1 -1
- data/lib/hecks/fuzzing/sequence_generator/query_binding.rb +3 -3
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +2 -2
- data/lib/hecks/fuzzing/sequence_generator.rb +1 -2
- data/lib/hecks/fuzzing/shrinker.rb +4 -1
- data/lib/hecks/fuzzing/value_generator.rb +14 -8
- data/lib/hecks/fuzzing.rb +1 -0
- data/lib/hecks/grammar/dialects.rb +56 -0
- data/lib/hecks/grammar/evolve/arguments.rb +1 -1
- data/lib/hecks/grammar/evolve/keywords.rb +1 -1
- data/lib/hecks/grammar/evolve/seeds.rb +34 -8
- data/lib/hecks/grammar/expression.bluebook +3 -0
- data/lib/hecks/grammar/expression_operators.json +1071 -1
- data/lib/hecks/grammar/operators.rb +45 -22
- data/lib/hecks/grammar/translation.bluebook +3 -1
- data/lib/hecks/grammar/translation_ledger.rb +45 -0
- data/lib/hecks/grammar/translation_rules.json +710 -0
- data/lib/hecks/grammar.rb +27 -23
- data/lib/hecks/hecks/adapter_words.json +7 -0
- data/lib/hecks/hecks/adapters/area_overlap.rb +156 -0
- data/lib/hecks/hecks/adapters/claude_agents.adapter +7 -0
- data/lib/hecks/hecks/adapters/claude_agents.rb +124 -0
- data/lib/hecks/hecks/adapters/claude_transcript.rb +71 -0
- data/lib/hecks/hecks/adapters/codebase/batch_runs.rb +60 -0
- data/lib/hecks/hecks/adapters/codebase/cache_clearing.rb +32 -0
- data/lib/hecks/hecks/adapters/codebase/cargo_tests.rb +50 -0
- data/lib/hecks/hecks/adapters/codebase/changed_specs.rb +52 -0
- data/lib/hecks/hecks/adapters/codebase/codemods.rb +3 -2
- data/lib/hecks/hecks/adapters/codebase/composing.rb +3 -2
- data/lib/hecks/hecks/adapters/codebase/conformance.rb +30 -1
- data/lib/hecks/hecks/adapters/codebase/corpus_queries.rb +53 -0
- data/lib/hecks/hecks/adapters/codebase/corpus_tasks.rb +1 -1
- data/lib/hecks/hecks/adapters/codebase/declarations.rb +144 -0
- data/lib/hecks/hecks/adapters/codebase/generated_keys.rb +31 -0
- data/lib/hecks/hecks/adapters/codebase/impacted_specs.rb +110 -0
- data/lib/hecks/hecks/adapters/codebase/inspection.rb +5 -4
- data/lib/hecks/hecks/adapters/codebase/language.rb +3 -2
- data/lib/hecks/hecks/adapters/codebase/launcher_text.rb +39 -0
- data/lib/hecks/hecks/adapters/codebase/minitest_runs.rb +52 -0
- data/lib/hecks/hecks/adapters/codebase/npm_registry.rb +7 -1
- data/lib/hecks/hecks/adapters/codebase/previewing.rb +62 -0
- data/lib/hecks/hecks/adapters/codebase/promotion/gate.rb +1 -1
- data/lib/hecks/hecks/adapters/codebase/reading.rb +94 -0
- data/lib/hecks/hecks/adapters/codebase/regeneration.rb +57 -10
- data/lib/hecks/hecks/adapters/codebase/release_facts.rb +1 -1
- data/lib/hecks/hecks/adapters/codebase/ruby_child.rb +5 -2
- data/lib/hecks/hecks/adapters/codebase/run_options.rb +35 -0
- data/lib/hecks/hecks/adapters/codebase/scripting.rb +39 -0
- data/lib/hecks/hecks/adapters/codebase/search.rb +91 -0
- data/lib/hecks/hecks/adapters/codebase/source_tree.rb +114 -51
- data/lib/hecks/hecks/adapters/codebase/spec_files/report.rb +71 -0
- data/lib/hecks/hecks/adapters/codebase/spec_files/shared_run.rb +73 -0
- data/lib/hecks/hecks/adapters/codebase/spec_files.rb +95 -0
- data/lib/hecks/hecks/adapters/codebase/static_directory.rb +59 -0
- data/lib/hecks/hecks/adapters/codebase/style.rb +2 -2
- data/lib/hecks/hecks/adapters/codebase/test_runner.rb +82 -14
- data/lib/hecks/hecks/adapters/codebase/test_scope.rb +105 -0
- data/lib/hecks/hecks/adapters/codebase/test_suite.rb +73 -21
- data/lib/hecks/hecks/adapters/console_capture.rb +28 -1
- data/lib/hecks/hecks/adapters/cost_explorer.rb +9 -1
- data/lib/hecks/hecks/adapters/deploy_plans.rb +12 -3
- data/lib/hecks/hecks/adapters/deploy_toolchain/answers.rb +14 -5
- data/lib/hecks/hecks/adapters/deploy_toolchain/scripts.rb +6 -3
- data/lib/hecks/hecks/adapters/deploy_toolchain.rb +3 -3
- data/lib/hecks/hecks/adapters/desktop_alert/alert.rb +50 -0
- data/lib/hecks/hecks/adapters/desktop_alert.adapter +7 -0
- data/lib/hecks/hecks/adapters/desktop_alert.rb +99 -0
- data/lib/hecks/hecks/adapters/dispatching/brief_text.rb +51 -0
- data/lib/hecks/hecks/adapters/dispatching/launcher.rb +97 -0
- data/lib/hecks/hecks/adapters/dispatching/plan.rb +82 -0
- data/lib/hecks/hecks/adapters/dispatching/rows.rb +66 -0
- data/lib/hecks/hecks/adapters/dispatching/settings.rb +81 -0
- data/lib/hecks/hecks/adapters/dispatching/settler.rb +40 -0
- data/lib/hecks/hecks/adapters/dispatching.adapter +7 -0
- data/lib/hecks/hecks/adapters/dispatching.rb +93 -0
- data/lib/hecks/hecks/adapters/doctor_reading/report.rb +27 -0
- data/lib/hecks/hecks/adapters/doctor_reading.adapter +7 -0
- data/lib/hecks/hecks/adapters/doctor_reading.rb +161 -0
- data/lib/hecks/hecks/adapters/finding_github.rb +4 -1
- data/lib/hecks/hecks/adapters/first_admin.rb +16 -9
- data/lib/hecks/hecks/adapters/git.rb +5 -5
- data/lib/hecks/hecks/adapters/git_checkout.adapter +7 -0
- data/lib/hecks/hecks/adapters/git_checkout.rb +99 -0
- data/lib/hecks/hecks/adapters/git_commit.adapter +7 -0
- data/lib/hecks/hecks/adapters/git_commit.rb +61 -0
- data/lib/hecks/hecks/adapters/git_landing/finisher.rb +82 -0
- data/lib/hecks/hecks/adapters/git_landing/foreign_hold.rb +111 -0
- data/lib/hecks/hecks/adapters/git_landing/line.rb +171 -0
- data/lib/hecks/hecks/adapters/git_landing/lineup.rb +98 -0
- data/lib/hecks/hecks/adapters/git_landing/merger.rb +85 -0
- data/lib/hecks/hecks/adapters/git_landing/numbering.rb +54 -0
- data/lib/hecks/hecks/adapters/git_landing/rivals.rb +59 -0
- data/lib/hecks/hecks/adapters/git_landing/ticket.rb +156 -0
- data/lib/hecks/hecks/adapters/git_landing/tidy.rb +147 -0
- data/lib/hecks/hecks/adapters/git_landing/train.rb +104 -0
- data/lib/hecks/hecks/adapters/git_landing/verdict.rb +64 -0
- data/lib/hecks/hecks/adapters/git_landing/waiting.rb +120 -0
- data/lib/hecks/hecks/adapters/git_landing.adapter +7 -0
- data/lib/hecks/hecks/adapters/git_landing.rb +189 -0
- data/lib/hecks/hecks/adapters/git_launcher.adapter +7 -0
- data/lib/hecks/hecks/adapters/git_launcher.rb +82 -0
- data/lib/hecks/hecks/adapters/git_main_pull.rb +99 -0
- data/lib/hecks/hecks/adapters/git_refs.rb +8 -4
- data/lib/hecks/hecks/adapters/git_rehearsal/bench.rb +82 -0
- data/lib/hecks/hecks/adapters/git_rehearsal/due.rb +84 -0
- data/lib/hecks/hecks/adapters/git_rehearsal/impacted/skips.rb +47 -0
- data/lib/hecks/hecks/adapters/git_rehearsal/impacted/spec_run.rb +75 -0
- data/lib/hecks/hecks/adapters/git_rehearsal/impacted.rb +181 -0
- data/lib/hecks/hecks/adapters/git_rehearsal/line.rb +76 -0
- data/lib/hecks/hecks/adapters/git_rehearsal/reading.rb +24 -0
- data/lib/hecks/hecks/adapters/git_rehearsal/record.rb +146 -0
- data/lib/hecks/hecks/adapters/git_rehearsal/spec_map/corpus.rb +25 -0
- data/lib/hecks/hecks/adapters/git_rehearsal/spec_map/needles.rb +61 -0
- data/lib/hecks/hecks/adapters/git_rehearsal/spec_map.rb +105 -0
- data/lib/hecks/hecks/adapters/git_rehearsal/stages.rb +118 -0
- data/lib/hecks/hecks/adapters/git_rehearsal.adapter +7 -0
- data/lib/hecks/hecks/adapters/git_rehearsal.rb +141 -0
- data/lib/hecks/hecks/adapters/git_train/bench.rb +76 -0
- data/lib/hecks/hecks/adapters/git_train/branch_guard.rb +62 -0
- data/lib/hecks/hecks/adapters/git_train/carriage.rb +115 -0
- data/lib/hecks/hecks/adapters/git_train/launcher_shape.rb +75 -0
- data/lib/hecks/hecks/adapters/git_train/local_branch.rb +69 -0
- data/lib/hecks/hecks/adapters/git_train/mint_step.rb +47 -0
- data/lib/hecks/hecks/adapters/git_train/reading.rb +26 -0
- data/lib/hecks/hecks/adapters/git_train/settlement.rb +167 -0
- data/lib/hecks/hecks/adapters/git_train/survey.rb +60 -0
- data/lib/hecks/hecks/adapters/git_train.adapter +7 -0
- data/lib/hecks/hecks/adapters/git_train.rb +146 -0
- data/lib/hecks/hecks/adapters/git_worktree_add.adapter +7 -0
- data/lib/hecks/hecks/adapters/git_worktree_add.rb +68 -0
- data/lib/hecks/hecks/adapters/git_worktrees.rb +81 -0
- data/lib/hecks/hecks/adapters/github_pull_requests.adapter +7 -0
- data/lib/hecks/hecks/adapters/github_pull_requests.rb +109 -0
- data/lib/hecks/hecks/adapters/host_http.rb +9 -8
- data/lib/hecks/hecks/adapters/in_process_access.rb +3 -2
- data/lib/hecks/hecks/adapters/in_process_boot.rb +68 -39
- data/lib/hecks/hecks/adapters/in_process_operations.rb +40 -20
- data/lib/hecks/hecks/adapters/in_process_undo.rb +17 -0
- data/lib/hecks/hecks/adapters/journal_store/changes.rb +18 -9
- data/lib/hecks/hecks/adapters/journal_store/compaction.rb +14 -7
- data/lib/hecks/hecks/adapters/journal_store/edge_audit.rb +35 -20
- data/lib/hecks/hecks/adapters/journal_store/examination.rb +20 -4
- data/lib/hecks/hecks/adapters/journal_store/held_domain/renaming.rb +45 -0
- data/lib/hecks/hecks/adapters/journal_store/held_domain.rb +47 -9
- data/lib/hecks/hecks/adapters/journal_store/held_mint.rb +128 -0
- data/lib/hecks/hecks/adapters/journal_store/readings/attestation.rb +3 -3
- data/lib/hecks/hecks/adapters/journal_store/readings/boot_status.rb +58 -0
- data/lib/hecks/hecks/adapters/journal_store/readings/composition.rb +39 -0
- data/lib/hecks/hecks/adapters/journal_store/readings/event_trace/outcomes.rb +76 -0
- data/lib/hecks/hecks/adapters/journal_store/readings/event_trace/postgres_reader.rb +44 -0
- data/lib/hecks/hecks/adapters/journal_store/readings/event_trace/reader.rb +64 -0
- data/lib/hecks/hecks/adapters/journal_store/readings/event_trace/report.rb +102 -0
- data/lib/hecks/hecks/adapters/journal_store/readings/event_trace/sqlite_reader.rb +43 -0
- data/lib/hecks/hecks/adapters/journal_store/readings/event_trace/stores.rb +80 -0
- data/lib/hecks/hecks/adapters/journal_store/readings/event_trace/subscribers.rb +72 -0
- data/lib/hecks/hecks/adapters/journal_store/readings/event_trace/summary.rb +39 -0
- data/lib/hecks/hecks/adapters/journal_store/readings/event_trace.rb +39 -0
- data/lib/hecks/hecks/adapters/journal_store/readings/mint_plan/command_line.rb +57 -0
- data/lib/hecks/hecks/adapters/journal_store/readings/mint_plan/edge_guards.rb +107 -0
- data/lib/hecks/hecks/adapters/journal_store/readings/mint_plan/inspection.rb +94 -0
- data/lib/hecks/hecks/adapters/journal_store/readings/mint_plan/plan.rb +66 -0
- data/lib/hecks/hecks/adapters/journal_store/readings/mint_plan/report.rb +72 -0
- data/lib/hecks/hecks/adapters/journal_store/readings/mint_plan/store.rb +34 -0
- data/lib/hecks/hecks/adapters/journal_store/readings/mint_plan/watermark_guards.rb +86 -0
- data/lib/hecks/hecks/adapters/journal_store/readings/mint_plan/world.rb +66 -0
- data/lib/hecks/hecks/adapters/journal_store/readings/mint_plan.rb +36 -0
- data/lib/hecks/hecks/adapters/journal_store/readings/projection_audit.rb +64 -0
- data/lib/hecks/hecks/adapters/journal_store/readings/scaffolding.rb +10 -7
- data/lib/hecks/hecks/adapters/journal_store/readings/sqlite_status/database_reading.rb +113 -0
- data/lib/hecks/hecks/adapters/journal_store/readings/sqlite_status/read_only.rb +33 -0
- data/lib/hecks/hecks/adapters/journal_store/readings/sqlite_status.rb +32 -0
- data/lib/hecks/hecks/adapters/journal_store/readings.rb +15 -3
- data/lib/hecks/hecks/adapters/journal_store/reconcile_report.rb +60 -0
- data/lib/hecks/hecks/adapters/journal_store/reconciliation.rb +121 -0
- data/lib/hecks/hecks/adapters/journal_store/translation_write/shape_edge.rb +59 -0
- data/lib/hecks/hecks/adapters/journal_store/translation_write.rb +123 -0
- data/lib/hecks/hecks/adapters/journal_store.rb +18 -2
- data/lib/hecks/hecks/adapters/landed_work.adapter +7 -0
- data/lib/hecks/hecks/adapters/landed_work.rb +52 -0
- data/lib/hecks/hecks/adapters/ledger/chain.rb +63 -0
- data/lib/hecks/hecks/adapters/ledger/journal.rb +105 -0
- data/lib/hecks/hecks/adapters/ledger.adapter +7 -0
- data/lib/hecks/hecks/adapters/ledger.rb +89 -0
- data/lib/hecks/hecks/adapters/local_files.rb +21 -7
- data/lib/hecks/hecks/adapters/pg_admin.rb +7 -1
- data/lib/hecks/hecks/adapters/process_pool.rb +3 -2
- data/lib/hecks/hecks/adapters/push_size.rb +57 -0
- data/lib/hecks/hecks/adapters/qa_quiescing.adapter +7 -0
- data/lib/hecks/hecks/adapters/qa_quiescing.rb +57 -0
- data/lib/hecks/hecks/adapters/rails_app_files.rb +64 -0
- data/lib/hecks/hecks/adapters/rehearsal/audit_report.rb +4 -1
- data/lib/hecks/hecks/adapters/rehearsal/inquiry.rb +39 -12
- data/lib/hecks/hecks/adapters/rehearsal/memory_store.rb +65 -5
- data/lib/hecks/hecks/adapters/rehearsal/postgres_census.rb +4 -4
- data/lib/hecks/hecks/adapters/rehearsal/postgres_reading.rb +30 -7
- data/lib/hecks/hecks/adapters/rehearsal/postgres_store.rb +4 -1
- data/lib/hecks/hecks/adapters/rehearsal/rust_audit.rb +1 -1
- data/lib/hecks/hecks/adapters/rehearsal/subject.rb +44 -9
- data/lib/hecks/hecks/adapters/rust_toolchain.rb +15 -4
- data/lib/hecks/hecks/adapters/rust_workspace.rb +1 -1
- data/lib/hecks/hecks/adapters/settings_reading/disposable_ledger.rb +32 -0
- data/lib/hecks/hecks/adapters/settings_reading/push_nudge.rb +26 -0
- data/lib/hecks/hecks/adapters/settings_reading/stores.rb +76 -0
- data/lib/hecks/hecks/adapters/settings_reading/test_scope.rb +28 -0
- data/lib/hecks/hecks/adapters/settings_reading.adapter +7 -0
- data/lib/hecks/hecks/adapters/settings_reading.rb +168 -0
- data/lib/hecks/hecks/adapters/shell.rb +3 -0
- data/lib/hecks/hecks/adapters/site_toolchain.rb +8 -8
- data/lib/hecks/hecks/adapters/started_work.rb +42 -0
- data/lib/hecks/hecks/adapters/terminal.rb +12 -10
- data/lib/hecks/hecks/adapters/where_reading.adapter +7 -0
- data/lib/hecks/hecks/adapters/where_reading.rb +64 -0
- data/lib/hecks/hecks/adapters/work_areas.rb +57 -0
- data/lib/hecks/hecks/adapters/work_limits.rb +57 -0
- data/lib/hecks/hecks/adapters/world_clearing.rb +2 -1
- data/lib/hecks/hecks/build/build.bluebook +576 -0
- data/lib/hecks/hecks/check/check.bluebook +966 -0
- data/lib/hecks/hecks/codebase.behaviors +72 -4
- data/lib/hecks/hecks/composer.behaviors +10 -10
- data/lib/hecks/hecks/context_map.hecksagon +7 -6
- data/lib/hecks/hecks/custodian.behaviors +124 -3
- data/lib/hecks/hecks/delegate/delegate.bluebook +355 -0
- data/lib/hecks/hecks/deliver/deliver.bluebook +945 -0
- data/lib/hecks/hecks/deploy.behaviors +1 -1
- data/lib/hecks/hecks/deploy.world +3 -4
- data/lib/hecks/hecks/evolve/evolve.bluebook +788 -0
- data/lib/hecks/hecks/find/find.bluebook +81 -0
- data/lib/hecks/hecks/hecks.behaviors +2 -2
- data/lib/hecks/hecks/hecks.bluebook +0 -138
- data/lib/hecks/hecks/hecks.contextmap +27 -0
- data/lib/hecks/hecks/hecks.hecksagon +312 -23
- data/lib/hecks/hecks/hecks.world +54 -27
- data/lib/hecks/hecks/implicit_moves.json +24 -0
- data/lib/hecks/hecks/migrate/migrate.bluebook +1066 -0
- data/lib/hecks/hecks/operate/operate.bluebook +695 -0
- data/lib/hecks/hecks/quiesce/quiesce.bluebook +117 -0
- data/lib/hecks/hecks/rehearse/rehearse.bluebook +133 -0
- data/lib/hecks/hecks/release/release.bluebook +916 -0
- data/lib/hecks/hecks/serve/serve.bluebook +134 -0
- data/lib/hecks/hecks/shapes.json +3 -0
- data/lib/hecks/hecks/site.behaviors +1 -1
- data/lib/hecks/hecks/site.world +3 -3
- data/lib/hecks/hecks/start/start.bluebook +194 -0
- data/lib/hecks/hecks/test/test.bluebook +638 -0
- data/lib/hecks/hecks/tickets.world +14 -0
- data/lib/hecks/ir.rb +8 -1
- data/lib/hecks/kernel_capabilities/templates.rb +4 -3
- data/lib/hecks/kernel_capabilities.rb +6 -6
- data/lib/hecks/language/adapter.bluebook +61 -0
- data/lib/hecks/language/bluebook/aggregate.bluebook +58 -6
- data/lib/hecks/language/bluebook/attaches/paging.bluebook +1 -1
- data/lib/hecks/language/bluebook/bluebook.behaviors +6 -6
- data/lib/hecks/language/bluebook/bluebook.bluebook +62 -51
- data/lib/hecks/language/bluebook/bluebook.hecksagon +1 -1
- data/lib/hecks/language/bluebook/command.bluebook +69 -2
- data/lib/hecks/language/bluebook/entity.bluebook +2 -2
- data/lib/hecks/language/bluebook/policy.behaviors +2 -2
- data/lib/hecks/language/bluebook/policy.bluebook +3 -3
- data/lib/hecks/language/bluebook/process_manager.bluebook +17 -4
- data/lib/hecks/language/bluebook/projection.bluebook +11 -10
- data/lib/hecks/language/bluebook/query.bluebook +1 -0
- data/lib/hecks/language/bluebook/shape.bluebook +1 -1
- data/lib/hecks/language/bluebook/syntax.bluebook +3 -1
- data/lib/hecks/language/bluebook/vocabulary.bluebook +417 -16
- data/lib/hecks/language/context_map/context_map.bluebook +90 -0
- data/lib/hecks/language/hecksagon/domain_port.bluebook +3 -2
- data/lib/hecks/language/hecksagon/hecksagon.bluebook +8 -8
- data/lib/hecks/language/hecksagon/port_operation.bluebook +14 -0
- data/lib/hecks/language/kinds/run.kind +120 -0
- data/lib/hecks/language/oidc.json +20 -0
- data/lib/hecks/language/port.bluebook +1 -1
- data/lib/hecks/language/sentence/sentence.bluebook +210 -0
- data/lib/hecks/language/translation/translation_aggregate.bluebook +24 -0
- data/lib/hecks/language/world/world.bluebook +6 -6
- data/lib/hecks/literal.rb +8 -2
- data/lib/hecks/module_folders.rb +31 -0
- data/lib/hecks/naming.rb +44 -14
- data/lib/hecks/persistence_legacy_fixture/postgres_stores.rb +7 -6
- data/lib/hecks/persistence_legacy_fixture/regenerate.rb +6 -2
- data/lib/hecks/ports/access_control.rb +2 -2
- data/lib/hecks/ports/agent/answers.rb +2 -2
- data/lib/hecks/ports/agent.rb +2 -2
- data/lib/hecks/ports/authentication.port +6 -0
- data/lib/hecks/ports/authentication.rb +67 -0
- data/lib/hecks/ports/authorization.rb +3 -3
- data/lib/hecks/ports/dispatch.port +5 -0
- data/lib/hecks/ports/dispatch.rb +28 -0
- data/lib/hecks/ports/key_vault.rb +10 -7
- data/lib/hecks/ports/loading.rb +23 -7
- data/lib/hecks/ports/persistence/append_only.rb +44 -9
- data/lib/hecks/ports/persistence/binding_policy.rb +73 -12
- data/lib/hecks/ports/persistence/codec_boundary.rb +4 -3
- data/lib/hecks/ports/persistence/mint_door.rb +74 -0
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +16 -15
- data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +9 -6
- data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +11 -11
- data/lib/hecks/ports/persistence/plugins/era/era_status.rb +48 -0
- data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/projected_bluebook.rb +127 -0
- data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +6 -6
- data/lib/hecks/ports/persistence/plugins/era/translation/additive_shape.rb +107 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +9 -3
- data/lib/hecks/ports/persistence/plugins/era/translation/compose.rb +155 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/compose_render.rb +79 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/edge_files.rb +49 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/edge_route.rb +61 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/implicit_edge.rb +67 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/translation/reconcile.rb +130 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/reconcile_files.rb +35 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +32 -19
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +13 -5
- data/lib/hecks/ports/persistence/plugins/era/translation.rb +5 -0
- data/lib/hecks/ports/persistence/plugins/era.rb +1 -1
- data/lib/hecks/ports/persistence/remote_runtime.rb +3 -2
- data/lib/hecks/ports/persistence/repository_factory.rb +11 -3
- data/lib/hecks/ports/persistence/set_statement.rb +29 -0
- data/lib/hecks/ports/persistence/set_transforming.rb +30 -0
- data/lib/hecks/ports/persistence/state_codec.rb +3 -3
- data/lib/hecks/ports/persistence/stored_name_guard.rb +36 -0
- data/lib/hecks/ports/persistence.rb +2 -4
- data/lib/hecks/ports/projection.rb +4 -1
- data/lib/hecks/ports/query/cursor.rb +131 -0
- data/lib/hecks/ports/query/in_memory.rb +17 -10
- data/lib/hecks/ports/query.rb +4 -2
- data/lib/hecks/ports.rb +1 -0
- data/lib/hecks/projection_files/expression_tables.rb +3 -3
- data/lib/hecks/projection_files/sentence_table.rb +17 -0
- data/lib/hecks/projection_files.rb +16 -13
- data/lib/hecks/projections/bootstrap_table.rb +3 -3
- data/lib/hecks/projections/ci_pipeline/table/checks.rb +137 -0
- data/lib/hecks/projections/ci_pipeline/table/lookup.rb +59 -0
- data/lib/hecks/projections/ci_pipeline/table/refusals.rb +58 -0
- data/lib/hecks/projections/ci_pipeline/table/rows.rb +119 -0
- data/lib/hecks/projections/ci_pipeline/table/tree.rb +30 -0
- data/lib/hecks/projections/ci_pipeline/table/typing.rb +92 -0
- data/lib/hecks/projections/ci_pipeline/table.rb +112 -0
- data/lib/hecks/projections/ci_pipeline/workflows.rb +162 -0
- data/lib/hecks/projections/ci_pipeline/yaml_text.rb +104 -0
- data/lib/hecks/projections/command_schema/shape.rb +95 -0
- data/lib/hecks/projections/command_schema.rb +74 -0
- data/lib/hecks/projections/context_map_diagram.rb +135 -0
- data/lib/hecks/projections/context_relationships.rb +125 -0
- data/lib/hecks/projections/deploy/box/access.rb +7 -4
- data/lib/hecks/projections/deploy/box/caddy.rb +2 -1
- data/lib/hecks/projections/deploy/box/compose.rb +4 -8
- data/lib/hecks/projections/deploy/box/edge.rb +2 -1
- data/lib/hecks/projections/deploy/box/hosting.rb +5 -3
- data/lib/hecks/projections/deploy/box/hosting_parts.rb +27 -12
- data/lib/hecks/projections/deploy/box/logging.rb +2 -3
- data/lib/hecks/projections/deploy/box/roll_recipe.rb +2 -4
- data/lib/hecks/projections/deploy/box/roll_scripts.rb +2 -17
- data/lib/hecks/projections/deploy/box/settings/containers.rb +29 -15
- data/lib/hecks/projections/deploy/box/settings/patterns.rb +5 -6
- data/lib/hecks/projections/deploy/box/settings/types.rb +5 -10
- data/lib/hecks/projections/deploy/box/settings.rb +9 -8
- data/lib/hecks/projections/deploy/box/stack_yaml.rb +10 -14
- data/lib/hecks/projections/deploy/box/templates/box.yaml.tmpl +0 -1
- data/lib/hecks/projections/deploy/box/templates/deploy-service.sh.tmpl +16 -3
- data/lib/hecks/projections/deploy/box/templates/hosting.mk.tmpl +7 -6
- data/lib/hecks/projections/deploy/box/templates/render-compose-taskdef.sh.tmpl +2 -6
- data/lib/hecks/projections/deploy/box/templates/render-compose.sh.tmpl +2 -6
- data/lib/hecks/projections/deploy/box.rb +17 -10
- data/lib/hecks/projections/deploy/fargate/assembly.rb +7 -2
- data/lib/hecks/projections/deploy/fargate/cdn/options.rb +4 -1
- data/lib/hecks/projections/deploy/fargate/check.rb +3 -1
- data/lib/hecks/projections/deploy/fargate/containers/rendering.rb +8 -5
- data/lib/hecks/projections/deploy/fargate/containers/routing.rb +2 -1
- data/lib/hecks/projections/deploy/fargate/monitoring/reading.rb +4 -1
- data/lib/hecks/projections/deploy/fargate/sections.rb +7 -4
- data/lib/hecks/projections/deploy/fargate/settings/ids.rb +4 -1
- data/lib/hecks/projections/deploy/fargate/settings/tuning.rb +13 -4
- data/lib/hecks/projections/deploy/fargate/stack/blocks.rb +6 -1
- data/lib/hecks/projections/deploy/fargate/stack/readers.rb +2 -1
- data/lib/hecks/projections/deploy/fargate/stack.rb +11 -6
- data/lib/hecks/projections/deploy/fargate/yaml.rb +10 -3
- data/lib/hecks/projections/deploy/fargate.rb +8 -3
- data/lib/hecks/projections/deploy/lambda/stack/blocks.rb +4 -2
- data/lib/hecks/projections/deploy/lambda/stack/deploy_chain.rb +2 -1
- data/lib/hecks/projections/deploy/lambda/stack/reading.rb +12 -9
- data/lib/hecks/projections/deploy/lambda/stack/reading_web.rb +7 -2
- data/lib/hecks/projections/deploy/lambda/stack/web.rb +2 -1
- data/lib/hecks/projections/deploy/lambda.rb +8 -3
- data/lib/hecks/projections/deploy/preview/containers.rb +14 -1
- data/lib/hecks/projections/deploy/preview/preview.sh.erb +2 -2
- data/lib/hecks/projections/deploy/preview/routing_yaml.rb +8 -6
- data/lib/hecks/projections/deploy/preview/settings.rb +23 -5
- data/lib/hecks/projections/deploy/preview/task_yaml.rb +1 -1
- data/lib/hecks/projections/deploy/preview/yaml_text.rb +2 -1
- data/lib/hecks/projections/deploy/scripts/settings.rb +5 -2
- data/lib/hecks/projections/deploy/shared.rb +12 -1
- data/lib/hecks/projections/deploy/shared_database_instance.rb +1 -1
- data/lib/hecks/projections/deploy/smoke.rb +3 -1
- data/lib/hecks/projections/deploy/template_diff/normalizer.rb +4 -1
- data/lib/hecks/projections/deploy/templates/fargate/Makefile.tmpl +1 -1
- data/lib/hecks/projections/deploy/templates/lambda/Makefile.tmpl +3 -3
- data/lib/hecks/projections/deploy/templates/lambda/mint_era_shared.tmpl +2 -2
- data/lib/hecks/projections/deploy/templates/lambda/nat_comment_oauth.tmpl +1 -1
- data/lib/hecks/projections/deploy/vercel.rb +1 -1
- data/lib/hecks/projections/diagrams/dispatch.rb +5 -3
- data/lib/hecks/projections/diagrams/statecharts.rb +8 -3
- data/lib/hecks/projections/diagrams/structure.rb +9 -6
- data/lib/hecks/projections/diagrams.rb +9 -7
- data/lib/hecks/projections/glossary/entries.rb +4 -4
- data/lib/hecks/projections/glossary/html/page.rb +4 -1
- data/lib/hecks/projections/glossary/html/parser.rb +13 -4
- data/lib/hecks/projections/glossary/html/sections.rb +4 -1
- data/lib/hecks/projections/glossary/html.rb +10 -3
- data/lib/hecks/projections/glossary/markdown.rb +4 -3
- data/lib/hecks/projections/glossary/mermaid.rb +8 -5
- data/lib/hecks/projections/glossary/sections.rb +1 -1
- data/lib/hecks/projections/glossary/sensitivity.rb +11 -4
- data/lib/hecks/projections/glossary/sentences/actions.rb +5 -2
- data/lib/hecks/projections/glossary/sentences/queries.rb +2 -1
- data/lib/hecks/projections/glossary/sentences/value_objects.rb +15 -6
- data/lib/hecks/projections/glossary/sentences.rb +6 -3
- data/lib/hecks/projections/glossary/slugs.rb +8 -1
- data/lib/hecks/projections/glossary.rb +3 -3
- data/lib/hecks/projections/model.rb +10 -6
- data/lib/hecks/projections/oidc.rb +19 -7
- data/lib/hecks/projections/parser_table.rb +10 -10
- data/lib/hecks/projections/phrase_table.rb +105 -0
- data/lib/hecks/projections/plain_english.rb +14 -4
- data/lib/hecks/projections/rails_app.rb +77 -0
- data/lib/hecks/projections/reference.rb +2 -2
- data/lib/hecks/projections/rust_vocabulary/arguments.rb +1 -1
- data/lib/hecks/projections/rust_vocabulary/tables.rb +20 -0
- data/lib/hecks/projections/rust_vocabulary.rb +14 -17
- data/lib/hecks/projections/site/admin.rb +7 -7
- data/lib/hecks/projections/site/admin_cms.rb +1 -1
- data/lib/hecks/projections/site/cms_editor/blocks.rb +5 -2
- data/lib/hecks/projections/site/cms_editor/checks.rb +9 -6
- data/lib/hecks/projections/site/cms_editor/color.rb +4 -1
- data/lib/hecks/projections/site/cms_editor/composition.rb +20 -18
- data/lib/hecks/projections/site/cms_editor/destructive.rb +4 -1
- data/lib/hecks/projections/site/cms_editor/drafts.rb +6 -2
- data/lib/hecks/projections/site/cms_editor/key_kinds.rb +4 -1
- data/lib/hecks/projections/site/cms_editor/limits.rb +1 -1
- data/lib/hecks/projections/site/cms_editor/media.rb +1 -1
- data/lib/hecks/projections/site/cms_editor/moment.rb +3 -1
- data/lib/hecks/projections/site/cms_editor/pickers.rb +1 -1
- data/lib/hecks/projections/site/cms_editor/preview.rb +10 -4
- data/lib/hecks/projections/site/cms_editor/relations.rb +3 -2
- data/lib/hecks/projections/site/cms_editor/scale_checks.rb +32 -25
- data/lib/hecks/projections/site/cms_editor/scheduling.rb +1 -1
- data/lib/hecks/projections/site/cms_editor/schema.rb +31 -29
- data/lib/hecks/projections/site/cms_editor/setting.rb +34 -27
- data/lib/hecks/projections/site/cms_editor/targets.rb +9 -6
- data/lib/hecks/projections/site/cms_editor/templates/src/app.ts.tmpl +3 -3
- data/lib/hecks/projections/site/cms_editor/templates/src/commands.ts.tmpl +2 -2
- data/lib/hecks/projections/site/cms_editor/templates/src/host.ts.tmpl +1 -1
- data/lib/hecks/projections/site/cms_editor/templates/src/media/handler.ts.tmpl +4 -4
- data/lib/hecks/projections/site/cms_editor/templates/src/names.ts.tmpl +14 -14
- data/lib/hecks/projections/site/cms_editor/templates/src/schema.ts.tmpl +12 -12
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/pages.ts.tmpl +12 -12
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/pickers.ts.tmpl +1 -1
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/schedule.ts.tmpl +2 -2
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/shell.ts.tmpl +11 -11
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/world.ts.tmpl +3 -3
- data/lib/hecks/projections/site/cms_editor/theme_checks.rb +8 -2
- data/lib/hecks/projections/site/cms_editor.rb +19 -16
- data/lib/hecks/projections/site/edge/checks.rb +1 -1
- data/lib/hecks/projections/site/edge/function_checks.rb +1 -1
- data/lib/hecks/projections/site/edge/function_code.rb +4 -1
- data/lib/hecks/projections/site/edge/pattern.rb +4 -1
- data/lib/hecks/projections/site/edge/route_checks.rb +2 -2
- data/lib/hecks/projections/site/edge/rules.rb +1 -1
- data/lib/hecks/projections/site/edge.rb +14 -8
- data/lib/hecks/projections/site/live_edge.rb +2 -2
- data/lib/hecks/projections/site/payload/fields/definitions.rb +1 -1
- data/lib/hecks/projections/site/payload/lifecycle/text.rb +1 -1
- data/lib/hecks/projections/site/payload/model.rb +16 -12
- data/lib/hecks/projections/site/payload/specs.rb +4 -2
- data/lib/hecks/projections/site/payload_driver.rb +9 -8
- data/lib/hecks/projections/site/probe.rb +12 -3
- data/lib/hecks/projections/site/regions.rb +9 -1
- data/lib/hecks/projections/site/role_probe.rb +7 -7
- data/lib/hecks/projections/site/root_rows.rb +4 -4
- data/lib/hecks/projections/site/routes_ts/listings.rb +13 -6
- data/lib/hecks/projections/site/routes_ts.rb +6 -3
- data/lib/hecks/projections/site/site_admin.rb +1 -1
- data/lib/hecks/projections/site/site_cdn.rb +3 -2
- data/lib/hecks/projections/site/site_host.rb +1 -1
- data/lib/hecks/projections/site/site_root.rb +4 -4
- data/lib/hecks/projections/site/table/checks.rb +4 -1
- data/lib/hecks/projections/site/table/navigation_checks.rb +2 -2
- data/lib/hecks/projections/site/table/row_builder.rb +7 -7
- data/lib/hecks/projections/site/table/row_defaults.rb +1 -1
- data/lib/hecks/projections/site/table.rb +32 -26
- data/lib/hecks/projections/statements/actions.rb +4 -1
- data/lib/hecks/projections/statements/relationships.rb +15 -2
- data/lib/hecks/projections/statements.rb +2 -2
- data/lib/hecks/projections/undo.rb +46 -0
- data/lib/hecks/projections/vocabulary.rb +6 -6
- data/lib/hecks/projections/world_skeleton.rb +68 -0
- data/lib/hecks/projections.rb +7 -0
- data/lib/hecks/projector/cli_audience.rb +12 -9
- data/lib/hecks/projector/cli_projector/claims.rb +19 -8
- data/lib/hecks/projector/cli_projector/command_help.rb +6 -1
- data/lib/hecks/projector/cli_projector/command_specs.rb +20 -6
- data/lib/hecks/projector/cli_projector/completion.rb +87 -0
- data/lib/hecks/projector/cli_projector/everyday.rb +40 -0
- data/lib/hecks/projector/cli_projector/json_listing.rb +77 -0
- data/lib/hecks/projector/cli_projector/listing.rb +20 -8
- data/lib/hecks/projector/cli_projector/modules.rb +39 -0
- data/lib/hecks/projector/cli_projector/option_specs.rb +1 -1
- data/lib/hecks/projector/cli_projector/query_specs.rb +2 -1
- data/lib/hecks/projector/cli_projector/renames.rb +58 -0
- data/lib/hecks/projector/cli_projector/search.rb +74 -0
- data/lib/hecks/projector/cli_projector/usage.rb +33 -12
- data/lib/hecks/projector/cli_projector.rb +26 -3
- data/lib/hecks/projector/docs_projector/behaviour.rb +5 -4
- data/lib/hecks/projector/docs_projector/closing.rb +3 -3
- data/lib/hecks/projector/docs_projector.rb +12 -7
- data/lib/hecks/projector/exporter/adapters.rb +15 -2
- data/lib/hecks/projector/exporter/bindings.rb +39 -134
- data/lib/hecks/projector/exporter/plugins.rb +55 -0
- data/lib/hecks/projector/exporter.rb +2 -0
- data/lib/hecks/projector/narrate_projector/commands.rb +4 -3
- data/lib/hecks/projector/narrate_projector/reactions.rb +2 -2
- data/lib/hecks/projector/narrate_projector.rb +9 -8
- data/lib/hecks/projector/target.rb +8 -8
- data/lib/hecks/projector.rb +9 -9
- data/lib/hecks/quality_control/adapters/agent.rb +2 -1
- data/lib/hecks/quality_control/adapters/ci_head.adapter +8 -0
- data/lib/hecks/quality_control/adapters/github_checks.adapter +1 -1
- data/lib/hecks/quality_control/adapters/github_issues.adapter +1 -1
- data/lib/hecks/quality_control/adapters/github_issues.rb +6 -2
- data/lib/hecks/quality_control/adapters/image_roll.adapter +9 -0
- data/lib/hecks/quality_control/adapters/image_roll.rb +158 -0
- data/lib/hecks/quality_control/adapters/main_maintenance.adapter +8 -0
- data/lib/hecks/quality_control/adapters/main_maintenance.rb +275 -0
- data/lib/hecks/quality_control/adapters/pump_tools.adapter +7 -0
- data/lib/hecks/quality_control/adapters/pump_tools.rb +18 -0
- data/lib/hecks/quality_control/adapters/push_due_reader.adapter +8 -0
- data/lib/hecks/quality_control/adapters/push_due_reader.rb +99 -0
- data/lib/hecks/quality_control/adapters/qa_auto_mint.rb +183 -0
- data/lib/hecks/quality_control/adapters/qa_era_mint.rb +94 -0
- data/lib/hecks/quality_control/adapters/qa_quiesce_marker.rb +126 -0
- data/lib/hecks/quality_control/adapters/run_keys.rb +36 -0
- data/lib/hecks/quality_control/adapters/run_reader.adapter +7 -0
- data/lib/hecks/quality_control/adapters/run_reader.rb +188 -0
- data/lib/hecks/quality_control/adapters/target_tools.rb +9 -3
- data/lib/hecks/quality_control/cli/child.rb +2 -0
- data/lib/hecks/quality_control/cli/qa_discover_external_domains/scanning.rb +1 -1
- data/lib/hecks/quality_control/cli/qa_domain_novelty/verdict.rb +1 -1
- data/lib/hecks/quality_control/cli/qa_domain_novelty.rb +3 -3
- data/lib/hecks/quality_control/cli/qa_event_pump/attached.rb +67 -0
- data/lib/hecks/quality_control/cli/qa_event_pump/consumer.rb +103 -0
- data/lib/hecks/quality_control/cli/qa_event_pump/cursors.rb +60 -0
- data/lib/hecks/quality_control/cli/qa_event_pump/reactions.rb +46 -0
- data/lib/hecks/quality_control/cli/qa_event_pump/reader.rb +30 -0
- data/lib/hecks/quality_control/cli/qa_event_pump.rb +76 -0
- data/lib/hecks/quality_control/cli/qa_fix_main_bug/publishing.rb +79 -0
- data/lib/hecks/quality_control/cli/qa_fix_main_bug.rb +167 -0
- data/lib/hecks/quality_control/cli/qa_generated_domains/evaluation.rb +13 -3
- data/lib/hecks/quality_control/cli/qa_generated_domains/promotion.rb +7 -4
- data/lib/hecks/quality_control/cli/qa_generated_domains.rb +4 -1
- data/lib/hecks/quality_control/cli/qa_log_bug/proof.rb +16 -3
- data/lib/hecks/quality_control/cli/qa_main_gate/gate_run.rb +52 -0
- data/lib/hecks/quality_control/cli/qa_main_gate/logging.rb +70 -0
- data/lib/hecks/quality_control/cli/qa_main_gate/shell.rb +47 -0
- data/lib/hecks/quality_control/cli/qa_main_gate/sweeping.rb +59 -0
- data/lib/hecks/quality_control/cli/qa_main_gate/worktree.rb +62 -0
- data/lib/hecks/quality_control/cli/qa_main_gate.rb +135 -0
- data/lib/hecks/quality_control/cli/qa_mine_combinations/booting.rb +4 -2
- data/lib/hecks/quality_control/cli/qa_mutate.rb +161 -0
- data/lib/hecks/quality_control/cli/qa_postgres_role.rb +1 -1
- data/lib/hecks/quality_control/cli/qa_pr_check/checking.rb +2 -3
- data/lib/hecks/quality_control/cli/qa_push_nudge.rb +48 -0
- data/lib/hecks/quality_control/cli/qa_seed_angles/seed.rb +1 -1
- data/lib/hecks/quality_control/cli/qa_sweep/all_mode/report.rb +4 -1
- data/lib/hecks/quality_control/cli/qa_sweep/parsing.rb +2 -1
- data/lib/hecks/quality_control/cli/qa_sweep/seat.rb +1 -1
- data/lib/hecks/quality_control/cli/qa_sweep.rb +4 -1
- data/lib/hecks/quality_control/cli/qa_tick/event_pump_step.rb +34 -0
- data/lib/hecks/quality_control/cli/qa_tick/output.rb +43 -26
- data/lib/hecks/quality_control/cli/qa_tick/preflight.rb +12 -3
- data/lib/hecks/quality_control/cli/qa_tick/push_nudge_step.rb +28 -0
- data/lib/hecks/quality_control/cli/qa_tick/quiesce_guard.rb +51 -0
- data/lib/hecks/quality_control/cli/qa_tick.rb +52 -8
- data/lib/hecks/quality_control/quality_control.behaviors +65 -0
- data/lib/hecks/quality_control/quality_control.bluebook +1224 -103
- data/lib/hecks/quality_control/quality_control.hecksagon +14 -3
- data/lib/hecks/quality_control/quality_control.ports.hecksagon +141 -2
- data/lib/hecks/query_ir/rules.rb +9 -9
- data/lib/hecks/query_ir/touchpoints.rb +10 -9
- data/lib/hecks/query_ir.rb +3 -3
- data/lib/hecks/query_specification/common/comparison.rb +1 -1
- data/lib/hecks/query_specification/common/dsl.rb +3 -4
- data/lib/hecks/query_specification/hop_path.rb +7 -4
- data/lib/hecks/rails_app/bluebook/rails_app.bluebook +60 -0
- data/lib/hecks/rails_app/bluebook/rails_app.hecksagon +4 -0
- data/lib/hecks/refused.rb +125 -0
- data/lib/hecks/release/approval.rb +15 -5
- data/lib/hecks/release/conditions.rb +46 -0
- data/lib/hecks/release/gem_pin.rb +7 -3
- data/lib/hecks/release/lane.rb +1 -1
- data/lib/hecks/release/runner/clean_tree.rb +4 -1
- data/lib/hecks/release/runner/console.rb +2 -1
- data/lib/hecks/release/runner/gem_publisher.rb +4 -4
- data/lib/hecks/release/version_commit.rb +48 -0
- data/lib/hecks/router/aggregate_reader.rb +3 -4
- data/lib/hecks/router/namespace_installer.rb +31 -17
- data/lib/hecks/router.rb +2 -10
- data/lib/hecks/runtime/adapter_lookup.rb +30 -7
- data/lib/hecks/runtime/adapter_namespace.rb +21 -0
- data/lib/hecks/runtime/asking.rb +30 -0
- data/lib/hecks/runtime/caller.rb +22 -0
- data/lib/hecks/runtime/capability_graph.rb +2 -1
- data/lib/hecks/runtime/collecting.rb +86 -0
- data/lib/hecks/runtime/command_interpreter/argument_gate.rb +13 -8
- data/lib/hecks/runtime/command_interpreter/delegation.rb +46 -17
- data/lib/hecks/runtime/command_interpreter/hydration.rb +22 -25
- data/lib/hecks/runtime/command_interpreter/list_mutations.rb +2 -1
- data/lib/hecks/runtime/command_interpreter/persisting.rb +23 -14
- data/lib/hecks/runtime/command_interpreter.rb +19 -12
- data/lib/hecks/runtime/command_rules/admissibility.rb +17 -12
- data/lib/hecks/runtime/command_rules/authorization.rb +8 -6
- data/lib/hecks/runtime/command_rules/emission.rb +0 -1
- data/lib/hecks/runtime/command_rules/guard_state.rb +10 -10
- data/lib/hecks/runtime/command_rules/invariants.rb +3 -1
- data/lib/hecks/runtime/command_rules/missing_reference.rb +25 -0
- data/lib/hecks/runtime/command_rules/one_of_references.rb +44 -0
- data/lib/hecks/runtime/command_rules/references.rb +14 -14
- data/lib/hecks/runtime/command_rules.rb +2 -0
- data/lib/hecks/runtime/declarations.rb +66 -0
- data/lib/hecks/runtime/dependency_planning/analyzer.rb +2 -1
- data/lib/hecks/runtime/dependency_planning/expression_reads.rb +3 -3
- data/lib/hecks/runtime/dependency_planning/mutation_analysis.rb +2 -1
- data/lib/hecks/runtime/dispatch_claim.rb +72 -0
- data/lib/hecks/runtime/dispatch_context.rb +41 -0
- data/lib/hecks/runtime/dispatcher/planning.rb +24 -0
- data/lib/hecks/runtime/dispatcher/queries.rb +17 -0
- data/lib/hecks/runtime/dispatcher/routing.rb +40 -12
- data/lib/hecks/runtime/dispatcher/validating.rb +73 -0
- data/lib/hecks/runtime/dispatcher/value_routing.rb +54 -0
- data/lib/hecks/runtime/dispatcher.rb +46 -24
- data/lib/hecks/runtime/dry_run_plan.rb +60 -0
- data/lib/hecks/runtime/entity_element/location.rb +15 -8
- data/lib/hecks/runtime/entity_element/mutations.rb +4 -2
- data/lib/hecks/runtime/entity_interpreter/enforcement.rb +1 -1
- data/lib/hecks/runtime/entity_interpreter/locating.rb +6 -1
- data/lib/hecks/runtime/entity_interpreter/saving.rb +5 -5
- data/lib/hecks/runtime/entity_interpreter.rb +11 -5
- data/lib/hecks/runtime/errors.rb +56 -17
- data/lib/hecks/runtime/identity.rb +9 -0
- data/lib/hecks/runtime/instance.rb +3 -3
- data/lib/hecks/runtime/interpreting.rb +63 -6
- data/lib/hecks/runtime/invocation_builder.rb +7 -5
- data/lib/hecks/runtime/invocation_facts.rb +13 -8
- data/lib/hecks/runtime/loader/boot_steps.rb +3 -2
- data/lib/hecks/runtime/loader/described.rb +4 -0
- data/lib/hecks/runtime/loader.rb +52 -62
- data/lib/hecks/runtime/outbox/consumer_runner.rb +9 -1
- data/lib/hecks/runtime/outbox/redrive.rb +105 -0
- data/lib/hecks/runtime/outbox/relay.rb +36 -43
- data/lib/hecks/runtime/outbox/runnable.rb +62 -0
- data/lib/hecks/runtime/outbox.rb +21 -3
- data/lib/hecks/runtime/policy_interpreter/arguments.rb +7 -1
- data/lib/hecks/runtime/policy_interpreter/fan_out.rb +26 -37
- data/lib/hecks/runtime/policy_interpreter.rb +25 -9
- data/lib/hecks/runtime/port_operation_interpreter.rb +47 -19
- data/lib/hecks/runtime/query_interpreter/arguments.rb +6 -5
- data/lib/hecks/runtime/query_interpreter/outside_answer.rb +1 -1
- data/lib/hecks/runtime/query_interpreter/paging.rb +42 -0
- data/lib/hecks/runtime/query_interpreter/plugin_answer.rb +54 -0
- data/lib/hecks/runtime/query_interpreter/store_specification.rb +32 -0
- data/lib/hecks/runtime/query_interpreter.rb +16 -9
- data/lib/hecks/runtime/reaction_invocation/target_resolution.rb +10 -4
- data/lib/hecks/runtime/reaction_invocation.rb +5 -3
- data/lib/hecks/runtime/read_model_interpreter/head_ordering.rb +1 -1
- data/lib/hecks/runtime/read_model_interpreter/reductions.rb +23 -15
- data/lib/hecks/runtime/read_model_interpreter/shaping.rb +18 -7
- data/lib/hecks/runtime/read_model_interpreter.rb +5 -2
- data/lib/hecks/runtime/reference_hop.rb +3 -2
- data/lib/hecks/runtime/refusal_wording.rb +27 -7
- data/lib/hecks/runtime/registry/adapter_capabilities.rb +43 -0
- data/lib/hecks/runtime/registry/ask_verification.rb +15 -4
- data/lib/hecks/runtime/registry/{chapter_verification.rb → bluebook_verification.rb} +23 -23
- data/lib/hecks/runtime/registry/context_maps.rb +26 -0
- data/lib/hecks/runtime/registry/declarations.rb +38 -6
- data/lib/hecks/runtime/registry/durability_warnings.rb +18 -2
- data/lib/hecks/runtime/registry/hecksagon_rules.rb +42 -25
- data/lib/hecks/runtime/registry/plugin_query_verification.rb +45 -0
- data/lib/hecks/runtime/registry/providers.rb +34 -99
- data/lib/hecks/runtime/registry/query_verification.rb +30 -21
- data/lib/hecks/runtime/registry/runtime_state.rb +14 -3
- data/lib/hecks/runtime/registry/saga_persistence.rb +7 -16
- data/lib/hecks/runtime/registry/verification.rb +9 -5
- data/lib/hecks/runtime/registry/world_defaults.rb +31 -6
- data/lib/hecks/runtime/registry/world_names.rb +99 -0
- data/lib/hecks/runtime/registry.rb +84 -53
- data/lib/hecks/runtime/row_sweep.rb +104 -0
- data/lib/hecks/runtime/saga_interpreter/compensation.rb +15 -4
- data/lib/hecks/runtime/saga_interpreter/correlation.rb +29 -3
- data/lib/hecks/runtime/saga_interpreter/delivery.rb +13 -11
- data/lib/hecks/runtime/saga_interpreter/lifecycle.rb +1 -1
- data/lib/hecks/runtime/saga_interpreter/redrive.rb +141 -0
- data/lib/hecks/runtime/saga_interpreter/transitions.rb +35 -6
- data/lib/hecks/runtime/saga_interpreter.rb +82 -6
- data/lib/hecks/runtime/saga_pending_dispatch.rb +4 -2
- data/lib/hecks/runtime/scalar_field.rb +73 -0
- data/lib/hecks/runtime/tenant_scope.rb +2 -1
- data/lib/hecks/runtime/validation.rb +24 -0
- data/lib/hecks/runtime/value/admission.rb +11 -11
- data/lib/hecks/runtime/value/coercion.rb +43 -15
- data/lib/hecks/runtime/value/field_checks.rb +13 -13
- data/lib/hecks/runtime/value/invariant_violation.rb +4 -1
- data/lib/hecks/runtime/value/references.rb +7 -4
- data/lib/hecks/runtime/value/validation.rb +7 -4
- data/lib/hecks/runtime/wasm_plugin.rb +98 -0
- data/lib/hecks/runtime.rb +14 -14
- data/lib/hecks/rust_build/command_schemas.rb +27 -0
- data/lib/hecks/rust_build/conformance/surface.rb +50 -7
- data/lib/hecks/rust_build/conformance.rb +41 -8
- data/lib/hecks/rust_build/conformance_fuzz.rb +2 -2
- data/lib/hecks/rust_build/domain_name.rb +4 -1
- data/lib/hecks/rust_build/host.rb +15 -9
- data/lib/hecks/rust_build/native_build.rb +2 -1
- data/lib/hecks/rust_build/parser.rb +25 -0
- data/lib/hecks/rust_build/prebuilt_binary.rb +25 -5
- data/lib/hecks/rust_build/project_rust/codegen_run.rb +51 -17
- data/lib/hecks/rust_build/project_rust/declared.rb +80 -0
- data/lib/hecks/rust_build/project_rust/domain_loader.rb +15 -2
- data/lib/hecks/rust_build/project_rust/features_header.rb +22 -0
- data/lib/hecks/rust_build/project_rust/manifest.rb +96 -0
- data/lib/hecks/rust_build/project_rust/meta.rb +36 -0
- data/lib/hecks/rust_build/project_rust/root_mod/notes.rb +1 -1
- data/lib/hecks/rust_build/project_rust/target_ir.rb +12 -3
- data/lib/hecks/rust_build/project_rust.rb +31 -73
- data/lib/hecks/rust_build/target_dir.rb +40 -0
- data/lib/hecks/rust_build/wasm.rb +46 -17
- data/lib/hecks/rust_build/wasm_browser.rb +8 -2
- data/lib/hecks/rust_build/write_if_changed.rb +19 -4
- data/lib/hecks/rust_build.rb +31 -3
- data/lib/hecks/rust_format.rb +32 -2
- data/lib/hecks/secrets.rb +111 -0
- data/lib/hecks/sentence_form.rb +212 -0
- data/lib/hecks/served_options.rb +2 -1
- data/lib/hecks/shape_ledger/edge_hygiene.rb +170 -0
- data/lib/hecks/shape_ledger/scope.rb +51 -0
- data/lib/hecks/shape_ledger/status.rb +89 -0
- data/lib/hecks/shape_ledger.rb +194 -0
- data/lib/hecks/site/bluebook/site.bluebook +40 -62
- data/lib/hecks/site/bluebook/site.ports.hecksagon +1 -1
- data/lib/hecks/sme/bluebook/sme.bluebook +7 -1
- data/lib/hecks/storehouse/audit.rb +38 -5
- data/lib/hecks/storehouse/bluebook_words.rb +35 -0
- data/lib/hecks/storehouse/callers.rb +8 -0
- data/lib/hecks/storehouse/dispatching.rb +20 -6
- data/lib/hecks/storehouse/identity.rb +41 -0
- data/lib/hecks/storehouse/introspection.rb +4 -4
- data/lib/hecks/storehouse/querying.rb +9 -9
- data/lib/hecks/storehouse.rb +38 -10
- data/lib/hecks/switches.rb +21 -0
- data/lib/hecks/tenancy/bluebook/tenancy.bluebook +5 -9
- data/lib/hecks/text.rb +57 -0
- data/lib/hecks/tickets/bluebook/adapters/work_areas.adapter +7 -0
- data/lib/hecks/tickets/bluebook/adapters/work_limits.adapter +7 -0
- data/lib/hecks/tickets/bluebook/tickets.behaviors +70 -0
- data/lib/hecks/tickets/bluebook/tickets.bluebook +350 -76
- data/lib/hecks/tickets/bluebook/tickets.hecksagon +8 -1
- data/lib/hecks/tickets/bluebook/tickets.ports.hecksagon +18 -1
- data/lib/hecks/tools/argument_gate_matrix/dispatching.rb +1 -1
- data/lib/hecks/tools/argument_gate_matrix/rows.rb +7 -7
- data/lib/hecks/tools/argument_gate_matrix/violations.rb +1 -1
- data/lib/hecks/tools/argument_gate_matrix.rb +3 -3
- data/lib/hecks/{cli → tools}/behaviors.rb +1 -1
- data/lib/hecks/{cli → tools}/canonicalise.rb +1 -1
- data/lib/hecks/{cli → tools}/check_engine_agreement.rb +1 -1
- data/lib/hecks/{cli → tools}/check_era.rb +7 -7
- data/lib/hecks/tools/ci_gate_decision.rb +5 -2
- data/lib/hecks/tools/ci_gates.rb +9 -3
- data/lib/hecks/tools/ci_workflows.rb +98 -0
- data/lib/hecks/tools/comment_style/caps_words.rb +11 -8
- data/lib/hecks/tools/comment_style/dictionary.rb +7 -2
- data/lib/hecks/tools/comment_style/fixing.rb +10 -5
- data/lib/hecks/tools/comment_style/long_blocks.rb +1 -1
- data/lib/hecks/tools/comment_style/masking.rb +5 -5
- data/lib/hecks/tools/comment_style/run.rb +4 -1
- data/lib/hecks/tools/comment_style/source_file.rb +10 -3
- data/lib/hecks/tools/comment_style/structure_checks.rb +13 -4
- data/lib/hecks/tools/comment_style/text_checks.rb +1 -1
- data/lib/hecks/tools/comment_style.rb +25 -2
- data/lib/hecks/{cli → tools}/console.rb +12 -7
- data/lib/hecks/tools/deploy_recipe/generation.rb +1 -1
- data/lib/hecks/tools/deploy_recipe/targets.rb +1 -1
- data/lib/hecks/tools/deploy_recipe/worlds.rb +15 -9
- data/lib/hecks/tools/deploy_recipe.rb +17 -10
- data/lib/hecks/tools/deploy_recipe_lint/fixtures.rb +1 -1
- data/lib/hecks/tools/deploy_recipe_lint/makefile.rb +7 -1
- data/lib/hecks/{cli → tools}/deploy_template_diff.rb +1 -1
- data/lib/hecks/{cli → tools}/doc_coverage.rb +2 -2
- data/lib/hecks/{cli → tools}/document.rb +15 -10
- data/lib/hecks/{cli → tools}/domain_stub.rb +1 -1
- data/lib/hecks/{cli → tools}/domain_writer.rb +5 -2
- data/lib/hecks/tools/downstream_model_check.rb +122 -0
- data/lib/hecks/tools/drop_implicit_append_fields.rb +4 -1
- data/lib/hecks/tools/era_projection_backfill.rb +11 -6
- data/lib/hecks/tools/era_reattest.rb +11 -6
- data/lib/hecks/tools/evolve_run.rb +2 -2
- data/lib/hecks/tools/expected_era.rb +139 -0
- data/lib/hecks/{cli → tools}/expression_projection.rb +3 -3
- data/lib/hecks/{cli → tools}/follow.rb +4 -4
- data/lib/hecks/tools/fuzz_sweep/findings.rb +18 -2
- data/lib/hecks/tools/fuzz_sweep/options.rb +45 -6
- data/lib/hecks/tools/fuzz_sweep/pool.rb +12 -1
- data/lib/hecks/tools/fuzz_sweep/shards.rb +155 -0
- data/lib/hecks/tools/fuzz_sweep.rb +28 -10
- data/lib/hecks/tools/gate.rb +65 -34
- data/lib/hecks/tools/gate_memo/local_files.rb +33 -0
- data/lib/hecks/tools/gate_memo/rehearsal.rb +54 -0
- data/lib/hecks/tools/gate_memo/ruby_files.rb +152 -0
- data/lib/hecks/tools/gate_memo/session.rb +114 -0
- data/lib/hecks/tools/gate_memo/standalone.rb +67 -0
- data/lib/hecks/tools/gate_memo.rb +196 -0
- data/lib/hecks/tools/gate_report.rb +56 -0
- data/lib/hecks/{cli → tools}/history.rb +5 -2
- data/lib/hecks/tools/hoist_local_givens/rewriting.rb +3 -2
- data/lib/hecks/tools/hoist_local_givens.rb +2 -4
- data/lib/hecks/{cli → tools}/interview_agent.rb +1 -1
- data/lib/hecks/{cli → tools}/interview_aggregate.rb +1 -1
- data/lib/hecks/{cli → tools}/interview_draft.rb +9 -2
- data/lib/hecks/{cli → tools}/interview_record.rb +8 -4
- data/lib/hecks/{cli → tools}/interview_run.rb +9 -7
- data/lib/hecks/{cli → tools}/interview_session/agent_state.rb +1 -1
- data/lib/hecks/{cli → tools}/interview_session/catalog.rb +1 -1
- data/lib/hecks/{cli → tools}/interview_session/manual_findings.rb +1 -1
- data/lib/hecks/{cli → tools}/interview_session/proposals.rb +6 -3
- data/lib/hecks/{cli → tools}/interview_session.rb +5 -4
- data/lib/hecks/tools/interview_shape/lifecycle.rb +92 -0
- data/lib/hecks/{cli → tools}/interview_shape.rb +20 -74
- data/lib/hecks/{cli → tools}/ir.rb +1 -1
- data/lib/hecks/tools/lanes/files.rb +1 -1
- data/lib/hecks/tools/lanes/prebuilt_workflow.rb +1 -1
- data/lib/hecks/tools/lanes/rows.rb +9 -3
- data/lib/hecks/tools/lanes/ruleset.rb +1 -1
- data/lib/hecks/tools/lanes/watch_workflow.rb +7 -9
- data/lib/hecks/tools/lanes/workflow.rb +2 -2
- data/lib/hecks/tools/lanes.rb +2 -2
- data/lib/hecks/{cli → tools}/mcp.rb +1 -1
- data/lib/hecks/tools/model_check/examination.rb +142 -0
- data/lib/hecks/{cli → tools}/model_check/sources.rb +5 -3
- data/lib/hecks/{cli → tools}/model_check.rb +2 -2
- data/lib/hecks/tools/move_disables/reasons.rb +35 -0
- data/lib/hecks/tools/move_disables.rb +211 -0
- data/lib/hecks/tools/move_former_name.rb +105 -0
- data/lib/hecks/tools/oidc_manifests.rb +16 -19
- data/lib/hecks/{cli → tools}/pattern_cases.rb +1 -1
- data/lib/hecks/tools/persistence_notice.rb +45 -0
- data/lib/hecks/{cli → tools}/present.rb +2 -2
- data/lib/hecks/{cli → tools}/project_cli/launcher_source.rb +39 -27
- data/lib/hecks/{cli → tools}/project_cli/templates.rb +42 -14
- data/lib/hecks/{cli → tools}/project_cli.rb +22 -20
- data/lib/hecks/tools/project_diagrams.rb +59 -0
- data/lib/hecks/{cli → tools}/project_glossary.rb +16 -16
- data/lib/hecks/{cli → tools}/reference.rb +3 -3
- data/lib/hecks/{cli → tools}/refresh_projections.rb +17 -12
- data/lib/hecks/{cli → tools}/refresh_rspec_runtime_baseline.rb +1 -1
- data/lib/hecks/tools/regeneration_run/pool.rb +148 -0
- data/lib/hecks/tools/regeneration_run/scratch.rb +76 -0
- data/lib/hecks/tools/regeneration_run.rb +134 -107
- data/lib/hecks/{cli → tools}/release.rb +1 -1
- data/lib/hecks/{cli → tools}/release_gem.rb +1 -1
- data/lib/hecks/tools/release_only.rb +5 -2
- data/lib/hecks/{cli → tools}/rspec_io_parallel_files.rb +1 -1
- data/lib/hecks/{cli → tools}/rspec_shard_files.rb +1 -1
- data/lib/hecks/{cli → tools}/run.rb +36 -7
- data/lib/hecks/{cli → tools}/run_expectations.rb +1 -1
- data/lib/hecks/tools/rust_comment_style/caps_words.rb +5 -5
- data/lib/hecks/tools/rust_comment_style/fixing.rb +1 -1
- data/lib/hecks/tools/rust_comment_style/masking.rb +7 -4
- data/lib/hecks/tools/rust_comment_style/source_file.rb +7 -2
- data/lib/hecks/tools/rust_comment_style/text_checks.rb +1 -1
- data/lib/hecks/tools/rust_comment_style/tokenizer.rb +4 -1
- data/lib/hecks/{cli → tools}/seed_semantics_corpus.rb +18 -8
- data/lib/hecks/{cli → tools}/serve_query_ir_mcp.rb +1 -1
- data/lib/hecks/{cli → tools}/shape.rb +6 -6
- data/lib/hecks/tools/site_routes/editor_files.rb +16 -13
- data/lib/hecks/tools/site_routes/root_files.rb +14 -13
- data/lib/hecks/tools/site_routes/templates.rb +5 -4
- data/lib/hecks/tools/site_routes.rb +18 -15
- data/lib/hecks/{cli → tools}/smoke_http/checks.rb +1 -1
- data/lib/hecks/{cli → tools}/smoke_http/settings.rb +1 -1
- data/lib/hecks/{cli → tools}/smoke_http/transport.rb +4 -3
- data/lib/hecks/{cli → tools}/smoke_http.rb +1 -1
- data/lib/hecks/{cli → tools}/smoke_test.rb +1 -1
- data/lib/hecks/{cli → tools}/spec_example.rb +1 -1
- data/lib/hecks/tools/stage_progress.rb +54 -0
- data/lib/hecks/{cli → tools}/statements.rb +8 -8
- data/lib/hecks/tools/stores.rb +123 -0
- data/lib/hecks/{cli → tools}/stress_concurrency_specs/reporting.rb +5 -2
- data/lib/hecks/{cli → tools}/stress_concurrency_specs/usage.rb +4 -4
- data/lib/hecks/{cli → tools}/stress_concurrency_specs.rb +3 -3
- data/lib/hecks/tools/tail_merge.rb +3 -3
- data/lib/hecks/tools/tenant_provisioning.rb +29 -6
- data/lib/hecks/tools/tools_doc.rb +22 -20
- data/lib/hecks/tools/translation_audit/domain_loading.rb +3 -3
- data/lib/hecks/tools/translation_audit/edges.rb +21 -16
- data/lib/hecks/tools/translation_audit/verdicts.rb +1 -1
- data/lib/hecks/tools/translation_audit.rb +16 -5
- data/lib/hecks/tools/translation_scaffold/reporting.rb +4 -1
- data/lib/hecks/tools/translation_scaffold.rb +5 -15
- data/lib/hecks/tools.rb +4 -1
- data/lib/hecks/vendoring/git_source.rb +7 -4
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks/vocabulary.rb +147 -10
- data/lib/hecks/workflow_pagination/judge.rb +48 -0
- data/lib/hecks/workflow_pagination/statement.rb +50 -0
- data/lib/hecks/workflow_pagination.rb +58 -0
- data/lib/hecks.rb +39 -18
- data/lib/rubocop/cop/hecks/bare_event_and_query_names.rb +58 -0
- data/lib/rubocop/cop/hecks/correlates_by_aggregate.rb +39 -0
- data/lib/rubocop/cop/hecks/creating_command_sets.rb +75 -0
- data/lib/rubocop/cop/hecks/inline_regexp.rb +29 -0
- data/lib/rubocop/cop/hecks/invariant_expression_language.rb +47 -0
- data/lib/rubocop/cop/hecks/lifecycle_needs_attribute.rb +58 -0
- data/lib/rubocop/cop/hecks/no_id_string_attribute.rb +49 -0
- data/lib/rubocop/cop/hecks/no_transport_in_runtime.rb +78 -0
- data/lib/rubocop/cop/hecks/one_terminal_mark_per_lifecycle.rb +47 -0
- data/lib/rubocop/cop/hecks/rails_model_rules.rb +46 -0
- data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +1 -1
- data/lib/rubocop/cop/hecks/train_wreck.rb +69 -0
- data/lib/rubocop/cop/hecks/unnamed_pipeline.rb +51 -0
- data/lib/rubocop/cop/hecks/value_object_shorthand.rb +56 -0
- data/prebuilt/13c04bdc5970295b310f0a980d397ba722232c8da99cc1a0cdb383249921e871.marshal +0 -0
- data/prebuilt/e3857b77f47d9ecc1787b2e1f5136364f3cb3444ef4aea5479f3b98167affea2.marshal +0 -0
- data/qa/settings.yml +32 -0
- data/rust/.gitignore +0 -6
- data/rust/Cargo.lock +8 -1
- data/rust/Cargo.toml +8 -8
- data/rust/build/Cargo.toml +1 -1
- data/rust/build/src/binding_pass.rs +68 -19
- data/rust/build/src/cargo_sync.rs +1 -1
- data/rust/build/src/lineage_pass.rs +4 -71
- data/rust/build/src/pipeline.rs +104 -73
- data/rust/build/src/resolve.rs +86 -29
- data/rust/build/src/seams_pass.rs +117 -99
- data/rust/build/src/world_pass.rs +21 -12
- data/rust/codegen/Cargo.toml +1 -1
- data/rust/codegen/src/domain_generator.rs +27 -2
- data/rust/codegen/src/expr_emitter.rs +6 -0
- data/rust/codegen/src/json_codec.rs +230 -33
- data/rust/codegen/src/main.rs +35 -34
- data/rust/codegen/src/manifest.rs +1 -1
- data/rust/codegen/src/naming.rs +5 -5
- data/rust/codegen/src/one_of_references.rs +125 -0
- data/rust/codegen/src/presenters.rs +3 -3
- data/rust/codegen/src/queries.rs +76 -9
- data/rust/codegen/src/reactions.rs +19 -14
- data/rust/codegen/src/read_models.rs +19 -14
- data/rust/codegen/src/registry.rs +110 -20
- data/rust/codegen/src/sidecars.rs +1 -1
- data/rust/expr/Cargo.lock +54 -0
- data/rust/expr/Cargo.toml +52 -0
- data/rust/{src/kernel → expr/src}/attribute_shapes/composite.rs +45 -14
- data/rust/{src/kernel → expr/src}/attribute_shapes/list.rs +1 -1
- data/rust/{src/kernel → expr/src}/attribute_shapes/mod.rs +2 -2
- data/rust/{src/kernel → expr/src}/attribute_shapes/optional.rs +1 -1
- data/rust/{src/kernel → expr/src}/attribute_shapes/scalar.rs +3 -3
- data/rust/{src/kernel → expr/src}/expr.rs +30 -8
- data/rust/{src/kernel → expr/src}/expression_operators/arithmetic.rs +41 -18
- data/rust/{src/kernel → expr/src}/expression_operators/comparison.rs +54 -8
- data/rust/{src/kernel → expr/src}/expression_operators/enumeration.rs +73 -6
- data/rust/{src/kernel → expr/src}/expression_operators/logical.rs +2 -2
- data/rust/expr/src/expression_operators/membership.rs +33 -0
- data/rust/{src/kernel → expr/src}/expression_operators/mod.rs +2 -2
- data/rust/expr/src/expression_operators/pattern_match.rs +179 -0
- data/rust/{src/kernel → expr/src}/expression_operators/positional.rs +8 -6
- data/rust/{src/kernel → expr/src}/expression_operators/presence.rs +7 -6
- data/rust/{src/kernel → expr/src}/expression_operators/sign_test.rs +14 -4
- data/rust/{src/kernel → expr/src}/expression_operators/sized.rs +16 -9
- data/rust/{src/kernel → expr/src}/expression_operators/text.rs +30 -17
- data/rust/{src/kernel → expr/src}/expression_operators/to_string.rs +8 -6
- data/rust/expr/src/lib.rs +17 -0
- data/rust/expr/src/refusal.rs +62 -0
- data/rust/expr/src/rendering.rs +135 -0
- data/rust/{src/kernel → expr/src}/ruby_float.rs +7 -6
- data/rust/host/Cargo.lock +137 -123
- data/rust/host/Cargo.toml +8 -4
- data/rust/host/HECKS_RELEASE +1 -1
- data/rust/host/core/Cargo.toml +64 -0
- data/rust/host/core/clippy.toml +4 -0
- data/rust/host/core/src/arg_limits.rs +48 -0
- data/rust/host/core/src/clock.rs +146 -0
- data/rust/host/core/src/dispatch.rs +920 -0
- data/rust/host/core/src/expr_json.rs +1137 -0
- data/rust/host/{src → core/src}/host.rs +13 -14
- data/rust/host/{src → core/src}/ir.rs +227 -18
- data/rust/host/core/src/journal.rs +126 -0
- data/rust/host/{src → core/src}/lambda_client.rs +27 -96
- data/rust/host/core/src/lib.rs +37 -0
- data/rust/host/{src → core/src}/needs.rs +66 -20
- data/rust/host/core/src/plugins.rs +269 -0
- data/rust/host/core/src/presentation.rs +471 -0
- data/rust/host/{src → core/src}/presentation_write.rs +75 -43
- data/rust/host/{src → core/src}/query_step.rs +54 -16
- data/rust/host/{src → core/src}/reference_validate.rs +6 -6
- data/rust/host/{src → core/src}/storage_shape.rs +2 -2
- data/rust/host/core/src/store/memory.rs +635 -0
- data/rust/host/core/src/store.rs +418 -0
- data/rust/host/{src → core/src}/ui_schema.rs +8 -8
- data/rust/host/core/src/validation.rs +129 -0
- data/rust/host/core/src/wasm_runner.rs +410 -0
- data/rust/host/fuzz/.gitignore +4 -0
- data/rust/host/fuzz/Cargo.lock +4479 -0
- data/rust/host/fuzz/Cargo.toml +42 -0
- data/rust/host/fuzz/README.md +55 -0
- data/rust/host/fuzz/fuzz_targets/postgres_equivalence.rs +24 -0
- data/rust/host/fuzz/src/backend.rs +273 -0
- data/rust/host/fuzz/src/equivalence.rs +195 -0
- data/rust/host/fuzz/src/footprint.rs +182 -0
- data/rust/host/fuzz/src/lib.rs +17 -0
- data/rust/host/fuzz/src/ops.rs +345 -0
- data/rust/host/fuzz/tests/smoke.rs +50 -0
- data/rust/host/src/api.rs +316 -105
- data/rust/host/src/auth/google_signin.rs +183 -0
- data/rust/host/src/auth.rs +527 -370
- data/rust/host/src/aws_lambda_invoker.rs +61 -0
- data/rust/host/src/bin/expr_harness.rs +9 -4
- data/rust/host/src/bin/ledger.rs +84 -0
- data/rust/host/src/bin/lineage_harness.rs +17 -21
- data/rust/host/src/bin/mint_harness.rs +12 -36
- data/rust/host/src/bin/validate_harness.rs +1 -6
- data/rust/host/src/boot.rs +466 -174
- data/rust/host/src/boundary_fuzz/ir.rs +10 -5
- data/rust/host/src/boundary_fuzz/server.rs +6 -5
- data/rust/host/src/checkout.rs +4 -1
- data/rust/host/src/commerce_ir.rs +126 -105
- data/rust/host/src/{dispatch.rs → dispatch_tests.rs} +367 -847
- data/rust/host/src/extension.rs +3 -4
- data/rust/host/src/hecksagon_boundary_tests.rs +149 -0
- data/rust/host/src/ledger.rs +683 -0
- data/rust/host/src/lib.rs +21 -28
- data/rust/host/src/payments/tests.rs +136 -38
- data/rust/host/src/payments.rs +36 -32
- data/rust/host/src/platform_signin/extension.rs +1 -1
- data/rust/host/src/platform_signin/mapping_tests.rs +12 -9
- data/rust/host/src/platform_signin/session.rs +7 -14
- data/rust/host/src/platform_signin/verify.rs +0 -2
- data/rust/host/src/presentation_tests.rs +382 -0
- data/rust/host/src/rate_limit.rs +18 -2
- data/rust/host/src/server.rs +222 -43
- data/rust/host/src/{journal.rs → store/journal_sql.rs} +21 -127
- data/rust/host/src/{mint.rs → store/mint.rs} +52 -18
- data/rust/host/src/store/postgres.rs +493 -0
- data/rust/host/src/store.rs +97 -0
- data/rust/host/src/store_conformance.rs +1265 -0
- data/rust/host/src/web/commerce.rs +10 -18
- data/rust/host/src/web/newsletter/cooldown.rs +107 -22
- data/rust/host/src/web/newsletter/naming.rs +104 -0
- data/rust/host/src/web/newsletter/tests.rs +53 -0
- data/rust/host/src/web/newsletter.rs +73 -50
- data/rust/host/src/web/newsletter_send.rs +12 -13
- data/rust/host/src/web/registration_receipt.rs +2 -2
- data/rust/host/src/web/registrations.rs +754 -375
- data/rust/host/src/web/resend_webhook.rs +3 -3
- data/rust/host/src/web/signin_tests.rs +131 -0
- data/rust/host/src/web.rs +1479 -149
- data/rust/lsp/Cargo.toml +1 -1
- data/rust/lsp/README.md +27 -27
- data/rust/lsp/src/diagnostics.rs +75 -15
- data/rust/lsp/src/main.rs +101 -5
- data/rust/parser/Cargo.toml +1 -1
- data/rust/parser/fuzz/Cargo.toml +2 -2
- data/rust/parser/fuzz/README.md +3 -3
- data/rust/parser/fuzz/fuzz_targets/{chapter.rs → bluebook.rs} +4 -4
- data/rust/parser/fuzz/fuzz_targets/resolve.rs +2 -2
- data/rust/parser/src/build/correlation_keys.rs +125 -0
- data/rust/parser/src/build/mod.rs +1 -0
- data/rust/parser/src/build/named_pattern.rs +22 -0
- data/rust/parser/src/build/references.rs +52 -0
- data/rust/parser/src/diag.rs +22 -1
- data/rust/parser/src/emit.rs +122 -6
- data/rust/parser/src/expr/ast_json.rs +10 -0
- data/rust/parser/src/expr/resolver.rs +49 -9
- data/rust/parser/src/ir.rs +66 -7
- data/rust/parser/src/keywords.rs +704 -419
- data/rust/parser/src/lex.rs +115 -87
- data/rust/parser/src/main.rs +182 -32
- data/rust/parser/src/parse/adapter.rs +28 -0
- data/rust/parser/src/parse/aggregate.rs +64 -35
- data/rust/parser/src/parse/audience.rs +65 -0
- data/rust/parser/src/parse/command.rs +114 -15
- data/rust/parser/src/parse/{chapter.rs → declaration.rs} +182 -54
- data/rust/parser/src/parse/domain_port.rs +66 -9
- data/rust/parser/src/parse/entity.rs +35 -32
- data/rust/parser/src/parse/file.rs +1 -1
- data/rust/parser/src/parse/hecksagon.rs +15 -5
- data/rust/parser/src/parse/mod.rs +85 -2
- data/rust/parser/src/parse/process_manager.rs +18 -11
- data/rust/parser/src/parse/query.rs +13 -3
- data/rust/parser/src/parse/read_model.rs +2 -1
- data/rust/parser/src/parse/world.rs +12 -8
- data/rust/parser/src/phrases.rs +1258 -0
- data/rust/parser/src/render_ruby.rs +549 -0
- data/rust/parser/src/sentence/changes.rs +437 -0
- data/rust/parser/src/sentence/command_step.rs +206 -0
- data/rust/parser/src/sentence/complete.rs +276 -0
- data/rust/parser/src/sentence/entity.rs +41 -0
- data/rust/parser/src/sentence/examples.rs +1012 -0
- data/rust/parser/src/sentence/format.rs +885 -0
- data/rust/parser/src/sentence/lower.rs +1366 -0
- data/rust/parser/src/sentence/mod.rs +444 -0
- data/rust/parser/src/sentence/names.rs +69 -0
- data/rust/parser/src/sentence/options.rs +154 -0
- data/rust/parser/src/sentence/pattern.rs +433 -0
- data/rust/parser/src/sentence/process.rs +200 -0
- data/rust/parser/src/sentence/queries.rs +431 -0
- data/rust/parser/src/sentence/report.rs +283 -0
- data/rust/parser/src/sentence/token.rs +106 -0
- data/rust/parser/src/sentence/values.rs +498 -0
- data/rust/src/exemplar/json.rs +17 -0
- data/rust/src/exemplar/queries.rs +1 -0
- data/rust/src/exemplar/reactions.rs +1 -1
- data/rust/src/exemplar/registry.rs +30 -0
- data/rust/src/kernel/cli.rs +239 -15
- data/rust/src/kernel/collecting.rs +174 -0
- data/rust/src/kernel/dispatch.rs +59 -29
- data/rust/src/kernel/json.rs +62 -0
- data/rust/src/kernel/mod.rs +7 -67
- data/rust/src/kernel/named_query.rs +225 -11
- data/rust/src/kernel/orchestrate.rs +241 -28
- data/rust/src/kernel/orchestrate_guard_tests.rs +15 -12
- data/rust/src/kernel/query_cursor.rs +251 -0
- data/rust/src/kernel/query_ordering.rs +6 -6
- data/rust/src/kernel/read_model.rs +162 -17
- data/rust/src/kernel/repository.rs +107 -17
- data/rust/src/kernel/vocab/mod.rs +4 -0
- data/rust/src/kernel/vocab/reducer.rs +109 -0
- data/rust/src/kernel/vocab/refusal_class.rs +124 -0
- data/rust/src/kernel/vocab/refusal_template.rs +2 -2
- data/rust/src/lib.rs +1 -1
- data/rust/web/Cargo.lock +47 -0
- data/rust/web/Cargo.toml +1 -1
- metadata +689 -161
- data/lib/hecks/chapters.rb +0 -147
- data/lib/hecks/cli/command_table.rb +0 -72
- data/lib/hecks/cli/launcher_words.rb +0 -168
- data/lib/hecks/cli/model_check/examination.rb +0 -93
- data/lib/hecks/cli/project_diagrams.rb +0 -59
- data/lib/hecks/cli/stores.rb +0 -55
- data/lib/hecks/cli.rb +0 -133
- data/lib/hecks/gate/stages.yml +0 -234
- data/lib/hecks/hecks/codebase.bluebook +0 -3241
- data/lib/hecks/hecks/custodian.bluebook +0 -904
- data/lib/hecks/hecks/governance.world +0 -7
- data/lib/hecks/hecks/rehearsal.bluebook +0 -471
- data/lib/hecks/hecks/tooling.bluebook +0 -1238
- data/lib/hecks/ports/persistence/plugins/era/expected_era.rb +0 -137
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/boot.rb +0 -43
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/domain_rename.rb +0 -91
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +0 -67
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/context.rb +0 -38
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +0 -100
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +0 -102
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/refusals.rb +0 -51
- data/lib/hecks/ports/persistence/postgres_dump/connection.rb +0 -59
- data/lib/hecks/ports/persistence/postgres_dump.rb +0 -100
- data/lib/hecks/projections/deploy/box/templates/tunnel-egress.yaml.tmpl +0 -11
- data/lib/hecks/projector/exporter/capability_values.rb +0 -55
- data/lib/hecks/quality_control/adapters/clearance_recorder.rb +0 -36
- data/lib/hecks/quality_control/adapters/github_ci_webhook.rb +0 -56
- data/lib/hecks/runtime/remote_dispatcher.rb +0 -176
- data/prebuilt/ad68f9e06c8068c2a4176b57abf3083d591d5d8980c2b3e897b97331f965b643.marshal +0 -0
- data/prebuilt/ee22ec8642c4006717ba2b80f46e5efb81de459eef9dfec043e838c0ad4fd720.marshal +0 -0
- data/prebuilt/verdicts-b69f5dcbba79c20daa0493476fa7c317004a30aa2bc3d3dc6a722ea70ae2b43b.json +0 -1
- data/rust/host/src/clock.rs +0 -8
- data/rust/host/src/expr_json.rs +0 -1983
- data/rust/host/src/presentation.rs +0 -842
- data/rust/host/src/wasm_runner.rs +0 -206
- data/rust/src/kernel/expression_operators/membership.rs +0 -20
- data/rust/src/kernel/expression_operators/pattern_match.rs +0 -108
- /data/lib/hecks/{ports/persistence/plugins/era → adapters/driven}/postgres_era/events.rb +0 -0
- /data/lib/hecks/{ports/persistence/plugins/era → adapters/driven}/postgres_era/head_writes.rb +0 -0
- /data/lib/hecks/{ports/persistence/plugins/era → adapters/driven}/postgres_era/lineage/base_sql.rb +0 -0
- /data/lib/hecks/{ports/persistence/plugins/era → adapters/driven}/postgres_era/lineage/field_cache.rb +0 -0
- /data/lib/hecks/{ports/persistence/plugins/era → adapters/driven}/postgres_era/lineage/head_compiler.rb +0 -0
- /data/lib/hecks/{ports/persistence/plugins/era → adapters/driven}/postgres_era/lineage/merge_writes.rb +0 -0
- /data/lib/hecks/{ports/persistence/plugins/era → adapters/driven}/postgres_era/lineage/resumable_backfill.rb +0 -0
- /data/lib/hecks/{ports/persistence/plugins/era → adapters/driven}/postgres_era/lineage/transform_installer.rb +0 -0
- /data/lib/hecks/{ports/persistence/plugins/era → adapters/driven}/postgres_era/lineage/transform_sql.rb +0 -0
- /data/lib/hecks/{ports/persistence/plugins/era → adapters/driven}/postgres_era/sagas.rb +0 -0
- /data/rust/host/{src → core/src}/field_hints.rs +0 -0
- /data/rust/host/{src → core/src}/log.rs +0 -0
- /data/rust/host/{src → core/src}/reference_transform.rs +0 -0
data/rust/host/src/web.rs
CHANGED
|
@@ -9,13 +9,14 @@ use crate::extension::{self, Ctx};
|
|
|
9
9
|
use crate::field_hints::{EMAIL_HINT, TEL_HINT, TEXTAREA_HINT, URL_HINT};
|
|
10
10
|
use crate::host::Host;
|
|
11
11
|
use crate::ir::ir;
|
|
12
|
-
use crate::journal::LineageConfig;
|
|
13
12
|
use crate::lambda_client::LambdaInvoker;
|
|
13
|
+
use crate::platform_signin::jti::JtiStore;
|
|
14
|
+
use crate::store::journal_sql::LineageConfig;
|
|
15
|
+
use crate::store::Store;
|
|
14
16
|
use serde_json::{json, Value};
|
|
15
17
|
use std::collections::HashMap;
|
|
16
18
|
use std::path::Path;
|
|
17
|
-
use
|
|
18
|
-
use tokio_postgres::Client;
|
|
19
|
+
use std::sync::OnceLock;
|
|
19
20
|
|
|
20
21
|
mod commerce;
|
|
21
22
|
mod newsletter;
|
|
@@ -23,9 +24,11 @@ mod newsletter_send;
|
|
|
23
24
|
mod registration_receipt;
|
|
24
25
|
mod registrations;
|
|
25
26
|
mod resend_webhook;
|
|
27
|
+
#[cfg(test)]
|
|
28
|
+
mod signin_tests;
|
|
26
29
|
|
|
27
30
|
pub use commerce::Commerce;
|
|
28
|
-
use registrations::payments_routes;
|
|
31
|
+
use registrations::{payments_routes, CheckoutRequest};
|
|
29
32
|
// payments.rs, its tests and the boot check reach these through `web::`.
|
|
30
33
|
pub use registrations::checkout_enabled;
|
|
31
34
|
#[cfg(test)]
|
|
@@ -73,7 +76,7 @@ pub async fn render(body: &Value, host: Host<'_>) -> Option<Value> {
|
|
|
73
76
|
raw_body: &raw_body,
|
|
74
77
|
cookies: &cookies,
|
|
75
78
|
domain_ir: ir(),
|
|
76
|
-
|
|
79
|
+
store: host.store,
|
|
77
80
|
wasm_path: host.wasm_path,
|
|
78
81
|
config: host.config,
|
|
79
82
|
invoker: host.invoker,
|
|
@@ -189,7 +192,7 @@ async fn route(
|
|
|
189
192
|
host: Host<'_>,
|
|
190
193
|
) -> Value {
|
|
191
194
|
let Host {
|
|
192
|
-
|
|
195
|
+
store, wasm_path, ..
|
|
193
196
|
} = host;
|
|
194
197
|
let domain_name = domain_ir.get("name").and_then(|v| v.as_str()).unwrap_or("");
|
|
195
198
|
let secret = session_secret();
|
|
@@ -259,7 +262,7 @@ async fn route(
|
|
|
259
262
|
};
|
|
260
263
|
|
|
261
264
|
if segments.len() == 2 {
|
|
262
|
-
return aggregate_index(domain_name, aggregate, &format,
|
|
265
|
+
return aggregate_index(domain_name, aggregate, &format, store, wasm_path).await;
|
|
263
266
|
}
|
|
264
267
|
|
|
265
268
|
if segments.len() == 3 {
|
|
@@ -276,6 +279,7 @@ async fn route(
|
|
|
276
279
|
method,
|
|
277
280
|
query,
|
|
278
281
|
raw_body,
|
|
282
|
+
crate::auth::caller(session.as_ref()),
|
|
279
283
|
host,
|
|
280
284
|
)
|
|
281
285
|
.await;
|
|
@@ -285,7 +289,7 @@ async fn route(
|
|
|
285
289
|
aggregate,
|
|
286
290
|
&verb_or_id,
|
|
287
291
|
&format,
|
|
288
|
-
|
|
292
|
+
store,
|
|
289
293
|
wasm_path,
|
|
290
294
|
)
|
|
291
295
|
.await;
|
|
@@ -309,10 +313,9 @@ async fn auth_route(
|
|
|
309
313
|
host: Host<'_>,
|
|
310
314
|
) -> Option<Value> {
|
|
311
315
|
let Host {
|
|
312
|
-
|
|
316
|
+
store,
|
|
313
317
|
wasm_path,
|
|
314
318
|
config,
|
|
315
|
-
invoker,
|
|
316
319
|
..
|
|
317
320
|
} = host;
|
|
318
321
|
let ctx = Ctx {
|
|
@@ -323,7 +326,7 @@ async fn auth_route(
|
|
|
323
326
|
raw_body,
|
|
324
327
|
cookies,
|
|
325
328
|
domain_ir: Some(domain_ir),
|
|
326
|
-
|
|
329
|
+
store: host.store,
|
|
327
330
|
wasm_path: host.wasm_path,
|
|
328
331
|
config: host.config,
|
|
329
332
|
invoker: host.invoker,
|
|
@@ -341,10 +344,7 @@ async fn auth_route(
|
|
|
341
344
|
&login_page(query.get("error").map(std::string::String::as_str)),
|
|
342
345
|
)),
|
|
343
346
|
|
|
344
|
-
("POST", "/logout") => Some(
|
|
345
|
-
"/login",
|
|
346
|
-
&format!("session=; Max-Age=0{}", cookie_flags()),
|
|
347
|
-
)),
|
|
347
|
+
("POST", "/logout") => Some(logout_response()),
|
|
348
348
|
|
|
349
349
|
// No password login: Google OAuth is the only sign-in path, and the
|
|
350
350
|
// /accounts/* routes here are just the session-cookie family that
|
|
@@ -360,15 +360,15 @@ async fn auth_route(
|
|
|
360
360
|
),
|
|
361
361
|
)),
|
|
362
362
|
|
|
363
|
-
("GET", "/accounts/me") =>
|
|
364
|
-
|
|
365
|
-
|
|
363
|
+
("GET", "/accounts/me") => Some(accounts_me_route(domain_ir, cookies, secret, store).await),
|
|
364
|
+
|
|
365
|
+
("POST", "/accounts/google-handoff") => Some(google_handoff_route(raw_body, secret)),
|
|
366
366
|
|
|
367
|
-
("GET", "/members") => Some(members_route(domain_ir, cookies, secret,
|
|
367
|
+
("GET", "/members") => Some(members_route(domain_ir, cookies, secret, store).await),
|
|
368
368
|
|
|
369
369
|
("POST", "/members") => Some(
|
|
370
370
|
add_member_route(
|
|
371
|
-
domain_ir, raw_body, cookies, secret,
|
|
371
|
+
domain_ir, raw_body, cookies, secret, store, wasm_path, config,
|
|
372
372
|
)
|
|
373
373
|
.await,
|
|
374
374
|
),
|
|
@@ -376,46 +376,50 @@ async fn auth_route(
|
|
|
376
376
|
("POST", "/signups") => Some(signup_route(raw_body, secret, host).await),
|
|
377
377
|
|
|
378
378
|
("POST", "/members/disable") => Some(
|
|
379
|
-
set_member_disabled_route(domain_ir, raw_body, cookies, secret,
|
|
379
|
+
set_member_disabled_route(domain_ir, raw_body, cookies, secret, store, config, true)
|
|
380
380
|
.await,
|
|
381
381
|
),
|
|
382
382
|
|
|
383
383
|
("POST", "/members/enable") => Some(
|
|
384
|
-
set_member_disabled_route(domain_ir, raw_body, cookies, secret,
|
|
384
|
+
set_member_disabled_route(domain_ir, raw_body, cookies, secret, store, config, false)
|
|
385
385
|
.await,
|
|
386
386
|
),
|
|
387
387
|
|
|
388
388
|
("POST", "/members/role") => {
|
|
389
|
-
Some(set_member_role_route(domain_ir, raw_body, cookies, secret,
|
|
389
|
+
Some(set_member_role_route(domain_ir, raw_body, cookies, secret, store, config).await)
|
|
390
390
|
}
|
|
391
391
|
|
|
392
392
|
("POST", "/members/delete") => Some(
|
|
393
|
-
set_member_deleted_route(domain_ir, raw_body, cookies, secret,
|
|
393
|
+
set_member_deleted_route(domain_ir, raw_body, cookies, secret, store, config).await,
|
|
394
394
|
),
|
|
395
395
|
|
|
396
396
|
// cms/src/endpoints/sso.ts verifies this with the same account_token
|
|
397
397
|
// wire format; deploy-aws/platform/template.yaml shares SessionSecret
|
|
398
398
|
// with the cms container as AUTH_SECRET so both sides agree on it.
|
|
399
399
|
("GET", "/accounts/sso-token") => {
|
|
400
|
-
Some(accounts_sso_token_route(domain_ir, cookies, secret,
|
|
400
|
+
Some(accounts_sso_token_route(domain_ir, cookies, secret, store).await)
|
|
401
401
|
}
|
|
402
402
|
|
|
403
|
-
("GET", "/auth/google") =>
|
|
404
|
-
Ok(url) => Some(redirect(&url)),
|
|
405
|
-
Err(e) => Some(respond(
|
|
406
|
-
500,
|
|
407
|
-
"text/plain",
|
|
408
|
-
&format!("Google sign-in isn't configured: {e}"),
|
|
409
|
-
)),
|
|
410
|
-
},
|
|
403
|
+
("GET", "/auth/google") => Some(google_start()),
|
|
411
404
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
405
|
+
// The sign-in cookie is cleared whatever the outcome: one cookie, one attempt.
|
|
406
|
+
("GET", "/auth/google/callback") => {
|
|
407
|
+
let secure = secure_cookies();
|
|
408
|
+
let response = match google_signin_for(query, cookies, secure) {
|
|
409
|
+
Some(signin) => {
|
|
410
|
+
google_callback(domain_ir, query, &signin.nonce, secret, host).await
|
|
411
|
+
}
|
|
412
|
+
None => redirect("/login?error=google_failed"),
|
|
413
|
+
};
|
|
414
|
+
Some(with_cookie(
|
|
415
|
+
response,
|
|
416
|
+
&auth::google_signin_clear_header(secure),
|
|
417
|
+
))
|
|
418
|
+
}
|
|
415
419
|
|
|
416
420
|
("GET" | "POST", "/admin/members") => Some(
|
|
417
421
|
admin_members_route(
|
|
418
|
-
method, domain_ir, raw_body, cookies, session, secret,
|
|
422
|
+
method, domain_ir, raw_body, cookies, session, secret, store, wasm_path, config,
|
|
419
423
|
)
|
|
420
424
|
.await,
|
|
421
425
|
),
|
|
@@ -433,11 +437,11 @@ async fn members_page_caller(
|
|
|
433
437
|
cookies: &HashMap<String, String>,
|
|
434
438
|
session: Option<&Session>,
|
|
435
439
|
secret: &str,
|
|
436
|
-
|
|
440
|
+
store: &Store,
|
|
437
441
|
wasm_path: &Path,
|
|
438
442
|
) -> Result<(), Value> {
|
|
439
443
|
if let Some(session) = session {
|
|
440
|
-
if is_admin(
|
|
444
|
+
if is_admin(store, wasm_path, domain_ir, &session.identity_id).await {
|
|
441
445
|
return Ok(());
|
|
442
446
|
}
|
|
443
447
|
}
|
|
@@ -445,7 +449,7 @@ async fn members_page_caller(
|
|
|
445
449
|
.get(&auth::account_cookie_name())
|
|
446
450
|
.and_then(|token| auth::verify_account_token(secret, token));
|
|
447
451
|
if let Some(email) = &account {
|
|
448
|
-
if auth::caller_is_admin(
|
|
452
|
+
if auth::caller_is_admin(store, domain_ir, email)
|
|
449
453
|
.await
|
|
450
454
|
.unwrap_or(false)
|
|
451
455
|
{
|
|
@@ -469,17 +473,17 @@ async fn admin_members_route(
|
|
|
469
473
|
cookies: &HashMap<String, String>,
|
|
470
474
|
session: Option<&Session>,
|
|
471
475
|
secret: &str,
|
|
472
|
-
|
|
476
|
+
store: &Store,
|
|
473
477
|
wasm_path: &Path,
|
|
474
478
|
config: &LineageConfig,
|
|
475
479
|
) -> Value {
|
|
476
480
|
if let Err(response) =
|
|
477
|
-
members_page_caller(domain_ir, cookies, session, secret,
|
|
481
|
+
members_page_caller(domain_ir, cookies, session, secret, store, wasm_path).await
|
|
478
482
|
{
|
|
479
483
|
return response;
|
|
480
484
|
}
|
|
481
485
|
if method == "GET" {
|
|
482
|
-
return html(200, &admin_members_page(
|
|
486
|
+
return html(200, &admin_members_page(store, domain_ir).await);
|
|
483
487
|
}
|
|
484
488
|
|
|
485
489
|
let form = parse_form(raw_body);
|
|
@@ -503,36 +507,72 @@ async fn admin_members_route(
|
|
|
503
507
|
{
|
|
504
508
|
return html(400, "<p>Give a valid email.</p>");
|
|
505
509
|
}
|
|
510
|
+
let days = match expiry_days(
|
|
511
|
+
form.get("expires_in_days")
|
|
512
|
+
.map(|s| Value::String(s.clone())),
|
|
513
|
+
) {
|
|
514
|
+
Ok(days) => days,
|
|
515
|
+
Err(message) => return html(400, &format!("<p>{}</p>", esc(message))),
|
|
516
|
+
};
|
|
506
517
|
let name = form
|
|
507
518
|
.get("name")
|
|
508
519
|
.map(|s| s.trim().to_string())
|
|
509
520
|
.filter(|s| !s.is_empty())
|
|
510
521
|
.unwrap_or_else(|| email.split('@').next().unwrap_or_default().to_string());
|
|
511
522
|
// Ok(false) is "already admitted": the grant below is then all there is to do.
|
|
512
|
-
if let Err(e) = auth::admit_person(
|
|
523
|
+
if let Err(e) = auth::admit_person(store, config, domain_ir, &email, &name).await {
|
|
513
524
|
return html(500, &format!("<p>{}</p>", esc(&e.to_string())));
|
|
514
525
|
}
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
526
|
+
let until = days.map(auth::expiry_after_days);
|
|
527
|
+
match auth::grant_access_until(
|
|
528
|
+
store,
|
|
529
|
+
wasm_path,
|
|
530
|
+
config,
|
|
531
|
+
domain_ir,
|
|
532
|
+
&email,
|
|
533
|
+
&role,
|
|
534
|
+
until.as_deref(),
|
|
535
|
+
)
|
|
536
|
+
.await
|
|
537
|
+
{
|
|
538
|
+
Ok(auth::GrantOutcome::Done) => redirect("/admin/members"),
|
|
539
|
+
Ok(auth::GrantOutcome::UnknownPerson) => html(404, "<p>No member with that email.</p>"),
|
|
540
|
+
Ok(auth::GrantOutcome::LastOpenAdmin) => {
|
|
541
|
+
html(409, &format!("<p>{}</p>", esc(NEEDS_OPEN_ADMIN)))
|
|
542
|
+
}
|
|
518
543
|
Err(e) => html(500, &format!("<p>{}</p>", esc(&e.to_string()))),
|
|
519
544
|
}
|
|
520
545
|
}
|
|
521
546
|
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
)
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
)
|
|
535
|
-
|
|
547
|
+
// The refusal when a change would leave no active admin without an end date.
|
|
548
|
+
const NEEDS_OPEN_ADMIN: &str = "a site must keep at least one admin without an end date";
|
|
549
|
+
|
|
550
|
+
// The optional `expires_in_days` of a grant: absent, null or blank means an
|
|
551
|
+
// open-ended grant; otherwise a whole number of days, 1 to `MAX_EXPIRY_DAYS`.
|
|
552
|
+
// A JSON number or a numeric string (a form field) both read.
|
|
553
|
+
fn expiry_days(raw: Option<Value>) -> Result<Option<u32>, &'static str> {
|
|
554
|
+
const MESSAGE: &str = "expires_in_days must be a whole number of days from 1 to 365";
|
|
555
|
+
let days = match raw {
|
|
556
|
+
None | Some(Value::Null) => return Ok(None),
|
|
557
|
+
Some(Value::Number(n)) => n.as_u64(),
|
|
558
|
+
Some(Value::String(s)) if s.trim().is_empty() => return Ok(None),
|
|
559
|
+
Some(Value::String(s)) => s.trim().parse::<u64>().ok(),
|
|
560
|
+
Some(_) => None,
|
|
561
|
+
};
|
|
562
|
+
days.filter(|d| (1..=u64::from(auth::MAX_EXPIRY_DAYS)).contains(d))
|
|
563
|
+
.map(|d| Some(d as u32))
|
|
564
|
+
.ok_or(MESSAGE)
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
async fn is_admin(store: &Store, wasm_path: &Path, domain_ir: &Value, identity_id: &str) -> bool {
|
|
568
|
+
let Some(query) = crate::ir::authorization_provider(domain_ir).and_then(|p| p.live_assignments)
|
|
569
|
+
else {
|
|
570
|
+
return false;
|
|
571
|
+
};
|
|
572
|
+
let args = json!({"actor_id": {"value": identity_id}});
|
|
573
|
+
dispatch::query(store, wasm_path, &query, args)
|
|
574
|
+
.await
|
|
575
|
+
.is_ok_and(|answer| auth::holds_admin(&auth::answer_rows(&answer)))
|
|
536
576
|
}
|
|
537
577
|
|
|
538
578
|
// Verifies the token AND that the person still has access, so a
|
|
@@ -541,7 +581,7 @@ async fn active_session_email(
|
|
|
541
581
|
domain_ir: &Value,
|
|
542
582
|
cookies: &HashMap<String, String>,
|
|
543
583
|
secret: &str,
|
|
544
|
-
|
|
584
|
+
store: &Store,
|
|
545
585
|
) -> Result<String, Value> {
|
|
546
586
|
let not_logged_in = || {
|
|
547
587
|
respond(
|
|
@@ -556,7 +596,7 @@ async fn active_session_email(
|
|
|
556
596
|
else {
|
|
557
597
|
return Err(not_logged_in());
|
|
558
598
|
};
|
|
559
|
-
match auth::has_access(
|
|
599
|
+
match auth::has_access(store, domain_ir, &email).await {
|
|
560
600
|
Ok(true) => Ok(email),
|
|
561
601
|
Ok(false) => Err(not_logged_in()),
|
|
562
602
|
Err(e) => Err(respond(
|
|
@@ -571,10 +611,10 @@ async fn accounts_me_route(
|
|
|
571
611
|
domain_ir: &Value,
|
|
572
612
|
cookies: &HashMap<String, String>,
|
|
573
613
|
secret: &str,
|
|
574
|
-
|
|
614
|
+
store: &Store,
|
|
575
615
|
) -> Value {
|
|
576
|
-
match active_session_email(domain_ir, cookies, secret,
|
|
577
|
-
Ok(email) => match auth::member_identity_id(
|
|
616
|
+
match active_session_email(domain_ir, cookies, secret, store).await {
|
|
617
|
+
Ok(email) => match auth::member_identity_id(store, domain_ir, &email).await {
|
|
578
618
|
Ok(identity_id) => respond(
|
|
579
619
|
200,
|
|
580
620
|
"application/json",
|
|
@@ -597,13 +637,12 @@ async fn members_route(
|
|
|
597
637
|
domain_ir: &Value,
|
|
598
638
|
cookies: &HashMap<String, String>,
|
|
599
639
|
secret: &str,
|
|
600
|
-
|
|
640
|
+
store: &Store,
|
|
601
641
|
) -> Value {
|
|
602
|
-
if let Err(response) = newsletter_send::require_admin(domain_ir, cookies, secret,
|
|
603
|
-
{
|
|
642
|
+
if let Err(response) = newsletter_send::require_admin(domain_ir, cookies, secret, store).await {
|
|
604
643
|
return response;
|
|
605
644
|
}
|
|
606
|
-
match auth::all_people(
|
|
645
|
+
match auth::all_people(store, domain_ir).await {
|
|
607
646
|
Ok(people) => respond(
|
|
608
647
|
200,
|
|
609
648
|
"application/json",
|
|
@@ -624,7 +663,7 @@ pub(crate) async fn add_member_route(
|
|
|
624
663
|
raw_body: &str,
|
|
625
664
|
cookies: &HashMap<String, String>,
|
|
626
665
|
secret: &str,
|
|
627
|
-
|
|
666
|
+
store: &Store,
|
|
628
667
|
wasm_path: &Path,
|
|
629
668
|
config: &LineageConfig,
|
|
630
669
|
) -> Value {
|
|
@@ -634,7 +673,7 @@ pub(crate) async fn add_member_route(
|
|
|
634
673
|
else {
|
|
635
674
|
return json_error(401, "not logged in");
|
|
636
675
|
};
|
|
637
|
-
match auth::caller_is_admin(
|
|
676
|
+
match auth::caller_is_admin(store, domain_ir, &caller).await {
|
|
638
677
|
Ok(true) => {}
|
|
639
678
|
Ok(false) => return json_error(403, "admins only"),
|
|
640
679
|
Err(e) => return json_error(500, &format!("members lookup failed: {e}")),
|
|
@@ -668,19 +707,36 @@ pub(crate) async fn add_member_route(
|
|
|
668
707
|
);
|
|
669
708
|
}
|
|
670
709
|
|
|
671
|
-
|
|
710
|
+
let days = match expiry_days(body.get("expires_in_days").cloned()) {
|
|
711
|
+
Ok(days) => days,
|
|
712
|
+
Err(message) => return json_error(400, message),
|
|
713
|
+
};
|
|
714
|
+
|
|
715
|
+
match auth::admit_person(store, config, domain_ir, &email, &name).await {
|
|
672
716
|
Ok(true) => {}
|
|
673
717
|
Ok(false) => return json_error(409, "that email is already admitted"),
|
|
674
718
|
Err(e) => return json_error(500, &format!("admit failed: {e}")),
|
|
675
719
|
}
|
|
676
720
|
let email = email.to_lowercase();
|
|
677
|
-
|
|
678
|
-
|
|
721
|
+
let until = days.map(auth::expiry_after_days);
|
|
722
|
+
match auth::grant_access_until(
|
|
723
|
+
store,
|
|
724
|
+
wasm_path,
|
|
725
|
+
config,
|
|
726
|
+
domain_ir,
|
|
727
|
+
&email,
|
|
728
|
+
&role,
|
|
729
|
+
until.as_deref(),
|
|
730
|
+
)
|
|
731
|
+
.await
|
|
732
|
+
{
|
|
733
|
+
Ok(auth::GrantOutcome::Done) => respond(
|
|
679
734
|
201,
|
|
680
735
|
"application/json",
|
|
681
|
-
&json!({"name": name, "email": email, "role": role, "linked": false, "granted": true, "disabled": false, "identity_id": null}).to_string(),
|
|
736
|
+
&json!({"name": name, "email": email, "role": role, "linked": false, "granted": true, "disabled": false, "access_expires_at": until, "expired": false, "identity_id": null}).to_string(),
|
|
682
737
|
),
|
|
683
|
-
Ok(
|
|
738
|
+
Ok(auth::GrantOutcome::UnknownPerson) => json_error(500, &format!("the person was admitted but the {role} role was not granted")),
|
|
739
|
+
Ok(auth::GrantOutcome::LastOpenAdmin) => json_error(409, NEEDS_OPEN_ADMIN),
|
|
684
740
|
Err(e) => json_error(500, &format!("the person was admitted but the {role} role was not granted: {e}")),
|
|
685
741
|
}
|
|
686
742
|
}
|
|
@@ -739,7 +795,7 @@ async fn set_member_disabled_route(
|
|
|
739
795
|
raw_body: &str,
|
|
740
796
|
cookies: &HashMap<String, String>,
|
|
741
797
|
secret: &str,
|
|
742
|
-
|
|
798
|
+
store: &Store,
|
|
743
799
|
config: &LineageConfig,
|
|
744
800
|
disable: bool,
|
|
745
801
|
) -> Value {
|
|
@@ -760,7 +816,7 @@ async fn set_member_disabled_route(
|
|
|
760
816
|
return json_error(400, "an email is required");
|
|
761
817
|
}
|
|
762
818
|
|
|
763
|
-
match auth::set_person_disabled(
|
|
819
|
+
match auth::set_person_disabled(store, config, domain_ir, &caller, &email, disable).await {
|
|
764
820
|
Ok(auth::DisableOutcome::Done) => {
|
|
765
821
|
let key = if disable { "disabled" } else { "enabled" };
|
|
766
822
|
respond(
|
|
@@ -774,6 +830,7 @@ async fn set_member_disabled_route(
|
|
|
774
830
|
json_error(403, "you can't disable your own admin access")
|
|
775
831
|
}
|
|
776
832
|
Ok(auth::DisableOutcome::UnknownPerson) => json_error(404, "no member with that email"),
|
|
833
|
+
Ok(auth::DisableOutcome::LastOpenAdmin) => json_error(409, NEEDS_OPEN_ADMIN),
|
|
777
834
|
Ok(auth::DisableOutcome::LastAdmin) => {
|
|
778
835
|
json_error(409, "there must always be at least one admin")
|
|
779
836
|
}
|
|
@@ -790,7 +847,7 @@ async fn set_member_deleted_route(
|
|
|
790
847
|
raw_body: &str,
|
|
791
848
|
cookies: &HashMap<String, String>,
|
|
792
849
|
secret: &str,
|
|
793
|
-
|
|
850
|
+
store: &Store,
|
|
794
851
|
config: &LineageConfig,
|
|
795
852
|
) -> Value {
|
|
796
853
|
let Some(caller) = cookies
|
|
@@ -810,7 +867,7 @@ async fn set_member_deleted_route(
|
|
|
810
867
|
return json_error(400, "an email is required");
|
|
811
868
|
}
|
|
812
869
|
|
|
813
|
-
match auth::set_person_deleted(
|
|
870
|
+
match auth::set_person_deleted(store, config, domain_ir, &caller, &email).await {
|
|
814
871
|
Ok(auth::DeleteOutcome::Done) => respond(
|
|
815
872
|
200,
|
|
816
873
|
"application/json",
|
|
@@ -830,7 +887,7 @@ async fn set_member_role_route(
|
|
|
830
887
|
raw_body: &str,
|
|
831
888
|
cookies: &HashMap<String, String>,
|
|
832
889
|
secret: &str,
|
|
833
|
-
|
|
890
|
+
store: &Store,
|
|
834
891
|
config: &LineageConfig,
|
|
835
892
|
) -> Value {
|
|
836
893
|
let Some(caller) = cookies
|
|
@@ -858,7 +915,7 @@ async fn set_member_role_route(
|
|
|
858
915
|
);
|
|
859
916
|
}
|
|
860
917
|
|
|
861
|
-
match auth::set_person_role(
|
|
918
|
+
match auth::set_person_role(store, config, domain_ir, &caller, &email, &role).await {
|
|
862
919
|
Ok(auth::RoleOutcome::Done) => respond(
|
|
863
920
|
200,
|
|
864
921
|
"application/json",
|
|
@@ -866,6 +923,7 @@ async fn set_member_role_route(
|
|
|
866
923
|
),
|
|
867
924
|
Ok(auth::RoleOutcome::CallerNotAdmin) => json_error(403, "admins only"),
|
|
868
925
|
Ok(auth::RoleOutcome::UnknownPerson) => json_error(404, "no member with that email"),
|
|
926
|
+
Ok(auth::RoleOutcome::LastOpenAdmin) => json_error(409, NEEDS_OPEN_ADMIN),
|
|
869
927
|
Ok(auth::RoleOutcome::LastAdmin) => {
|
|
870
928
|
json_error(409, "there must always be at least one admin")
|
|
871
929
|
}
|
|
@@ -879,7 +937,7 @@ async fn registrations_list_route(
|
|
|
879
937
|
domain_ir: &Value,
|
|
880
938
|
cookies: &HashMap<String, String>,
|
|
881
939
|
secret: &str,
|
|
882
|
-
|
|
940
|
+
store: &Store,
|
|
883
941
|
wasm_path: &Path,
|
|
884
942
|
config: &LineageConfig,
|
|
885
943
|
) -> Value {
|
|
@@ -889,13 +947,13 @@ async fn registrations_list_route(
|
|
|
889
947
|
else {
|
|
890
948
|
return json_error(401, "not logged in");
|
|
891
949
|
};
|
|
892
|
-
match auth::caller_is_admin(
|
|
950
|
+
match auth::caller_is_admin(store, domain_ir, &caller).await {
|
|
893
951
|
Ok(true) => {}
|
|
894
952
|
Ok(false) => return json_error(403, "admins only"),
|
|
895
953
|
Err(e) => return json_error(500, &format!("members lookup failed: {e}")),
|
|
896
954
|
}
|
|
897
955
|
|
|
898
|
-
let read = match dispatch::read(
|
|
956
|
+
let read = match dispatch::read(store, wasm_path).await {
|
|
899
957
|
Ok(read) => read,
|
|
900
958
|
Err(e) => return json_error(500, &format!("registrations lookup failed: {e:#}")),
|
|
901
959
|
};
|
|
@@ -960,10 +1018,10 @@ async fn accounts_sso_token_route(
|
|
|
960
1018
|
domain_ir: &Value,
|
|
961
1019
|
cookies: &HashMap<String, String>,
|
|
962
1020
|
secret: &str,
|
|
963
|
-
|
|
1021
|
+
store: &Store,
|
|
964
1022
|
) -> Value {
|
|
965
1023
|
const SSO_TOKEN_TTL_SECS: u64 = 60;
|
|
966
|
-
let email = match active_session_email(domain_ir, cookies, secret,
|
|
1024
|
+
let email = match active_session_email(domain_ir, cookies, secret, store).await {
|
|
967
1025
|
Ok(email) => email,
|
|
968
1026
|
Err(response) => return response,
|
|
969
1027
|
};
|
|
@@ -974,29 +1032,100 @@ async fn accounts_sso_token_route(
|
|
|
974
1032
|
&json!({"token": sso_token}).to_string(),
|
|
975
1033
|
)
|
|
976
1034
|
}
|
|
1035
|
+
// Logging out ends both sessions: the Governance `session` cookie and the account cookie.
|
|
1036
|
+
fn logout_response() -> Value {
|
|
1037
|
+
with_cookie(
|
|
1038
|
+
redirect_with_cookie("/login", &format!("session=; Max-Age=0{}", cookie_flags())),
|
|
1039
|
+
&format!(
|
|
1040
|
+
"{}=; Max-Age=0{}",
|
|
1041
|
+
auth::account_cookie_name(),
|
|
1042
|
+
cookie_flags()
|
|
1043
|
+
),
|
|
1044
|
+
)
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
// Starts a Google sign-in bound to this browser: the cookie set here holds the state and nonce
|
|
1048
|
+
// the callback must bring back (auth/google_signin.rs).
|
|
1049
|
+
fn google_start() -> Value {
|
|
1050
|
+
let signin = auth::GoogleSignin::fresh();
|
|
1051
|
+
match auth::authorization_url(&redirect_uri(), &signin) {
|
|
1052
|
+
Ok(url) => redirect_with_cookie(
|
|
1053
|
+
&url,
|
|
1054
|
+
&auth::google_signin_set_header(&signin, secure_cookies()),
|
|
1055
|
+
),
|
|
1056
|
+
Err(e) => respond(
|
|
1057
|
+
500,
|
|
1058
|
+
"text/plain",
|
|
1059
|
+
&format!("Google sign-in isn't configured: {e}"),
|
|
1060
|
+
),
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
// The sign-in this browser started, when the callback's `state` is the one its cookie holds.
|
|
1065
|
+
fn google_signin_for(
|
|
1066
|
+
query: &HashMap<String, String>,
|
|
1067
|
+
cookies: &HashMap<String, String>,
|
|
1068
|
+
secure: bool,
|
|
1069
|
+
) -> Option<auth::GoogleSignin> {
|
|
1070
|
+
let Some(signin) = auth::read_google_signin(cookies, secure) else {
|
|
1071
|
+
eprintln!("google_callback: no sign-in cookie");
|
|
1072
|
+
return None;
|
|
1073
|
+
};
|
|
1074
|
+
let state = query.get("state").map_or("", String::as_str);
|
|
1075
|
+
if !signin.state_matches(state) {
|
|
1076
|
+
eprintln!("google_callback: state does not match the sign-in cookie");
|
|
1077
|
+
return None;
|
|
1078
|
+
}
|
|
1079
|
+
Some(signin)
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
// A site on another origin than this host trades the handoff token from the callback's redirect
|
|
1083
|
+
// for the account token here, server-to-server, so the long-lived token never rides a URL.
|
|
1084
|
+
fn google_handoff_route(raw_body: &str, secret: &str) -> Value {
|
|
1085
|
+
static SPENT: OnceLock<JtiStore> = OnceLock::new();
|
|
1086
|
+
let spent = SPENT.get_or_init(JtiStore::new);
|
|
1087
|
+
let token = serde_json::from_str::<Value>(raw_body)
|
|
1088
|
+
.ok()
|
|
1089
|
+
.and_then(|body| body.get("token")?.as_str().map(str::to_string));
|
|
1090
|
+
token
|
|
1091
|
+
.and_then(|token| auth::redeem_handoff_token(secret, &token, spent))
|
|
1092
|
+
.map_or_else(
|
|
1093
|
+
|| {
|
|
1094
|
+
json_error(
|
|
1095
|
+
401,
|
|
1096
|
+
"this sign-in handoff is invalid, expired or already used",
|
|
1097
|
+
)
|
|
1098
|
+
},
|
|
1099
|
+
|account| {
|
|
1100
|
+
respond(
|
|
1101
|
+
200,
|
|
1102
|
+
"application/json",
|
|
1103
|
+
&json!({ "token": account }).to_string(),
|
|
1104
|
+
)
|
|
1105
|
+
},
|
|
1106
|
+
)
|
|
1107
|
+
}
|
|
1108
|
+
|
|
977
1109
|
async fn google_callback(
|
|
978
1110
|
domain_ir: &Value,
|
|
979
1111
|
query: &HashMap<String, String>,
|
|
980
|
-
|
|
981
|
-
wasm_path: &Path,
|
|
982
|
-
config: &LineageConfig,
|
|
1112
|
+
nonce: &str,
|
|
983
1113
|
secret: &str,
|
|
984
|
-
|
|
1114
|
+
host: Host<'_>,
|
|
985
1115
|
) -> Value {
|
|
1116
|
+
let Host {
|
|
1117
|
+
store,
|
|
1118
|
+
wasm_path,
|
|
1119
|
+
config,
|
|
1120
|
+
invoker,
|
|
1121
|
+
..
|
|
1122
|
+
} = host;
|
|
986
1123
|
let Some(code) = query.get("code") else {
|
|
987
1124
|
eprintln!("google_callback: missing code");
|
|
988
1125
|
return redirect("/login?error=google_failed");
|
|
989
1126
|
};
|
|
990
|
-
let Some(state) = query.get("state") else {
|
|
991
|
-
eprintln!("google_callback: missing state");
|
|
992
|
-
return redirect("/login?error=google_failed");
|
|
993
|
-
};
|
|
994
|
-
if let Err(e) = auth::verify_state(state, secret) {
|
|
995
|
-
eprintln!("google_callback: state: {e}");
|
|
996
|
-
return redirect("/login?error=google_failed");
|
|
997
|
-
}
|
|
998
1127
|
|
|
999
|
-
let claims = match auth::verify(code, &redirect_uri()).await {
|
|
1128
|
+
let claims = match auth::verify(code, &redirect_uri(), nonce).await {
|
|
1000
1129
|
Ok(c) => c,
|
|
1001
1130
|
Err(e) => {
|
|
1002
1131
|
eprintln!("google_callback: token exchange: {e}");
|
|
@@ -1004,7 +1133,7 @@ async fn google_callback(
|
|
|
1004
1133
|
}
|
|
1005
1134
|
};
|
|
1006
1135
|
|
|
1007
|
-
let read = match dispatch::read(
|
|
1136
|
+
let read = match dispatch::read(store, wasm_path).await {
|
|
1008
1137
|
Ok(r) => r,
|
|
1009
1138
|
Err(e) => {
|
|
1010
1139
|
eprintln!("google_callback: read: {e:#}");
|
|
@@ -1016,7 +1145,7 @@ async fn google_callback(
|
|
|
1016
1145
|
// PostgresEra head first — Identity is not in the WASM journal.
|
|
1017
1146
|
// Journal `instances` is only a fallback for Lambda-era rows.
|
|
1018
1147
|
let identity_id =
|
|
1019
|
-
match auth::resolve_identity_from_head(
|
|
1148
|
+
match auth::resolve_identity_from_head(store, domain_ir, &claims.issuer, &claims.subject)
|
|
1020
1149
|
.await
|
|
1021
1150
|
{
|
|
1022
1151
|
Ok(found) => found,
|
|
@@ -1030,7 +1159,7 @@ async fn google_callback(
|
|
|
1030
1159
|
|
|
1031
1160
|
let session = match identity_id {
|
|
1032
1161
|
Some(ref identity_id) => {
|
|
1033
|
-
match auth::session_for_member_by_identity(
|
|
1162
|
+
match auth::session_for_member_by_identity(store, domain_ir, identity_id).await {
|
|
1034
1163
|
Ok(s) => s,
|
|
1035
1164
|
Err(e) => {
|
|
1036
1165
|
eprintln!("google_callback: member by identity: {e:#}");
|
|
@@ -1045,7 +1174,7 @@ async fn google_callback(
|
|
|
1045
1174
|
None if claims.email_verified => {
|
|
1046
1175
|
let email = claims.email.clone().unwrap_or_default();
|
|
1047
1176
|
match auth::provision(
|
|
1048
|
-
|
|
1177
|
+
store,
|
|
1049
1178
|
wasm_path,
|
|
1050
1179
|
config,
|
|
1051
1180
|
domain_ir,
|
|
@@ -1072,11 +1201,12 @@ async fn google_callback(
|
|
|
1072
1201
|
|
|
1073
1202
|
// The site's admin authenticates on the account cookie, not this
|
|
1074
1203
|
// host's own session cookie. Cross-origin, a cookie here would never
|
|
1075
|
-
// reach the site, so
|
|
1204
|
+
// reach the site, so a one-minute handoff token goes to its
|
|
1205
|
+
// /api/google-handoff, which trades it at /accounts/google-handoff.
|
|
1076
1206
|
let site = site_url();
|
|
1077
1207
|
let site = site.trim_end_matches('/');
|
|
1078
|
-
let token = auth::account_token(secret, &session.email, auth::SESSION_TTL_SECS);
|
|
1079
1208
|
if same_origin(site, &redirect_uri()) {
|
|
1209
|
+
let token = auth::account_token(secret, &session.email, auth::SESSION_TTL_SECS);
|
|
1080
1210
|
let cookie = format!(
|
|
1081
1211
|
"{}={token}{}; Max-Age={}",
|
|
1082
1212
|
auth::account_cookie_name(),
|
|
@@ -1089,7 +1219,7 @@ async fn google_callback(
|
|
|
1089
1219
|
redirect(&format!(
|
|
1090
1220
|
"{}/api/google-handoff?token={}",
|
|
1091
1221
|
site,
|
|
1092
|
-
auth::urlencode(&
|
|
1222
|
+
auth::urlencode(&auth::handoff_token(secret, &session.email))
|
|
1093
1223
|
))
|
|
1094
1224
|
}
|
|
1095
1225
|
}
|
|
@@ -1133,26 +1263,34 @@ fn login_page(error: Option<&str>) -> String {
|
|
|
1133
1263
|
)
|
|
1134
1264
|
}
|
|
1135
1265
|
|
|
1136
|
-
async fn admin_members_page(
|
|
1137
|
-
let people = auth::all_people(
|
|
1138
|
-
.await
|
|
1139
|
-
.unwrap_or_default();
|
|
1266
|
+
async fn admin_members_page(store: &Store, domain_ir: &Value) -> String {
|
|
1267
|
+
let people = auth::all_people(store, domain_ir).await.unwrap_or_default();
|
|
1140
1268
|
let rows: String = people
|
|
1141
1269
|
.iter()
|
|
1142
1270
|
.map(|p| {
|
|
1143
1271
|
let name = p.get("name").and_then(|v| v.as_str()).unwrap_or("");
|
|
1144
1272
|
let email = p.get("email").and_then(|v| v.as_str()).unwrap_or("");
|
|
1145
1273
|
let role = p.get("role").and_then(|v| v.as_str()).unwrap_or("—");
|
|
1146
|
-
let
|
|
1147
|
-
"
|
|
1274
|
+
let until = p
|
|
1275
|
+
.get("access_expires_at")
|
|
1276
|
+
.and_then(|v| v.as_str())
|
|
1277
|
+
.unwrap_or("");
|
|
1278
|
+
let expired = p.get("expired").and_then(serde_json::Value::as_bool) == Some(true);
|
|
1279
|
+
let mut access = if expired {
|
|
1280
|
+
format!("access ended {until}")
|
|
1281
|
+
} else if p.get("linked").and_then(serde_json::Value::as_bool) == Some(true) {
|
|
1282
|
+
"linked".to_string()
|
|
1148
1283
|
} else if p.get("granted").and_then(serde_json::Value::as_bool) == Some(true) {
|
|
1149
|
-
"granted, not yet signed in"
|
|
1284
|
+
"granted, not yet signed in".to_string()
|
|
1150
1285
|
} else {
|
|
1151
|
-
"no access"
|
|
1286
|
+
"no access".to_string()
|
|
1152
1287
|
};
|
|
1288
|
+
if !expired && !until.is_empty() {
|
|
1289
|
+
access = format!("{access}, until {until}");
|
|
1290
|
+
}
|
|
1153
1291
|
format!(
|
|
1154
1292
|
r#"<tr class="border-b border-slate-100"><td class="py-2 pr-4">{}</td><td class="py-2 pr-4">{}</td><td class="py-2 pr-4">{}</td><td class="py-2">{}</td></tr>"#,
|
|
1155
|
-
esc(name), esc(email), esc(role), esc(access)
|
|
1293
|
+
esc(name), esc(email), esc(role), esc(&access)
|
|
1156
1294
|
)
|
|
1157
1295
|
})
|
|
1158
1296
|
.collect::<Vec<_>>()
|
|
@@ -1164,10 +1302,11 @@ async fn admin_members_page(client: &Mutex<Client>, domain_ir: &Value) -> String
|
|
|
1164
1302
|
<p class="mb-4"><a href="/" class="text-slate-500 hover:text-slate-700">← back</a></p>
|
|
1165
1303
|
<h1 class="text-xl font-semibold mb-4">Members</h1>
|
|
1166
1304
|
<table class="w-full text-sm mb-6"><thead><tr class="text-left text-slate-500 text-xs uppercase"><th class="py-2 pr-4">Name</th><th class="py-2 pr-4">Email</th><th class="py-2 pr-4">Role</th><th class="py-2">Access</th></tr></thead><tbody>{rows}</tbody></table>
|
|
1167
|
-
<form method="post" action="/admin/members" class="flex gap-2 items-center">
|
|
1305
|
+
<form method="post" action="/admin/members" class="flex flex-wrap gap-2 items-center">
|
|
1168
1306
|
<input type="text" name="name" placeholder="Name" class="w-40 border border-slate-300 rounded-md px-3 py-2 text-sm" />
|
|
1169
1307
|
<input type="email" name="email" placeholder="Email (their Google account)" required class="flex-1 border border-slate-300 rounded-md px-3 py-2 text-sm" />
|
|
1170
1308
|
<select name="role" class="border border-slate-300 rounded-md px-3 py-2 text-sm"><option value="Admin">Admin</option><option value="Owner">Owner</option><option value="Member">Member</option></select>
|
|
1309
|
+
<input type="number" name="expires_in_days" min="1" max="365" step="1" placeholder="Days (optional)" title="Leave empty for access with no end date" class="w-32 border border-slate-300 rounded-md px-3 py-2 text-sm" />
|
|
1171
1310
|
<button type="submit" class="px-4 py-2 bg-slate-900 text-white rounded-md text-sm">Grant access</button>
|
|
1172
1311
|
</form></main></body></html>"#
|
|
1173
1312
|
)
|
|
@@ -1276,7 +1415,7 @@ fn humanize(path: &str) -> String {
|
|
|
1276
1415
|
const PRIMITIVES: &[&str] = &["String", "Integer", "Float", "TrueClass", "FalseClass"];
|
|
1277
1416
|
|
|
1278
1417
|
// Dispatch order: list? -> reference? -> admits -> value object ->
|
|
1279
|
-
// primitive. `domain_ir` is the whole
|
|
1418
|
+
// primitive. `domain_ir` is the whole bluebook, needed for cross-aggregate lookups.
|
|
1280
1419
|
fn resolve_field(domain_ir: &Value, attribute: &Value, aggregate: &Value, path: &str) -> Field {
|
|
1281
1420
|
let is_list = attribute
|
|
1282
1421
|
.get("list")
|
|
@@ -1752,12 +1891,12 @@ async fn aggregate_index(
|
|
|
1752
1891
|
domain_name: &str,
|
|
1753
1892
|
aggregate: &Value,
|
|
1754
1893
|
format: &str,
|
|
1755
|
-
|
|
1894
|
+
store: &Store,
|
|
1756
1895
|
wasm_path: &Path,
|
|
1757
1896
|
) -> Value {
|
|
1758
1897
|
let agg = agg_name(aggregate);
|
|
1759
1898
|
let prefix = format!("{domain_name}::{agg}#");
|
|
1760
|
-
let records = match dispatch::read(
|
|
1899
|
+
let records = match dispatch::read(store, wasm_path).await {
|
|
1761
1900
|
Ok(result) => instances_for(&result, &prefix),
|
|
1762
1901
|
Err(e) => return respond(500, "text/plain", &format!("{e:#}")),
|
|
1763
1902
|
};
|
|
@@ -1841,12 +1980,12 @@ async fn record_show(
|
|
|
1841
1980
|
aggregate: &Value,
|
|
1842
1981
|
id: &str,
|
|
1843
1982
|
format: &str,
|
|
1844
|
-
|
|
1983
|
+
store: &Store,
|
|
1845
1984
|
wasm_path: &Path,
|
|
1846
1985
|
) -> Value {
|
|
1847
1986
|
let agg = agg_name(aggregate);
|
|
1848
1987
|
let prefix = format!("{domain_name}::{agg}#");
|
|
1849
|
-
let result = match dispatch::read(
|
|
1988
|
+
let result = match dispatch::read(store, wasm_path).await {
|
|
1850
1989
|
Ok(r) => r,
|
|
1851
1990
|
Err(e) => return respond(500, "text/plain", &format!("{e:#}")),
|
|
1852
1991
|
};
|
|
@@ -1923,10 +2062,11 @@ async fn command_route(
|
|
|
1923
2062
|
method: &str,
|
|
1924
2063
|
query: &HashMap<String, String>,
|
|
1925
2064
|
raw_body: &str,
|
|
2065
|
+
caller: dispatch::Caller<'_>,
|
|
1926
2066
|
host: Host<'_>,
|
|
1927
2067
|
) -> Value {
|
|
1928
2068
|
let Host {
|
|
1929
|
-
|
|
2069
|
+
store,
|
|
1930
2070
|
wasm_path,
|
|
1931
2071
|
config,
|
|
1932
2072
|
invoker,
|
|
@@ -1937,20 +2077,37 @@ async fn command_route(
|
|
|
1937
2077
|
|
|
1938
2078
|
if format != "html" {
|
|
1939
2079
|
if method == "GET" {
|
|
2080
|
+
if query.get("schema").map(String::as_str) == Some("1") {
|
|
2081
|
+
return crate::ir::command_schema(agg_name(aggregate), cname).map_or_else(
|
|
2082
|
+
|| respond(404, "text/plain", &format!("no schema for {cname}")),
|
|
2083
|
+
|schema| respond(200, "application/json", &schema.to_string()),
|
|
2084
|
+
);
|
|
2085
|
+
}
|
|
1940
2086
|
return respond(200, "application/json", &command.to_string());
|
|
1941
2087
|
}
|
|
1942
2088
|
let raw = parse_form(raw_body);
|
|
2089
|
+
if crate::validation::requested(query) {
|
|
2090
|
+
let facts = Preview {
|
|
2091
|
+
domain_name,
|
|
2092
|
+
aggregate,
|
|
2093
|
+
command,
|
|
2094
|
+
fields: &fields,
|
|
2095
|
+
raw: &raw,
|
|
2096
|
+
};
|
|
2097
|
+
return preview_submission(&facts, host, caller).await;
|
|
2098
|
+
}
|
|
1943
2099
|
return submit(
|
|
1944
2100
|
domain_name,
|
|
1945
2101
|
aggregate,
|
|
1946
2102
|
command,
|
|
1947
2103
|
&fields,
|
|
1948
2104
|
&raw,
|
|
1949
|
-
|
|
2105
|
+
store,
|
|
1950
2106
|
wasm_path,
|
|
1951
2107
|
config,
|
|
1952
2108
|
true,
|
|
1953
2109
|
invoker,
|
|
2110
|
+
caller,
|
|
1954
2111
|
)
|
|
1955
2112
|
.await;
|
|
1956
2113
|
}
|
|
@@ -1980,14 +2137,54 @@ async fn command_route(
|
|
|
1980
2137
|
command,
|
|
1981
2138
|
&fields,
|
|
1982
2139
|
&raw,
|
|
1983
|
-
|
|
2140
|
+
store,
|
|
1984
2141
|
wasm_path,
|
|
1985
2142
|
config,
|
|
1986
2143
|
false,
|
|
1987
2144
|
invoker,
|
|
2145
|
+
caller,
|
|
1988
2146
|
)
|
|
1989
2147
|
.await
|
|
1990
2148
|
}
|
|
2149
|
+
// What a preview of a submitted form is asked about: the command and the values posted for it.
|
|
2150
|
+
struct Preview<'a> {
|
|
2151
|
+
domain_name: &'a str,
|
|
2152
|
+
aggregate: &'a Value,
|
|
2153
|
+
command: &'a Value,
|
|
2154
|
+
fields: &'a [Field],
|
|
2155
|
+
raw: &'a HashMap<String, String>,
|
|
2156
|
+
}
|
|
2157
|
+
|
|
2158
|
+
// Whether the submitted command would be accepted, answered as the client envelope with 200 whether
|
|
2159
|
+
// or not it would be refused, and kept nowhere. A malformed submission is a 400, as a real one is.
|
|
2160
|
+
async fn preview_submission(
|
|
2161
|
+
asked: &Preview<'_>,
|
|
2162
|
+
host: Host<'_>,
|
|
2163
|
+
caller: dispatch::Caller<'_>,
|
|
2164
|
+
) -> Value {
|
|
2165
|
+
let Preview {
|
|
2166
|
+
domain_name,
|
|
2167
|
+
aggregate,
|
|
2168
|
+
command,
|
|
2169
|
+
fields,
|
|
2170
|
+
raw,
|
|
2171
|
+
} = *asked;
|
|
2172
|
+
let cname = command.get("name").and_then(|v| v.as_str()).unwrap_or("");
|
|
2173
|
+
let verb = format!("{domain_name}::{}.{cname}", agg_name(aggregate));
|
|
2174
|
+
let args = match extract_args(fields, raw) {
|
|
2175
|
+
Ok(args) => args,
|
|
2176
|
+
Err(e) => return bad_request(domain_name, aggregate, command, fields, raw, true, &e),
|
|
2177
|
+
};
|
|
2178
|
+
match dispatch::preview_request(host, &verb, args, caller).await {
|
|
2179
|
+
Ok(outcome) => respond(
|
|
2180
|
+
200,
|
|
2181
|
+
"application/json",
|
|
2182
|
+
&crate::validation::answer(&outcome.result).to_string(),
|
|
2183
|
+
),
|
|
2184
|
+
Err(e) => respond(500, "text/plain", &format!("{e:#}")),
|
|
2185
|
+
}
|
|
2186
|
+
}
|
|
2187
|
+
|
|
1991
2188
|
#[allow(clippy::too_many_arguments)]
|
|
1992
2189
|
async fn submit(
|
|
1993
2190
|
domain_name: &str,
|
|
@@ -1995,11 +2192,12 @@ async fn submit(
|
|
|
1995
2192
|
command: &Value,
|
|
1996
2193
|
fields: &[Field],
|
|
1997
2194
|
raw: &HashMap<String, String>,
|
|
1998
|
-
|
|
2195
|
+
store: &Store,
|
|
1999
2196
|
wasm_path: &Path,
|
|
2000
2197
|
config: &LineageConfig,
|
|
2001
2198
|
json_mode: bool,
|
|
2002
2199
|
invoker: &dyn LambdaInvoker,
|
|
2200
|
+
caller: dispatch::Caller<'_>,
|
|
2003
2201
|
) -> Value {
|
|
2004
2202
|
let agg = agg_name(aggregate);
|
|
2005
2203
|
let cname = command.get("name").and_then(|v| v.as_str()).unwrap_or("");
|
|
@@ -2012,13 +2210,12 @@ async fn submit(
|
|
|
2012
2210
|
Err(e) => return bad_request(domain_name, aggregate, command, fields, raw, json_mode, &e),
|
|
2013
2211
|
};
|
|
2014
2212
|
|
|
2015
|
-
//
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
Host::new(client, wasm_path, config, invoker),
|
|
2213
|
+
// Dispatched as the signed-in caller, checked against the role the command declares.
|
|
2214
|
+
let outcome = match dispatch::handle_request(
|
|
2215
|
+
Host::new(store, wasm_path, config, invoker),
|
|
2019
2216
|
&verb,
|
|
2020
2217
|
args,
|
|
2021
|
-
|
|
2218
|
+
caller,
|
|
2022
2219
|
)
|
|
2023
2220
|
.await
|
|
2024
2221
|
{
|
|
@@ -2414,6 +2611,20 @@ fn redirect_with_cookie(location: &str, cookie: &str) -> Value {
|
|
|
2414
2611
|
json!({"statusCode": 302, "headers": {"location": location}, "cookies": [cookie], "body": "", "isBase64Encoded": false})
|
|
2415
2612
|
}
|
|
2416
2613
|
|
|
2614
|
+
// `response` with one more `Set-Cookie` beside any it already carries.
|
|
2615
|
+
fn with_cookie(mut response: Value, cookie: &str) -> Value {
|
|
2616
|
+
match response.get_mut("cookies").and_then(Value::as_array_mut) {
|
|
2617
|
+
Some(cookies) => cookies.push(json!(cookie)),
|
|
2618
|
+
None => response["cookies"] = json!([cookie]),
|
|
2619
|
+
}
|
|
2620
|
+
response
|
|
2621
|
+
}
|
|
2622
|
+
|
|
2623
|
+
// Whether cookies this host sets carry `Secure` (see `cookie_flags`).
|
|
2624
|
+
fn secure_cookies() -> bool {
|
|
2625
|
+
cookie_flags() == COOKIE_FLAGS_SECURE
|
|
2626
|
+
}
|
|
2627
|
+
|
|
2417
2628
|
const DEFAULT_SITE_URL: &str = "http://localhost:4321";
|
|
2418
2629
|
|
|
2419
2630
|
/// The public site's origin: `SITE_URL`, else the local development address.
|
|
@@ -2441,6 +2652,8 @@ fn respond_with_cookie(status: u16, content_type: &str, body: &str, cookie: &str
|
|
|
2441
2652
|
#[cfg(test)]
|
|
2442
2653
|
mod tests {
|
|
2443
2654
|
use super::*;
|
|
2655
|
+
#[allow(unused_imports)]
|
|
2656
|
+
use crate::store::StoreFixtures;
|
|
2444
2657
|
|
|
2445
2658
|
// A record's id is user-supplied; left unescaped, a malicious one
|
|
2446
2659
|
// would render as live markup — these tests pin that it can't.
|
|
@@ -2851,6 +3064,190 @@ mod tests {
|
|
|
2851
3064
|
);
|
|
2852
3065
|
}
|
|
2853
3066
|
|
|
3067
|
+
// A form submitted through the generated UI dispatches as the signed-in caller, so a role the
|
|
3068
|
+
// command does not declare is refused as it is over the internal protocol.
|
|
3069
|
+
#[tokio::test]
|
|
3070
|
+
async fn a_form_submit_dispatches_as_the_signed_in_caller() {
|
|
3071
|
+
let client = crate::dispatch_tests::tests::memory_store();
|
|
3072
|
+
let wasm = crate::dispatch_tests::tests::wasm_path();
|
|
3073
|
+
let ir: Value = serde_json::from_str(
|
|
3074
|
+
&std::fs::read_to_string(
|
|
3075
|
+
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
|
3076
|
+
.join("../dist/banking.ir.json"),
|
|
3077
|
+
)
|
|
3078
|
+
.expect("hecks build_wasm writes banking.ir.json beside the wasm"),
|
|
3079
|
+
)
|
|
3080
|
+
.expect("valid IR");
|
|
3081
|
+
let lineage = LineageConfig {
|
|
3082
|
+
domain: "Banking".to_string(),
|
|
3083
|
+
era: None,
|
|
3084
|
+
mirrored: None,
|
|
3085
|
+
seal_every: None,
|
|
3086
|
+
};
|
|
3087
|
+
let invoker = crate::lambda_client::NeverInvoker;
|
|
3088
|
+
let submit_as = |role: &'static str, reference: &'static str| {
|
|
3089
|
+
let (client, wasm, ir, lineage, invoker) = (&client, &wasm, &ir, &lineage, &invoker);
|
|
3090
|
+
async move {
|
|
3091
|
+
let cookies = signed_in_cookies(role);
|
|
3092
|
+
let form = format!(
|
|
3093
|
+
"reference.value={reference}&name.given=Ada&name.family=Lovelace&email.address=ada%40example.com"
|
|
3094
|
+
);
|
|
3095
|
+
let host = Host::new(client, wasm, lineage, invoker);
|
|
3096
|
+
let path = "/Banking/Customer/Register.json";
|
|
3097
|
+
route(ir, "POST", path, &HashMap::new(), &form, &cookies, host).await
|
|
3098
|
+
}
|
|
3099
|
+
};
|
|
3100
|
+
|
|
3101
|
+
let teller = submit_as("Teller", "CUST-9201").await;
|
|
3102
|
+
assert_eq!(teller["statusCode"], 422, "{teller}");
|
|
3103
|
+
assert!(
|
|
3104
|
+
teller["body"]
|
|
3105
|
+
.as_str()
|
|
3106
|
+
.unwrap_or_default()
|
|
3107
|
+
.contains("Unauthorized"),
|
|
3108
|
+
"{teller}"
|
|
3109
|
+
);
|
|
3110
|
+
let clerk = submit_as("Branch clerk", "CUST-9202").await;
|
|
3111
|
+
assert_eq!(clerk["statusCode"], 201, "{clerk}");
|
|
3112
|
+
}
|
|
3113
|
+
|
|
3114
|
+
// `?dry_run=1` on the JSON command route: the signed-in caller's verdict, as the client envelope,
|
|
3115
|
+
// with nothing kept.
|
|
3116
|
+
#[tokio::test]
|
|
3117
|
+
async fn a_preview_of_a_submitted_form_answers_the_envelope_as_the_signed_in_caller() {
|
|
3118
|
+
let client = crate::dispatch_tests::tests::memory_store();
|
|
3119
|
+
let wasm = crate::dispatch_tests::tests::wasm_path();
|
|
3120
|
+
let ir: Value = serde_json::from_str(
|
|
3121
|
+
&std::fs::read_to_string(
|
|
3122
|
+
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
|
3123
|
+
.join("../dist/banking.ir.json"),
|
|
3124
|
+
)
|
|
3125
|
+
.expect("hecks build_wasm writes banking.ir.json beside the wasm"),
|
|
3126
|
+
)
|
|
3127
|
+
.expect("valid IR");
|
|
3128
|
+
let lineage = LineageConfig {
|
|
3129
|
+
domain: "Banking".to_string(),
|
|
3130
|
+
era: None,
|
|
3131
|
+
mirrored: None,
|
|
3132
|
+
seal_every: None,
|
|
3133
|
+
};
|
|
3134
|
+
let invoker = crate::lambda_client::NeverInvoker;
|
|
3135
|
+
let post = |role: &'static str, asked: &'static str| {
|
|
3136
|
+
let (client, wasm, ir, lineage, invoker) = (&client, &wasm, &ir, &lineage, &invoker);
|
|
3137
|
+
async move {
|
|
3138
|
+
let cookies = signed_in_cookies(role);
|
|
3139
|
+
let form = "reference.value=CUST-9301&name.given=Ada&name.family=Lovelace&email.address=ada%40example.com";
|
|
3140
|
+
let query = HashMap::from([("dry_run".to_string(), asked.to_string())]);
|
|
3141
|
+
let host = Host::new(client, wasm, lineage, invoker);
|
|
3142
|
+
let path = "/Banking/Customer/Register.json";
|
|
3143
|
+
route(ir, "POST", path, &query, form, &cookies, host).await
|
|
3144
|
+
}
|
|
3145
|
+
};
|
|
3146
|
+
let verdict = |response: &Value| -> Value {
|
|
3147
|
+
serde_json::from_str(response["body"].as_str().unwrap_or("{}")).expect("a JSON body")
|
|
3148
|
+
};
|
|
3149
|
+
|
|
3150
|
+
let teller = post("Teller", "1").await;
|
|
3151
|
+
assert_eq!(teller["statusCode"], 200, "{teller}");
|
|
3152
|
+
let refusal = &verdict(&teller)["refusals"][0];
|
|
3153
|
+
assert_eq!(
|
|
3154
|
+
(refusal["kind"].as_str(), refusal["stage"].as_str()),
|
|
3155
|
+
(Some("unauthorized"), Some("role"))
|
|
3156
|
+
);
|
|
3157
|
+
|
|
3158
|
+
let clerk = post("Branch clerk", "1").await;
|
|
3159
|
+
assert_eq!(clerk["statusCode"], 200, "{clerk}");
|
|
3160
|
+
assert_eq!(
|
|
3161
|
+
verdict(&clerk),
|
|
3162
|
+
serde_json::json!({ "ok": true, "refusals": [] })
|
|
3163
|
+
);
|
|
3164
|
+
|
|
3165
|
+
// Kept nothing: the same registration, for real, is still accepted.
|
|
3166
|
+
let real = post("Branch clerk", "0").await;
|
|
3167
|
+
assert_eq!(real["statusCode"], 201, "{real}");
|
|
3168
|
+
}
|
|
3169
|
+
|
|
3170
|
+
// `?schema=1` on a command's JSON route: the JSON Schema the build wrote beside the IR.
|
|
3171
|
+
#[tokio::test]
|
|
3172
|
+
async fn a_command_json_route_serves_its_schema_when_asked() {
|
|
3173
|
+
let client = crate::dispatch_tests::tests::memory_store();
|
|
3174
|
+
let wasm = crate::dispatch_tests::tests::wasm_path();
|
|
3175
|
+
let ir_path =
|
|
3176
|
+
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../dist/banking.ir.json");
|
|
3177
|
+
let ir: Value =
|
|
3178
|
+
serde_json::from_str(&std::fs::read_to_string(&ir_path).expect("banking.ir.json"))
|
|
3179
|
+
.expect("valid IR");
|
|
3180
|
+
crate::ir::install_schemas(ir_path.to_str());
|
|
3181
|
+
let lineage = LineageConfig {
|
|
3182
|
+
domain: "Banking".to_string(),
|
|
3183
|
+
era: None,
|
|
3184
|
+
mirrored: None,
|
|
3185
|
+
seal_every: None,
|
|
3186
|
+
};
|
|
3187
|
+
let invoker = crate::lambda_client::NeverInvoker;
|
|
3188
|
+
let host = Host::new(&client, &wasm, &lineage, &invoker);
|
|
3189
|
+
let cookies = signed_in_cookies("Branch clerk");
|
|
3190
|
+
let query = HashMap::from([("schema".to_string(), "1".to_string())]);
|
|
3191
|
+
|
|
3192
|
+
let found = route(
|
|
3193
|
+
&ir,
|
|
3194
|
+
"GET",
|
|
3195
|
+
"/Banking/Customer/Register.json",
|
|
3196
|
+
&query,
|
|
3197
|
+
"",
|
|
3198
|
+
&cookies,
|
|
3199
|
+
host,
|
|
3200
|
+
)
|
|
3201
|
+
.await;
|
|
3202
|
+
assert_eq!(found["statusCode"], 200, "{found}");
|
|
3203
|
+
let schema: Value =
|
|
3204
|
+
serde_json::from_str(found["body"].as_str().unwrap_or("{}")).expect("JSON");
|
|
3205
|
+
assert_eq!(
|
|
3206
|
+
schema["$schema"],
|
|
3207
|
+
"https://json-schema.org/draft/2020-12/schema"
|
|
3208
|
+
);
|
|
3209
|
+
assert!(schema["properties"].get("reference").is_some(), "{schema}");
|
|
3210
|
+
|
|
3211
|
+
let host = Host::new(&client, &wasm, &lineage, &invoker);
|
|
3212
|
+
let plain = route(
|
|
3213
|
+
&ir,
|
|
3214
|
+
"GET",
|
|
3215
|
+
"/Banking/Customer/Register.json",
|
|
3216
|
+
&HashMap::new(),
|
|
3217
|
+
"",
|
|
3218
|
+
&cookies,
|
|
3219
|
+
host,
|
|
3220
|
+
)
|
|
3221
|
+
.await;
|
|
3222
|
+
assert_eq!(plain["statusCode"], 200);
|
|
3223
|
+
assert!(
|
|
3224
|
+
plain["body"].as_str().unwrap_or("").contains("\"name\""),
|
|
3225
|
+
"the descriptor is unchanged"
|
|
3226
|
+
);
|
|
3227
|
+
}
|
|
3228
|
+
|
|
3229
|
+
// A session cookie signed with the host's own secret, for a caller Governance knows nothing of.
|
|
3230
|
+
fn signed_in_cookies(role: &str) -> HashMap<String, String> {
|
|
3231
|
+
if crate::secrets::var("SESSION_SECRET")
|
|
3232
|
+
.unwrap_or_default()
|
|
3233
|
+
.is_empty()
|
|
3234
|
+
{
|
|
3235
|
+
crate::secrets::provide("SESSION_SECRET", "a-session-secret-for-tests".to_string());
|
|
3236
|
+
}
|
|
3237
|
+
let session = Session {
|
|
3238
|
+
identity_id: String::new(),
|
|
3239
|
+
email: "clerk@example.com".to_string(),
|
|
3240
|
+
name: "Clerk".to_string(),
|
|
3241
|
+
role: Some(role.to_string()),
|
|
3242
|
+
};
|
|
3243
|
+
let mut cookies = HashMap::new();
|
|
3244
|
+
cookies.insert(
|
|
3245
|
+
"session".to_string(),
|
|
3246
|
+
auth::session_cookie(&session_secret(), &session),
|
|
3247
|
+
);
|
|
3248
|
+
cookies
|
|
3249
|
+
}
|
|
3250
|
+
|
|
2854
3251
|
#[test]
|
|
2855
3252
|
fn extract_args_surfaces_a_collision_as_an_error_rather_than_panicking() {
|
|
2856
3253
|
let scalar_field = Field {
|
|
@@ -2925,9 +3322,9 @@ mod tests {
|
|
|
2925
3322
|
|
|
2926
3323
|
// scratch_db provisions a real Postgres schema; only the Stripe hop
|
|
2927
3324
|
// in payments.rs's own tests is faked, aimed at a local recording server.
|
|
2928
|
-
use tokio_postgres::NoTls;
|
|
3325
|
+
use tokio_postgres::{Client, NoTls};
|
|
2929
3326
|
|
|
2930
|
-
pub(super) async fn scratch_db(name: &str) ->
|
|
3327
|
+
pub(super) async fn scratch_db(name: &str) -> Store {
|
|
2931
3328
|
let (admin, conn) = tokio_postgres::connect(&crate::test_pg::conninfo("postgres"), NoTls)
|
|
2932
3329
|
.await
|
|
2933
3330
|
.expect("connect to postgres");
|
|
@@ -2949,8 +3346,10 @@ mod tests {
|
|
|
2949
3346
|
tokio::spawn(async move {
|
|
2950
3347
|
let _ = conn.await;
|
|
2951
3348
|
});
|
|
2952
|
-
crate::
|
|
2953
|
-
|
|
3349
|
+
crate::store::journal_sql::ensure_schema(&client)
|
|
3350
|
+
.await
|
|
3351
|
+
.unwrap();
|
|
3352
|
+
crate::store::postgres_store(client)
|
|
2954
3353
|
}
|
|
2955
3354
|
|
|
2956
3355
|
// Duplicates dispatch.rs's own provision_lineage shape rather than
|
|
@@ -2975,7 +3374,7 @@ mod tests {
|
|
|
2975
3374
|
.await
|
|
2976
3375
|
.unwrap();
|
|
2977
3376
|
|
|
2978
|
-
let journal_table = format!("hecks_journal_{}", crate::
|
|
3377
|
+
let journal_table = format!("hecks_journal_{}", crate::store::journal_sql::snake(domain));
|
|
2979
3378
|
client
|
|
2980
3379
|
.batch_execute(&format!(
|
|
2981
3380
|
"CREATE TABLE IF NOT EXISTS \"{journal_table}\" (
|
|
@@ -2993,9 +3392,12 @@ mod tests {
|
|
|
2993
3392
|
for name in aggregate_storage_names {
|
|
2994
3393
|
// domain-qualified (docs/decisions/0059) — matches what a real
|
|
2995
3394
|
// `journal::append_lineage_mutation` write actually targets now.
|
|
2996
|
-
let snapshot_table = crate::
|
|
3395
|
+
let snapshot_table = crate::store::journal_sql::qualified_name(
|
|
2997
3396
|
domain,
|
|
2998
|
-
&format!(
|
|
3397
|
+
&format!(
|
|
3398
|
+
"{}_head_snapshot_{era}",
|
|
3399
|
+
crate::store::journal_sql::snake(name)
|
|
3400
|
+
),
|
|
2999
3401
|
);
|
|
3000
3402
|
client
|
|
3001
3403
|
.batch_execute(&format!("CREATE TABLE IF NOT EXISTS \"{snapshot_table}\" (id text PRIMARY KEY, ordinal bigint NOT NULL, state jsonb NOT NULL)"))
|
|
@@ -3098,7 +3500,7 @@ mod tests {
|
|
|
3098
3500
|
// Same head-view shape auth.rs's own tests build for the Membership
|
|
3099
3501
|
// aggregate, seeded with one granted, linked admin and one admitted
|
|
3100
3502
|
// person with no access yet.
|
|
3101
|
-
async fn scratch_members_db(name: &str) -> (
|
|
3503
|
+
async fn scratch_members_db(name: &str) -> (Store, Value) {
|
|
3102
3504
|
let client = scratch_db(name).await;
|
|
3103
3505
|
{
|
|
3104
3506
|
let guard = client.lock().await;
|
|
@@ -3154,11 +3556,11 @@ mod tests {
|
|
|
3154
3556
|
assert_eq!(names, ["amy", "Zed"], "sorted case-insensitively by name");
|
|
3155
3557
|
assert_eq!(
|
|
3156
3558
|
people[1],
|
|
3157
|
-
json!({"name": "Zed", "email": "zed@example.com", "role": "Admin", "linked": true, "granted": true, "disabled": false, "identity_id": "id-1"})
|
|
3559
|
+
json!({"name": "Zed", "email": "zed@example.com", "role": "Admin", "linked": true, "granted": true, "disabled": false, "access_expires_at": null, "expired": false, "identity_id": "id-1"})
|
|
3158
3560
|
);
|
|
3159
3561
|
assert_eq!(
|
|
3160
3562
|
people[0],
|
|
3161
|
-
json!({"name": "amy", "email": "amy@example.com", "role": null, "linked": false, "granted": false, "disabled": false, "identity_id": null})
|
|
3563
|
+
json!({"name": "amy", "email": "amy@example.com", "role": null, "linked": false, "granted": false, "disabled": false, "access_expires_at": null, "expired": false, "identity_id": null})
|
|
3162
3564
|
);
|
|
3163
3565
|
}
|
|
3164
3566
|
|
|
@@ -3249,6 +3651,7 @@ mod tests {
|
|
|
3249
3651
|
domain: "Acme".to_string(),
|
|
3250
3652
|
era: Some(1),
|
|
3251
3653
|
mirrored: None,
|
|
3654
|
+
seal_every: None,
|
|
3252
3655
|
}
|
|
3253
3656
|
}
|
|
3254
3657
|
|
|
@@ -3498,7 +3901,7 @@ mod tests {
|
|
|
3498
3901
|
body: &str,
|
|
3499
3902
|
cookies: &HashMap<String, String>,
|
|
3500
3903
|
secret: &str,
|
|
3501
|
-
client: &
|
|
3904
|
+
client: &Store,
|
|
3502
3905
|
domain_ir: &Value,
|
|
3503
3906
|
) -> Value {
|
|
3504
3907
|
admin_members_route(
|
|
@@ -3642,7 +4045,7 @@ mod tests {
|
|
|
3642
4045
|
let created: Value = serde_json::from_str(response["body"].as_str().unwrap()).unwrap();
|
|
3643
4046
|
assert_eq!(
|
|
3644
4047
|
created,
|
|
3645
|
-
json!({"name": "New Person", "email": "new@example.com", "role": "Admin", "linked": false, "granted": true, "disabled": false, "identity_id": null})
|
|
4048
|
+
json!({"name": "New Person", "email": "new@example.com", "role": "Admin", "linked": false, "granted": true, "disabled": false, "access_expires_at": null, "expired": false, "identity_id": null})
|
|
3646
4049
|
);
|
|
3647
4050
|
|
|
3648
4051
|
let listing = members_route(&domain_ir, &cookies, secret, &client).await;
|
|
@@ -3671,7 +4074,7 @@ mod tests {
|
|
|
3671
4074
|
|
|
3672
4075
|
// The scratch members database with amy also granted Admin, so two
|
|
3673
4076
|
// active admins exist and one can act on the other.
|
|
3674
|
-
async fn scratch_two_admins(name: &str) -> (
|
|
4077
|
+
async fn scratch_two_admins(name: &str) -> (Store, Value) {
|
|
3675
4078
|
let (client, domain_ir) = scratch_members_db(name).await;
|
|
3676
4079
|
let granted = auth::grant_access(
|
|
3677
4080
|
&client,
|
|
@@ -3688,7 +4091,7 @@ mod tests {
|
|
|
3688
4091
|
}
|
|
3689
4092
|
|
|
3690
4093
|
async fn switch_access(
|
|
3691
|
-
client: &
|
|
4094
|
+
client: &Store,
|
|
3692
4095
|
domain_ir: &Value,
|
|
3693
4096
|
caller: &str,
|
|
3694
4097
|
email: &str,
|
|
@@ -3713,7 +4116,7 @@ mod tests {
|
|
|
3713
4116
|
body["error"].as_str().unwrap_or_default().to_string()
|
|
3714
4117
|
}
|
|
3715
4118
|
|
|
3716
|
-
async fn person(client: &
|
|
4119
|
+
async fn person(client: &Store, domain_ir: &Value, email: &str) -> Value {
|
|
3717
4120
|
let people = auth::all_people(client, domain_ir).await.unwrap();
|
|
3718
4121
|
people
|
|
3719
4122
|
.into_iter()
|
|
@@ -3721,7 +4124,7 @@ mod tests {
|
|
|
3721
4124
|
.unwrap_or_else(|| panic!("{email} is not listed"))
|
|
3722
4125
|
}
|
|
3723
4126
|
|
|
3724
|
-
async fn journal_rows(client: &
|
|
4127
|
+
async fn journal_rows(client: &Store, id: &str) -> i64 {
|
|
3725
4128
|
let guard = client.lock().await;
|
|
3726
4129
|
guard
|
|
3727
4130
|
.query_one(
|
|
@@ -3835,7 +4238,7 @@ mod tests {
|
|
|
3835
4238
|
}
|
|
3836
4239
|
|
|
3837
4240
|
async fn set_role(
|
|
3838
|
-
client: &
|
|
4241
|
+
client: &Store,
|
|
3839
4242
|
domain_ir: &Value,
|
|
3840
4243
|
caller: &str,
|
|
3841
4244
|
email: &str,
|
|
@@ -4439,6 +4842,7 @@ mod tests {
|
|
|
4439
4842
|
domain: "Studio".to_string(),
|
|
4440
4843
|
era: Some(1),
|
|
4441
4844
|
mirrored: None,
|
|
4845
|
+
seal_every: None,
|
|
4442
4846
|
};
|
|
4443
4847
|
// The wasm path doesn't exist: every refusal below must come before any registration read.
|
|
4444
4848
|
let wasm_path = Path::new("does-not-exist.wasm");
|
|
@@ -4530,6 +4934,7 @@ mod tests {
|
|
|
4530
4934
|
domain: "Studio".to_string(),
|
|
4531
4935
|
era: Some(1),
|
|
4532
4936
|
mirrored: None,
|
|
4937
|
+
seal_every: None,
|
|
4533
4938
|
};
|
|
4534
4939
|
// No wasm at all: clearing the admin gate then fails with a 500, never 401/403.
|
|
4535
4940
|
let missing_wasm = Path::new("does-not-exist.wasm");
|
|
@@ -4874,6 +5279,931 @@ mod tests {
|
|
|
4874
5279
|
);
|
|
4875
5280
|
}
|
|
4876
5281
|
|
|
5282
|
+
// ---- time-boxed access: a window on the row that ends by itself ----
|
|
5283
|
+
|
|
5284
|
+
const PAST: &str = "2020-01-01T00:00:00Z";
|
|
5285
|
+
const FUTURE: &str = "2099-01-01T00:00:00Z";
|
|
5286
|
+
|
|
5287
|
+
// Writes `access_expires_at` straight onto a row, the way a window that
|
|
5288
|
+
// ran out (or was mangled) looks to the host, whatever wrote it.
|
|
5289
|
+
async fn set_window(client: &Store, email: &str, window: Value) {
|
|
5290
|
+
client
|
|
5291
|
+
.lock()
|
|
5292
|
+
.await
|
|
5293
|
+
.execute(
|
|
5294
|
+
"UPDATE acme_member_head_snapshot_1 SET state = jsonb_set(state, '{access_expires_at}', $2::jsonb) WHERE id = $1",
|
|
5295
|
+
&[&email, &window],
|
|
5296
|
+
)
|
|
5297
|
+
.await
|
|
5298
|
+
.unwrap();
|
|
5299
|
+
}
|
|
5300
|
+
|
|
5301
|
+
fn form_grant(email: &str, role: &str, days: &str) -> String {
|
|
5302
|
+
format!("email={email}&role={role}&expires_in_days={days}")
|
|
5303
|
+
}
|
|
5304
|
+
|
|
5305
|
+
const NEEDS_OPEN: &str = "a site must keep at least one admin without an end date";
|
|
5306
|
+
|
|
5307
|
+
// Posts a grant through the members form as `caller`; returns the status.
|
|
5308
|
+
async fn form_status(
|
|
5309
|
+
client: &Store,
|
|
5310
|
+
domain_ir: &Value,
|
|
5311
|
+
caller: &str,
|
|
5312
|
+
email: &str,
|
|
5313
|
+
role: &str,
|
|
5314
|
+
days: &str,
|
|
5315
|
+
) -> Value {
|
|
5316
|
+
let secret = "s3cret";
|
|
5317
|
+
members_page(
|
|
5318
|
+
"POST",
|
|
5319
|
+
&form_grant(email, role, days),
|
|
5320
|
+
&session_cookies(secret, caller),
|
|
5321
|
+
secret,
|
|
5322
|
+
client,
|
|
5323
|
+
domain_ir,
|
|
5324
|
+
)
|
|
5325
|
+
.await
|
|
5326
|
+
}
|
|
5327
|
+
|
|
5328
|
+
async fn expires_of(client: &Store, domain_ir: &Value, email: &str) -> Value {
|
|
5329
|
+
person(client, domain_ir, email).await["access_expires_at"].clone()
|
|
5330
|
+
}
|
|
5331
|
+
|
|
5332
|
+
#[tokio::test]
|
|
5333
|
+
async fn a_lone_open_ended_admin_cannot_be_given_a_window_or_a_lesser_role() {
|
|
5334
|
+
let (client, domain_ir) = scratch_members_db("hecks_host_web_test_open_lone").await;
|
|
5335
|
+
let before = journal_rows(&client, "zed@example.com").await;
|
|
5336
|
+
|
|
5337
|
+
for days in ["7", "365"] {
|
|
5338
|
+
let r = form_status(
|
|
5339
|
+
&client,
|
|
5340
|
+
&domain_ir,
|
|
5341
|
+
"zed@example.com",
|
|
5342
|
+
"zed%40example.com",
|
|
5343
|
+
"Admin",
|
|
5344
|
+
days,
|
|
5345
|
+
)
|
|
5346
|
+
.await;
|
|
5347
|
+
assert_eq!(r["statusCode"], 409, "{r:?}");
|
|
5348
|
+
assert!(r["body"].as_str().unwrap().contains(NEEDS_OPEN), "{r:?}");
|
|
5349
|
+
}
|
|
5350
|
+
// The same grant that drops the admin role also takes the open-ended admin away.
|
|
5351
|
+
let r = form_status(
|
|
5352
|
+
&client,
|
|
5353
|
+
&domain_ir,
|
|
5354
|
+
"zed@example.com",
|
|
5355
|
+
"zed%40example.com",
|
|
5356
|
+
"Member",
|
|
5357
|
+
"",
|
|
5358
|
+
)
|
|
5359
|
+
.await;
|
|
5360
|
+
assert_eq!(r["statusCode"], 409, "{r:?}");
|
|
5361
|
+
assert_eq!(
|
|
5362
|
+
journal_rows(&client, "zed@example.com").await,
|
|
5363
|
+
before,
|
|
5364
|
+
"nothing was written"
|
|
5365
|
+
);
|
|
5366
|
+
assert_eq!(
|
|
5367
|
+
expires_of(&client, &domain_ir, "zed@example.com").await,
|
|
5368
|
+
Value::Null
|
|
5369
|
+
);
|
|
5370
|
+
|
|
5371
|
+
// A plain grant that keeps the role is fine, and so is anything for a non-admin.
|
|
5372
|
+
let r = form_status(
|
|
5373
|
+
&client,
|
|
5374
|
+
&domain_ir,
|
|
5375
|
+
"zed@example.com",
|
|
5376
|
+
"zed%40example.com",
|
|
5377
|
+
"Owner",
|
|
5378
|
+
"",
|
|
5379
|
+
)
|
|
5380
|
+
.await;
|
|
5381
|
+
assert_eq!(r["statusCode"], 302, "{r:?}");
|
|
5382
|
+
let r = form_status(
|
|
5383
|
+
&client,
|
|
5384
|
+
&domain_ir,
|
|
5385
|
+
"zed@example.com",
|
|
5386
|
+
"amy%40example.com",
|
|
5387
|
+
"Member",
|
|
5388
|
+
"5",
|
|
5389
|
+
)
|
|
5390
|
+
.await;
|
|
5391
|
+
assert_eq!(
|
|
5392
|
+
r["statusCode"], 302,
|
|
5393
|
+
"a non-admin takes a window freely: {r:?}"
|
|
5394
|
+
);
|
|
5395
|
+
assert!(expires_of(&client, &domain_ir, "amy@example.com")
|
|
5396
|
+
.await
|
|
5397
|
+
.is_string());
|
|
5398
|
+
}
|
|
5399
|
+
|
|
5400
|
+
#[tokio::test]
|
|
5401
|
+
async fn with_two_open_ended_admins_either_may_take_a_window_but_not_both() {
|
|
5402
|
+
let (client, domain_ir) = scratch_two_admins("hecks_host_web_test_open_two").await;
|
|
5403
|
+
let r = form_status(
|
|
5404
|
+
&client,
|
|
5405
|
+
&domain_ir,
|
|
5406
|
+
"zed@example.com",
|
|
5407
|
+
"amy%40example.com",
|
|
5408
|
+
"Admin",
|
|
5409
|
+
"10",
|
|
5410
|
+
)
|
|
5411
|
+
.await;
|
|
5412
|
+
assert_eq!(r["statusCode"], 302, "{r:?}");
|
|
5413
|
+
assert!(expires_of(&client, &domain_ir, "amy@example.com")
|
|
5414
|
+
.await
|
|
5415
|
+
.is_string());
|
|
5416
|
+
|
|
5417
|
+
// zed is now the one open-ended admin left.
|
|
5418
|
+
let r = form_status(
|
|
5419
|
+
&client,
|
|
5420
|
+
&domain_ir,
|
|
5421
|
+
"amy@example.com",
|
|
5422
|
+
"zed%40example.com",
|
|
5423
|
+
"Admin",
|
|
5424
|
+
"10",
|
|
5425
|
+
)
|
|
5426
|
+
.await;
|
|
5427
|
+
assert_eq!(r["statusCode"], 409, "{r:?}");
|
|
5428
|
+
assert_eq!(
|
|
5429
|
+
expires_of(&client, &domain_ir, "zed@example.com").await,
|
|
5430
|
+
Value::Null
|
|
5431
|
+
);
|
|
5432
|
+
}
|
|
5433
|
+
|
|
5434
|
+
#[tokio::test]
|
|
5435
|
+
async fn an_open_ended_admin_beside_a_windowed_one_is_protected_on_every_path() {
|
|
5436
|
+
let (client, domain_ir) = scratch_two_admins("hecks_host_web_test_open_beside").await;
|
|
5437
|
+
set_window(&client, "amy@example.com", json!(FUTURE)).await;
|
|
5438
|
+
|
|
5439
|
+
// Renewing the windowed admin, or lifting the window, is always allowed.
|
|
5440
|
+
for days in ["30", ""] {
|
|
5441
|
+
let r = form_status(
|
|
5442
|
+
&client,
|
|
5443
|
+
&domain_ir,
|
|
5444
|
+
"zed@example.com",
|
|
5445
|
+
"amy%40example.com",
|
|
5446
|
+
"Admin",
|
|
5447
|
+
days,
|
|
5448
|
+
)
|
|
5449
|
+
.await;
|
|
5450
|
+
assert_eq!(r["statusCode"], 302, "{r:?}");
|
|
5451
|
+
}
|
|
5452
|
+
set_window(&client, "amy@example.com", json!(FUTURE)).await;
|
|
5453
|
+
|
|
5454
|
+
// Windowing, disabling or demoting zed would leave only a window that will end.
|
|
5455
|
+
let r = form_status(
|
|
5456
|
+
&client,
|
|
5457
|
+
&domain_ir,
|
|
5458
|
+
"amy@example.com",
|
|
5459
|
+
"zed%40example.com",
|
|
5460
|
+
"Admin",
|
|
5461
|
+
"9",
|
|
5462
|
+
)
|
|
5463
|
+
.await;
|
|
5464
|
+
assert_eq!(r["statusCode"], 409, "{r:?}");
|
|
5465
|
+
let r = switch_access(
|
|
5466
|
+
&client,
|
|
5467
|
+
&domain_ir,
|
|
5468
|
+
"amy@example.com",
|
|
5469
|
+
"zed@example.com",
|
|
5470
|
+
true,
|
|
5471
|
+
)
|
|
5472
|
+
.await;
|
|
5473
|
+
assert_eq!(r["statusCode"], 409, "{r:?}");
|
|
5474
|
+
assert_eq!(error_of(&r), NEEDS_OPEN);
|
|
5475
|
+
let r = set_role(
|
|
5476
|
+
&client,
|
|
5477
|
+
&domain_ir,
|
|
5478
|
+
"amy@example.com",
|
|
5479
|
+
"zed@example.com",
|
|
5480
|
+
"Member",
|
|
5481
|
+
)
|
|
5482
|
+
.await;
|
|
5483
|
+
assert_eq!(r["statusCode"], 409, "{r:?}");
|
|
5484
|
+
assert_eq!(error_of(&r), NEEDS_OPEN);
|
|
5485
|
+
let zed = person(&client, &domain_ir, "zed@example.com").await;
|
|
5486
|
+
assert_eq!(
|
|
5487
|
+
(zed["role"].clone(), zed["disabled"].clone()),
|
|
5488
|
+
(json!("Admin"), json!(false))
|
|
5489
|
+
);
|
|
5490
|
+
|
|
5491
|
+
// The windowed admin itself may be disabled, demoted or re-enabled: zed stays.
|
|
5492
|
+
let r = switch_access(
|
|
5493
|
+
&client,
|
|
5494
|
+
&domain_ir,
|
|
5495
|
+
"zed@example.com",
|
|
5496
|
+
"amy@example.com",
|
|
5497
|
+
true,
|
|
5498
|
+
)
|
|
5499
|
+
.await;
|
|
5500
|
+
assert_eq!(r["statusCode"], 200, "{r:?}");
|
|
5501
|
+
let r = switch_access(
|
|
5502
|
+
&client,
|
|
5503
|
+
&domain_ir,
|
|
5504
|
+
"zed@example.com",
|
|
5505
|
+
"amy@example.com",
|
|
5506
|
+
false,
|
|
5507
|
+
)
|
|
5508
|
+
.await;
|
|
5509
|
+
assert_eq!(r["statusCode"], 200, "{r:?}");
|
|
5510
|
+
let r = set_role(
|
|
5511
|
+
&client,
|
|
5512
|
+
&domain_ir,
|
|
5513
|
+
"zed@example.com",
|
|
5514
|
+
"amy@example.com",
|
|
5515
|
+
"Member",
|
|
5516
|
+
)
|
|
5517
|
+
.await;
|
|
5518
|
+
assert_eq!(r["statusCode"], 200, "{r:?}");
|
|
5519
|
+
}
|
|
5520
|
+
|
|
5521
|
+
#[tokio::test]
|
|
5522
|
+
async fn a_lapsed_or_unreadable_admin_is_not_an_open_ended_active_one() {
|
|
5523
|
+
let (client, domain_ir) = scratch_two_admins("hecks_host_web_test_open_lapsed").await;
|
|
5524
|
+
set_window(&client, "amy@example.com", json!(PAST)).await;
|
|
5525
|
+
// amy's window ended, so zed is the only active admin and is protected.
|
|
5526
|
+
let r = form_status(
|
|
5527
|
+
&client,
|
|
5528
|
+
&domain_ir,
|
|
5529
|
+
"zed@example.com",
|
|
5530
|
+
"zed%40example.com",
|
|
5531
|
+
"Admin",
|
|
5532
|
+
"3",
|
|
5533
|
+
)
|
|
5534
|
+
.await;
|
|
5535
|
+
assert_eq!(r["statusCode"], 409, "{r:?}");
|
|
5536
|
+
let r = switch_access(
|
|
5537
|
+
&client,
|
|
5538
|
+
&domain_ir,
|
|
5539
|
+
"zed@example.com",
|
|
5540
|
+
"amy@example.com",
|
|
5541
|
+
true,
|
|
5542
|
+
)
|
|
5543
|
+
.await;
|
|
5544
|
+
assert_eq!(
|
|
5545
|
+
r["statusCode"], 200,
|
|
5546
|
+
"disabling an admin who already lapsed is harmless: {r:?}"
|
|
5547
|
+
);
|
|
5548
|
+
|
|
5549
|
+
// An unreadable window fails closed: zed is lapsed, so amy is the one left.
|
|
5550
|
+
let (client, domain_ir) = scratch_two_admins("hecks_host_web_test_open_unreadable").await;
|
|
5551
|
+
set_window(&client, "zed@example.com", json!("whenever")).await;
|
|
5552
|
+
let r = form_status(
|
|
5553
|
+
&client,
|
|
5554
|
+
&domain_ir,
|
|
5555
|
+
"amy@example.com",
|
|
5556
|
+
"amy%40example.com",
|
|
5557
|
+
"Admin",
|
|
5558
|
+
"3",
|
|
5559
|
+
)
|
|
5560
|
+
.await;
|
|
5561
|
+
assert_eq!(r["statusCode"], 409, "{r:?}");
|
|
5562
|
+
let r = set_role(
|
|
5563
|
+
&client,
|
|
5564
|
+
&domain_ir,
|
|
5565
|
+
"amy@example.com",
|
|
5566
|
+
"amy@example.com",
|
|
5567
|
+
"Member",
|
|
5568
|
+
)
|
|
5569
|
+
.await;
|
|
5570
|
+
assert_eq!(r["statusCode"], 409, "{r:?}");
|
|
5571
|
+
// Renewing the unreadable row, or clearing it with a plain grant, is allowed.
|
|
5572
|
+
let r = form_status(
|
|
5573
|
+
&client,
|
|
5574
|
+
&domain_ir,
|
|
5575
|
+
"amy@example.com",
|
|
5576
|
+
"zed%40example.com",
|
|
5577
|
+
"Admin",
|
|
5578
|
+
"3",
|
|
5579
|
+
)
|
|
5580
|
+
.await;
|
|
5581
|
+
assert_eq!(r["statusCode"], 302, "{r:?}");
|
|
5582
|
+
set_window(&client, "zed@example.com", json!("whenever")).await;
|
|
5583
|
+
let r = form_status(
|
|
5584
|
+
&client,
|
|
5585
|
+
&domain_ir,
|
|
5586
|
+
"amy@example.com",
|
|
5587
|
+
"zed%40example.com",
|
|
5588
|
+
"Admin",
|
|
5589
|
+
"",
|
|
5590
|
+
)
|
|
5591
|
+
.await;
|
|
5592
|
+
assert_eq!(r["statusCode"], 302, "{r:?}");
|
|
5593
|
+
assert_eq!(
|
|
5594
|
+
expires_of(&client, &domain_ir, "zed@example.com").await,
|
|
5595
|
+
Value::Null
|
|
5596
|
+
);
|
|
5597
|
+
}
|
|
5598
|
+
|
|
5599
|
+
#[tokio::test]
|
|
5600
|
+
async fn a_client_admin_stays_open_ended_while_an_operator_works_on_a_window() {
|
|
5601
|
+
let (client, domain_ir) = scratch_two_admins("hecks_host_web_test_open_operator").await;
|
|
5602
|
+
// amy is the operator, with a window; zed is the client's open-ended admin.
|
|
5603
|
+
let r = form_status(
|
|
5604
|
+
&client,
|
|
5605
|
+
&domain_ir,
|
|
5606
|
+
"zed@example.com",
|
|
5607
|
+
"amy%40example.com",
|
|
5608
|
+
"Admin",
|
|
5609
|
+
"30",
|
|
5610
|
+
)
|
|
5611
|
+
.await;
|
|
5612
|
+
assert_eq!(r["statusCode"], 302, "{r:?}");
|
|
5613
|
+
let r = form_status(
|
|
5614
|
+
&client,
|
|
5615
|
+
&domain_ir,
|
|
5616
|
+
"zed@example.com",
|
|
5617
|
+
"amy%40example.com",
|
|
5618
|
+
"Admin",
|
|
5619
|
+
"60",
|
|
5620
|
+
)
|
|
5621
|
+
.await;
|
|
5622
|
+
assert_eq!(r["statusCode"], 302, "the operator's window renews: {r:?}");
|
|
5623
|
+
let r = form_status(
|
|
5624
|
+
&client,
|
|
5625
|
+
&domain_ir,
|
|
5626
|
+
"amy@example.com",
|
|
5627
|
+
"zed%40example.com",
|
|
5628
|
+
"Admin",
|
|
5629
|
+
"1",
|
|
5630
|
+
)
|
|
5631
|
+
.await;
|
|
5632
|
+
assert_eq!(
|
|
5633
|
+
r["statusCode"], 409,
|
|
5634
|
+
"the operator cannot window the client admin: {r:?}"
|
|
5635
|
+
);
|
|
5636
|
+
// Even after the operator's window ends, the client admin still opens the gate.
|
|
5637
|
+
set_window(&client, "amy@example.com", json!(PAST)).await;
|
|
5638
|
+
assert!(
|
|
5639
|
+
auth::caller_is_admin(&client, &domain_ir, "zed@example.com")
|
|
5640
|
+
.await
|
|
5641
|
+
.unwrap()
|
|
5642
|
+
);
|
|
5643
|
+
assert!(
|
|
5644
|
+
!auth::caller_is_admin(&client, &domain_ir, "amy@example.com")
|
|
5645
|
+
.await
|
|
5646
|
+
.unwrap()
|
|
5647
|
+
);
|
|
5648
|
+
}
|
|
5649
|
+
|
|
5650
|
+
#[tokio::test]
|
|
5651
|
+
async fn the_first_admin_of_a_site_must_be_open_ended() {
|
|
5652
|
+
let (client, domain_ir) = scratch_members_db("hecks_host_web_test_open_first").await;
|
|
5653
|
+
// Nobody holds an admin role yet.
|
|
5654
|
+
client
|
|
5655
|
+
.lock()
|
|
5656
|
+
.await
|
|
5657
|
+
.execute("UPDATE acme_member_head_snapshot_1 SET state = state - 'role' WHERE id = 'zed@example.com'", &[])
|
|
5658
|
+
.await
|
|
5659
|
+
.unwrap();
|
|
5660
|
+
let outcome = |days: Option<&'static str>, who: &'static str| {
|
|
5661
|
+
let (client, domain_ir) = (&client, &domain_ir);
|
|
5662
|
+
async move {
|
|
5663
|
+
auth::grant_access_until(
|
|
5664
|
+
client,
|
|
5665
|
+
Path::new("unused"),
|
|
5666
|
+
&members_config(),
|
|
5667
|
+
domain_ir,
|
|
5668
|
+
who,
|
|
5669
|
+
"Admin",
|
|
5670
|
+
days,
|
|
5671
|
+
)
|
|
5672
|
+
.await
|
|
5673
|
+
.unwrap()
|
|
5674
|
+
}
|
|
5675
|
+
};
|
|
5676
|
+
assert_eq!(
|
|
5677
|
+
outcome(Some(FUTURE), "zed@example.com").await,
|
|
5678
|
+
auth::GrantOutcome::LastOpenAdmin
|
|
5679
|
+
);
|
|
5680
|
+
assert_eq!(
|
|
5681
|
+
person(&client, &domain_ir, "zed@example.com").await["role"],
|
|
5682
|
+
Value::Null
|
|
5683
|
+
);
|
|
5684
|
+
|
|
5685
|
+
// Open-ended first admin is allowed, and a windowed second one after it.
|
|
5686
|
+
assert_eq!(
|
|
5687
|
+
outcome(None, "zed@example.com").await,
|
|
5688
|
+
auth::GrantOutcome::Done
|
|
5689
|
+
);
|
|
5690
|
+
let r = form_status(
|
|
5691
|
+
&client,
|
|
5692
|
+
&domain_ir,
|
|
5693
|
+
"zed@example.com",
|
|
5694
|
+
"amy%40example.com",
|
|
5695
|
+
"Admin",
|
|
5696
|
+
"5",
|
|
5697
|
+
)
|
|
5698
|
+
.await;
|
|
5699
|
+
assert_eq!(r["statusCode"], 302, "{r:?}");
|
|
5700
|
+
}
|
|
5701
|
+
|
|
5702
|
+
#[tokio::test]
|
|
5703
|
+
async fn a_json_grant_to_a_new_person_never_trips_the_open_ended_guard() {
|
|
5704
|
+
let secret = "s3cret";
|
|
5705
|
+
let (client, domain_ir) = scratch_members_db("hecks_host_web_test_open_json").await;
|
|
5706
|
+
let body = json!({"email": "new@example.com", "name": "New", "role": "Admin", "expires_in_days": 5}).to_string();
|
|
5707
|
+
let r = add_member_route(
|
|
5708
|
+
&domain_ir,
|
|
5709
|
+
&body,
|
|
5710
|
+
&session_cookies(secret, "zed@example.com"),
|
|
5711
|
+
secret,
|
|
5712
|
+
&client,
|
|
5713
|
+
Path::new("unused"),
|
|
5714
|
+
&members_config(),
|
|
5715
|
+
)
|
|
5716
|
+
.await;
|
|
5717
|
+
assert_eq!(r["statusCode"], 201, "{r:?}");
|
|
5718
|
+
// The lone open-ended admin is still zed; windowing him is refused.
|
|
5719
|
+
let outcome = auth::grant_access_until(
|
|
5720
|
+
&client,
|
|
5721
|
+
Path::new("unused"),
|
|
5722
|
+
&members_config(),
|
|
5723
|
+
&domain_ir,
|
|
5724
|
+
"zed@example.com",
|
|
5725
|
+
"Admin",
|
|
5726
|
+
Some(FUTURE),
|
|
5727
|
+
)
|
|
5728
|
+
.await
|
|
5729
|
+
.unwrap();
|
|
5730
|
+
assert_eq!(outcome, auth::GrantOutcome::LastOpenAdmin);
|
|
5731
|
+
let outcome = auth::grant_access_until(
|
|
5732
|
+
&client,
|
|
5733
|
+
Path::new("unused"),
|
|
5734
|
+
&members_config(),
|
|
5735
|
+
&domain_ir,
|
|
5736
|
+
"nobody@example.com",
|
|
5737
|
+
"Admin",
|
|
5738
|
+
None,
|
|
5739
|
+
)
|
|
5740
|
+
.await
|
|
5741
|
+
.unwrap();
|
|
5742
|
+
assert_eq!(outcome, auth::GrantOutcome::UnknownPerson);
|
|
5743
|
+
}
|
|
5744
|
+
|
|
5745
|
+
#[tokio::test]
|
|
5746
|
+
async fn a_person_whose_window_has_ended_gets_no_access_anywhere_but_the_row_keeps_its_role() {
|
|
5747
|
+
let secret = "s3cret";
|
|
5748
|
+
let (client, domain_ir) = scratch_two_admins("hecks_host_web_test_expiry_access").await;
|
|
5749
|
+
client
|
|
5750
|
+
.lock()
|
|
5751
|
+
.await
|
|
5752
|
+
.execute(
|
|
5753
|
+
"UPDATE acme_member_head_snapshot_1 SET state = state || '{\"identity_id\": {\"value\": \"id-amy\"}}'::jsonb WHERE id = 'amy@example.com'",
|
|
5754
|
+
&[],
|
|
5755
|
+
)
|
|
5756
|
+
.await
|
|
5757
|
+
.unwrap();
|
|
5758
|
+
set_window(&client, "amy@example.com", json!(PAST)).await;
|
|
5759
|
+
let amy_cookies = session_cookies(secret, "amy@example.com");
|
|
5760
|
+
|
|
5761
|
+
assert!(!auth::has_access(&client, &domain_ir, "amy@example.com")
|
|
5762
|
+
.await
|
|
5763
|
+
.unwrap());
|
|
5764
|
+
assert_eq!(
|
|
5765
|
+
auth::active_role(&client, &domain_ir, "amy@example.com")
|
|
5766
|
+
.await
|
|
5767
|
+
.unwrap(),
|
|
5768
|
+
None
|
|
5769
|
+
);
|
|
5770
|
+
assert!(
|
|
5771
|
+
!auth::caller_is_admin(&client, &domain_ir, "amy@example.com")
|
|
5772
|
+
.await
|
|
5773
|
+
.unwrap()
|
|
5774
|
+
);
|
|
5775
|
+
// The CMS-facing session check and the SSO token route both refuse her.
|
|
5776
|
+
assert_eq!(
|
|
5777
|
+
accounts_me_route(&domain_ir, &amy_cookies, secret, &client).await["statusCode"],
|
|
5778
|
+
401
|
|
5779
|
+
);
|
|
5780
|
+
assert_eq!(
|
|
5781
|
+
accounts_sso_token_route(&domain_ir, &amy_cookies, secret, &client).await["statusCode"],
|
|
5782
|
+
401
|
|
5783
|
+
);
|
|
5784
|
+
// A new Google sign-in through her linked identity is refused too.
|
|
5785
|
+
assert!(
|
|
5786
|
+
auth::session_for_member_by_identity(&client, &domain_ir, "id-amy")
|
|
5787
|
+
.await
|
|
5788
|
+
.unwrap()
|
|
5789
|
+
.is_none()
|
|
5790
|
+
);
|
|
5791
|
+
let amy = person(&client, &domain_ir, "amy@example.com").await;
|
|
5792
|
+
assert_eq!(
|
|
5793
|
+
(
|
|
5794
|
+
amy["role"].as_str(),
|
|
5795
|
+
amy["granted"].clone(),
|
|
5796
|
+
amy["disabled"].clone(),
|
|
5797
|
+
amy["expired"].clone(),
|
|
5798
|
+
amy["access_expires_at"].as_str()
|
|
5799
|
+
),
|
|
5800
|
+
(
|
|
5801
|
+
Some("Admin"),
|
|
5802
|
+
json!(false),
|
|
5803
|
+
json!(false),
|
|
5804
|
+
json!(true),
|
|
5805
|
+
Some(PAST)
|
|
5806
|
+
)
|
|
5807
|
+
);
|
|
5808
|
+
// Her window has not touched zed's access.
|
|
5809
|
+
assert!(auth::has_access(&client, &domain_ir, "zed@example.com")
|
|
5810
|
+
.await
|
|
5811
|
+
.unwrap());
|
|
5812
|
+
}
|
|
5813
|
+
|
|
5814
|
+
#[tokio::test]
|
|
5815
|
+
async fn a_window_still_open_changes_nothing_until_it_ends() {
|
|
5816
|
+
let secret = "s3cret";
|
|
5817
|
+
let (client, domain_ir) = scratch_two_admins("hecks_host_web_test_expiry_open").await;
|
|
5818
|
+
set_window(&client, "amy@example.com", json!(FUTURE)).await;
|
|
5819
|
+
|
|
5820
|
+
assert!(auth::has_access(&client, &domain_ir, "amy@example.com")
|
|
5821
|
+
.await
|
|
5822
|
+
.unwrap());
|
|
5823
|
+
assert!(
|
|
5824
|
+
auth::caller_is_admin(&client, &domain_ir, "amy@example.com")
|
|
5825
|
+
.await
|
|
5826
|
+
.unwrap()
|
|
5827
|
+
);
|
|
5828
|
+
assert_eq!(
|
|
5829
|
+
accounts_me_route(
|
|
5830
|
+
&domain_ir,
|
|
5831
|
+
&session_cookies(secret, "amy@example.com"),
|
|
5832
|
+
secret,
|
|
5833
|
+
&client
|
|
5834
|
+
)
|
|
5835
|
+
.await["statusCode"],
|
|
5836
|
+
200
|
|
5837
|
+
);
|
|
5838
|
+
let amy = person(&client, &domain_ir, "amy@example.com").await;
|
|
5839
|
+
assert_eq!(
|
|
5840
|
+
(
|
|
5841
|
+
amy["granted"].clone(),
|
|
5842
|
+
amy["expired"].clone(),
|
|
5843
|
+
amy["access_expires_at"].as_str()
|
|
5844
|
+
),
|
|
5845
|
+
(json!(true), json!(false), Some(FUTURE))
|
|
5846
|
+
);
|
|
5847
|
+
}
|
|
5848
|
+
|
|
5849
|
+
#[tokio::test]
|
|
5850
|
+
async fn a_malformed_window_counts_as_ended_and_fails_closed() {
|
|
5851
|
+
let (client, domain_ir) = scratch_two_admins("hecks_host_web_test_expiry_malformed").await;
|
|
5852
|
+
for bad in [
|
|
5853
|
+
json!("tomorrow"),
|
|
5854
|
+
json!(""),
|
|
5855
|
+
json!(12345),
|
|
5856
|
+
json!(true),
|
|
5857
|
+
json!("2099-13-40T00:00:00Z"),
|
|
5858
|
+
json!("2099-01-01T00:00:00+02:00"),
|
|
5859
|
+
json!({"value": "not a time"}),
|
|
5860
|
+
] {
|
|
5861
|
+
set_window(&client, "amy@example.com", bad.clone()).await;
|
|
5862
|
+
assert!(
|
|
5863
|
+
!auth::has_access(&client, &domain_ir, "amy@example.com")
|
|
5864
|
+
.await
|
|
5865
|
+
.unwrap(),
|
|
5866
|
+
"{bad}"
|
|
5867
|
+
);
|
|
5868
|
+
assert_eq!(
|
|
5869
|
+
person(&client, &domain_ir, "amy@example.com").await["expired"],
|
|
5870
|
+
true,
|
|
5871
|
+
"{bad}"
|
|
5872
|
+
);
|
|
5873
|
+
}
|
|
5874
|
+
// A wrapped value reads like a bare one; null is no window at all.
|
|
5875
|
+
set_window(&client, "amy@example.com", json!({"value": FUTURE})).await;
|
|
5876
|
+
assert!(auth::has_access(&client, &domain_ir, "amy@example.com")
|
|
5877
|
+
.await
|
|
5878
|
+
.unwrap());
|
|
5879
|
+
set_window(&client, "amy@example.com", Value::Null).await;
|
|
5880
|
+
assert!(auth::has_access(&client, &domain_ir, "amy@example.com")
|
|
5881
|
+
.await
|
|
5882
|
+
.unwrap());
|
|
5883
|
+
}
|
|
5884
|
+
|
|
5885
|
+
#[tokio::test]
|
|
5886
|
+
async fn an_expired_admin_neither_passes_the_admin_gate_nor_counts_as_the_last_admin() {
|
|
5887
|
+
let (client, domain_ir) = scratch_two_admins("hecks_host_web_test_expiry_admins").await;
|
|
5888
|
+
set_window(&client, "amy@example.com", json!(PAST)).await;
|
|
5889
|
+
|
|
5890
|
+
// The expired admin cannot act as one.
|
|
5891
|
+
let response = switch_access(
|
|
5892
|
+
&client,
|
|
5893
|
+
&domain_ir,
|
|
5894
|
+
"amy@example.com",
|
|
5895
|
+
"zed@example.com",
|
|
5896
|
+
true,
|
|
5897
|
+
)
|
|
5898
|
+
.await;
|
|
5899
|
+
assert_eq!(response["statusCode"], 403, "{response:?}");
|
|
5900
|
+
let response = set_role(
|
|
5901
|
+
&client,
|
|
5902
|
+
&domain_ir,
|
|
5903
|
+
"amy@example.com",
|
|
5904
|
+
"zed@example.com",
|
|
5905
|
+
"Member",
|
|
5906
|
+
)
|
|
5907
|
+
.await;
|
|
5908
|
+
assert_eq!(response["statusCode"], 403, "{response:?}");
|
|
5909
|
+
|
|
5910
|
+
// Zed is the only admin that counts, so the last-admin guard protects him
|
|
5911
|
+
// even though two rows hold the Admin role.
|
|
5912
|
+
let response = set_role(
|
|
5913
|
+
&client,
|
|
5914
|
+
&domain_ir,
|
|
5915
|
+
"zed@example.com",
|
|
5916
|
+
"zed@example.com",
|
|
5917
|
+
"Member",
|
|
5918
|
+
)
|
|
5919
|
+
.await;
|
|
5920
|
+
assert_eq!(response["statusCode"], 409, "{response:?}");
|
|
5921
|
+
assert_eq!(
|
|
5922
|
+
auth::set_person_disabled(
|
|
5923
|
+
&client,
|
|
5924
|
+
&members_config(),
|
|
5925
|
+
&domain_ir,
|
|
5926
|
+
"zed@example.com",
|
|
5927
|
+
"zed@example.com",
|
|
5928
|
+
true
|
|
5929
|
+
)
|
|
5930
|
+
.await
|
|
5931
|
+
.unwrap(),
|
|
5932
|
+
auth::DisableOutcome::SelfDisable
|
|
5933
|
+
);
|
|
5934
|
+
|
|
5935
|
+
// Removing the expired admin is not refused: she was never one of the active admins.
|
|
5936
|
+
let response = switch_access(
|
|
5937
|
+
&client,
|
|
5938
|
+
&domain_ir,
|
|
5939
|
+
"zed@example.com",
|
|
5940
|
+
"amy@example.com",
|
|
5941
|
+
true,
|
|
5942
|
+
)
|
|
5943
|
+
.await;
|
|
5944
|
+
assert_eq!(response["statusCode"], 200, "{response:?}");
|
|
5945
|
+
assert_eq!(
|
|
5946
|
+
person(&client, &domain_ir, "amy@example.com").await["disabled"],
|
|
5947
|
+
true
|
|
5948
|
+
);
|
|
5949
|
+
}
|
|
5950
|
+
|
|
5951
|
+
#[tokio::test]
|
|
5952
|
+
async fn a_second_admin_whose_window_ended_does_not_unlock_removing_the_last_active_one() {
|
|
5953
|
+
let (client, domain_ir) = scratch_two_admins("hecks_host_web_test_expiry_last").await;
|
|
5954
|
+
// amy would be the second admin, but she has run out: demoting zed through any
|
|
5955
|
+
// other admin is impossible and zed alone is still protected from demotion.
|
|
5956
|
+
set_window(&client, "amy@example.com", json!(PAST)).await;
|
|
5957
|
+
assert_eq!(
|
|
5958
|
+
auth::set_person_role(
|
|
5959
|
+
&client,
|
|
5960
|
+
&members_config(),
|
|
5961
|
+
&domain_ir,
|
|
5962
|
+
"zed@example.com",
|
|
5963
|
+
"zed@example.com",
|
|
5964
|
+
"Member"
|
|
5965
|
+
)
|
|
5966
|
+
.await
|
|
5967
|
+
.unwrap(),
|
|
5968
|
+
auth::RoleOutcome::LastAdmin
|
|
5969
|
+
);
|
|
5970
|
+
}
|
|
5971
|
+
|
|
5972
|
+
#[tokio::test]
|
|
5973
|
+
async fn granting_with_a_window_sets_it_and_granting_again_renews_or_clears_it() {
|
|
5974
|
+
let secret = "s3cret";
|
|
5975
|
+
let (client, domain_ir) = scratch_members_db("hecks_host_web_test_expiry_grant").await;
|
|
5976
|
+
let admin = session_cookies(secret, "zed@example.com");
|
|
5977
|
+
let now = || {
|
|
5978
|
+
std::time::SystemTime::now()
|
|
5979
|
+
.duration_since(std::time::UNIX_EPOCH)
|
|
5980
|
+
.unwrap()
|
|
5981
|
+
.as_secs() as i64
|
|
5982
|
+
};
|
|
5983
|
+
let ends = |person: &Value| {
|
|
5984
|
+
crate::clock::parse_iso_utc(person["access_expires_at"].as_str().unwrap()).unwrap()
|
|
5985
|
+
};
|
|
5986
|
+
|
|
5987
|
+
let first = members_page(
|
|
5988
|
+
"POST",
|
|
5989
|
+
&form_grant("amy%40example.com", "Member", "7"),
|
|
5990
|
+
&admin,
|
|
5991
|
+
secret,
|
|
5992
|
+
&client,
|
|
5993
|
+
&domain_ir,
|
|
5994
|
+
)
|
|
5995
|
+
.await;
|
|
5996
|
+
assert_eq!(first["statusCode"], 302, "{first:?}");
|
|
5997
|
+
let amy = person(&client, &domain_ir, "amy@example.com").await;
|
|
5998
|
+
assert_eq!(amy["granted"], true);
|
|
5999
|
+
assert!(
|
|
6000
|
+
(ends(&amy) - (now() + 7 * 86_400)).abs() < 120,
|
|
6001
|
+
"a seven day window: {amy}"
|
|
6002
|
+
);
|
|
6003
|
+
|
|
6004
|
+
// Granting again extends the window from now.
|
|
6005
|
+
let renewed = members_page(
|
|
6006
|
+
"POST",
|
|
6007
|
+
&form_grant("amy%40example.com", "Member", "30"),
|
|
6008
|
+
&admin,
|
|
6009
|
+
secret,
|
|
6010
|
+
&client,
|
|
6011
|
+
&domain_ir,
|
|
6012
|
+
)
|
|
6013
|
+
.await;
|
|
6014
|
+
assert_eq!(renewed["statusCode"], 302, "{renewed:?}");
|
|
6015
|
+
let amy = person(&client, &domain_ir, "amy@example.com").await;
|
|
6016
|
+
assert!((ends(&amy) - (now() + 30 * 86_400)).abs() < 120, "{amy}");
|
|
6017
|
+
|
|
6018
|
+
// Renewal revives a lapsed row: the role comes back with the new window.
|
|
6019
|
+
set_window(&client, "amy@example.com", json!(PAST)).await;
|
|
6020
|
+
assert!(!auth::has_access(&client, &domain_ir, "amy@example.com")
|
|
6021
|
+
.await
|
|
6022
|
+
.unwrap());
|
|
6023
|
+
let revived = members_page(
|
|
6024
|
+
"POST",
|
|
6025
|
+
&form_grant("amy%40example.com", "Member", "1"),
|
|
6026
|
+
&admin,
|
|
6027
|
+
secret,
|
|
6028
|
+
&client,
|
|
6029
|
+
&domain_ir,
|
|
6030
|
+
)
|
|
6031
|
+
.await;
|
|
6032
|
+
assert_eq!(revived["statusCode"], 302, "{revived:?}");
|
|
6033
|
+
assert!(auth::has_access(&client, &domain_ir, "amy@example.com")
|
|
6034
|
+
.await
|
|
6035
|
+
.unwrap());
|
|
6036
|
+
|
|
6037
|
+
// A grant with an empty field makes the access open-ended again.
|
|
6038
|
+
let open = members_page(
|
|
6039
|
+
"POST",
|
|
6040
|
+
&form_grant("amy%40example.com", "Member", ""),
|
|
6041
|
+
&admin,
|
|
6042
|
+
secret,
|
|
6043
|
+
&client,
|
|
6044
|
+
&domain_ir,
|
|
6045
|
+
)
|
|
6046
|
+
.await;
|
|
6047
|
+
assert_eq!(open["statusCode"], 302, "{open:?}");
|
|
6048
|
+
let amy = person(&client, &domain_ir, "amy@example.com").await;
|
|
6049
|
+
assert_eq!(
|
|
6050
|
+
(amy["access_expires_at"].clone(), amy["expired"].clone()),
|
|
6051
|
+
(Value::Null, json!(false))
|
|
6052
|
+
);
|
|
6053
|
+
}
|
|
6054
|
+
|
|
6055
|
+
#[tokio::test]
|
|
6056
|
+
async fn a_grant_with_an_unusable_expires_in_days_is_refused_and_writes_nothing() {
|
|
6057
|
+
let secret = "s3cret";
|
|
6058
|
+
let (client, domain_ir) = scratch_members_db("hecks_host_web_test_expiry_refuse").await;
|
|
6059
|
+
let admin = session_cookies(secret, "zed@example.com");
|
|
6060
|
+
let before = journal_rows(&client, "amy@example.com").await;
|
|
6061
|
+
|
|
6062
|
+
for bad in ["0", "-3", "366", "1.5", "soon", "99999999999999999999"] {
|
|
6063
|
+
let form = members_page(
|
|
6064
|
+
"POST",
|
|
6065
|
+
&form_grant("amy%40example.com", "Member", bad),
|
|
6066
|
+
&admin,
|
|
6067
|
+
secret,
|
|
6068
|
+
&client,
|
|
6069
|
+
&domain_ir,
|
|
6070
|
+
)
|
|
6071
|
+
.await;
|
|
6072
|
+
assert_eq!(form["statusCode"], 400, "form {bad}: {form:?}");
|
|
6073
|
+
}
|
|
6074
|
+
for bad in [
|
|
6075
|
+
json!(0),
|
|
6076
|
+
json!(366),
|
|
6077
|
+
json!(1.5),
|
|
6078
|
+
json!("soon"),
|
|
6079
|
+
json!([7]),
|
|
6080
|
+
json!(-1),
|
|
6081
|
+
] {
|
|
6082
|
+
let body = json!({"email": "new@example.com", "name": "New", "expires_in_days": bad})
|
|
6083
|
+
.to_string();
|
|
6084
|
+
let response = add_member_route(
|
|
6085
|
+
&domain_ir,
|
|
6086
|
+
&body,
|
|
6087
|
+
&admin,
|
|
6088
|
+
secret,
|
|
6089
|
+
&client,
|
|
6090
|
+
Path::new("unused"),
|
|
6091
|
+
&members_config(),
|
|
6092
|
+
)
|
|
6093
|
+
.await;
|
|
6094
|
+
assert_eq!(response["statusCode"], 400, "json {bad}: {response:?}");
|
|
6095
|
+
}
|
|
6096
|
+
assert_eq!(journal_rows(&client, "amy@example.com").await, before);
|
|
6097
|
+
assert_eq!(
|
|
6098
|
+
auth::all_people(&client, &domain_ir).await.unwrap().len(),
|
|
6099
|
+
2,
|
|
6100
|
+
"nobody was admitted by a refused grant"
|
|
6101
|
+
);
|
|
6102
|
+
for edge in ["1", "365"] {
|
|
6103
|
+
let form = members_page(
|
|
6104
|
+
"POST",
|
|
6105
|
+
&form_grant("amy%40example.com", "Member", edge),
|
|
6106
|
+
&admin,
|
|
6107
|
+
secret,
|
|
6108
|
+
&client,
|
|
6109
|
+
&domain_ir,
|
|
6110
|
+
)
|
|
6111
|
+
.await;
|
|
6112
|
+
assert_eq!(form["statusCode"], 302, "{edge}: {form:?}");
|
|
6113
|
+
}
|
|
6114
|
+
}
|
|
6115
|
+
|
|
6116
|
+
#[tokio::test]
|
|
6117
|
+
async fn the_json_admit_and_grant_route_takes_expires_in_days_as_a_number_or_a_string() {
|
|
6118
|
+
let secret = "s3cret";
|
|
6119
|
+
let (client, domain_ir) = scratch_members_db("hecks_host_web_test_expiry_json").await;
|
|
6120
|
+
let admin = session_cookies(secret, "zed@example.com");
|
|
6121
|
+
for (email, days) in [("a@example.com", json!(14)), ("b@example.com", json!("14"))] {
|
|
6122
|
+
let body =
|
|
6123
|
+
json!({"email": email, "name": "N", "role": "Member", "expires_in_days": days})
|
|
6124
|
+
.to_string();
|
|
6125
|
+
let response = add_member_route(
|
|
6126
|
+
&domain_ir,
|
|
6127
|
+
&body,
|
|
6128
|
+
&admin,
|
|
6129
|
+
secret,
|
|
6130
|
+
&client,
|
|
6131
|
+
Path::new("unused"),
|
|
6132
|
+
&members_config(),
|
|
6133
|
+
)
|
|
6134
|
+
.await;
|
|
6135
|
+
assert_eq!(response["statusCode"], 201, "{response:?}");
|
|
6136
|
+
let created: Value = serde_json::from_str(response["body"].as_str().unwrap()).unwrap();
|
|
6137
|
+
assert_eq!(created["expired"], false);
|
|
6138
|
+
let listed = person(&client, &domain_ir, email).await;
|
|
6139
|
+
assert_eq!(listed["access_expires_at"], created["access_expires_at"]);
|
|
6140
|
+
assert!(listed["access_expires_at"].as_str().unwrap().ends_with('Z'));
|
|
6141
|
+
assert_eq!(listed["granted"], true);
|
|
6142
|
+
}
|
|
6143
|
+
}
|
|
6144
|
+
|
|
6145
|
+
#[tokio::test]
|
|
6146
|
+
async fn the_members_page_shows_each_window_and_offers_the_days_field() {
|
|
6147
|
+
let (client, domain_ir) = scratch_two_admins("hecks_host_web_test_expiry_page").await;
|
|
6148
|
+
set_window(&client, "amy@example.com", json!(PAST)).await;
|
|
6149
|
+
set_window(&client, "zed@example.com", json!(FUTURE)).await;
|
|
6150
|
+
let page = admin_members_page(&client, &domain_ir).await;
|
|
6151
|
+
assert!(page.contains(&format!("access ended {PAST}")), "{page}");
|
|
6152
|
+
assert!(page.contains(&format!("linked, until {FUTURE}")), "{page}");
|
|
6153
|
+
assert!(page.contains(r#"name="expires_in_days""#));
|
|
6154
|
+
}
|
|
6155
|
+
|
|
6156
|
+
#[tokio::test]
|
|
6157
|
+
async fn rows_without_a_window_behave_exactly_as_before() {
|
|
6158
|
+
let secret = "s3cret";
|
|
6159
|
+
let (client, domain_ir) = scratch_two_admins("hecks_host_web_test_expiry_none").await;
|
|
6160
|
+
assert!(auth::has_access(&client, &domain_ir, "amy@example.com")
|
|
6161
|
+
.await
|
|
6162
|
+
.unwrap());
|
|
6163
|
+
assert_eq!(
|
|
6164
|
+
auth::active_role(&client, &domain_ir, "amy@example.com")
|
|
6165
|
+
.await
|
|
6166
|
+
.unwrap()
|
|
6167
|
+
.as_deref(),
|
|
6168
|
+
Some("Admin")
|
|
6169
|
+
);
|
|
6170
|
+
assert_eq!(
|
|
6171
|
+
accounts_me_route(
|
|
6172
|
+
&domain_ir,
|
|
6173
|
+
&session_cookies(secret, "zed@example.com"),
|
|
6174
|
+
secret,
|
|
6175
|
+
&client
|
|
6176
|
+
)
|
|
6177
|
+
.await["statusCode"],
|
|
6178
|
+
200
|
|
6179
|
+
);
|
|
6180
|
+
// The disabled flag still works on its own, and a plain grant adds no field to the row.
|
|
6181
|
+
assert_eq!(
|
|
6182
|
+
switch_access(
|
|
6183
|
+
&client,
|
|
6184
|
+
&domain_ir,
|
|
6185
|
+
"zed@example.com",
|
|
6186
|
+
"amy@example.com",
|
|
6187
|
+
true
|
|
6188
|
+
)
|
|
6189
|
+
.await["statusCode"],
|
|
6190
|
+
200
|
|
6191
|
+
);
|
|
6192
|
+
assert!(!auth::has_access(&client, &domain_ir, "amy@example.com")
|
|
6193
|
+
.await
|
|
6194
|
+
.unwrap());
|
|
6195
|
+
let guard = client.lock().await;
|
|
6196
|
+
let with_field: i64 = guard
|
|
6197
|
+
.query_one(
|
|
6198
|
+
"SELECT count(*) FROM acme_member_head_snapshot_1 WHERE state ? 'access_expires_at'",
|
|
6199
|
+
&[],
|
|
6200
|
+
)
|
|
6201
|
+
.await
|
|
6202
|
+
.unwrap()
|
|
6203
|
+
.get(0);
|
|
6204
|
+
assert_eq!(with_field, 0);
|
|
6205
|
+
}
|
|
6206
|
+
|
|
4877
6207
|
#[test]
|
|
4878
6208
|
fn sorted_by_name_orders_case_insensitively_and_puts_a_missing_name_first() {
|
|
4879
6209
|
let people = vec![
|