hecks 3.10.0 → 3.12.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 +29 -0
- data/examples/pizzas/bluebook/pizzas.hecksagon +7 -0
- data/exe/hecks +4 -4
- data/lib/hecks/adapters/driven/d1.rb +2 -10
- data/lib/hecks/adapters/driven/heki.adapter +1 -1
- data/lib/hecks/adapters/driven/heki.rb +2 -12
- data/lib/hecks/adapters/driven/lambda.adapter +1 -1
- data/lib/hecks/adapters/driven/lambda.rb +3 -18
- data/lib/hecks/adapters/driven/postgres.rb +4 -11
- data/lib/hecks/adapters/driven/sqlite.adapter +1 -1
- data/lib/hecks/adapters/driven/sqlite.rb +2 -10
- data/lib/hecks/adapters/driving/admission.rb +46 -0
- data/lib/hecks/adapters/driving/cli/nesting.rb +36 -0
- data/lib/hecks/adapters/driving/cli/shorthand.rb +88 -0
- data/lib/hecks/adapters/driving/cli.rb +127 -0
- data/lib/hecks/adapters/driving/cli_runner/answers.rb +160 -0
- data/lib/hecks/adapters/driving/cli_runner/resolution.rb +158 -0
- data/lib/hecks/adapters/driving/cli_runner/settling.rb +120 -0
- data/lib/hecks/adapters/driving/cli_runner/streaming.rb +93 -0
- data/lib/hecks/adapters/driving/cli_runner/suggestions.rb +47 -0
- data/lib/hecks/adapters/driving/cli_runner.rb +70 -0
- data/lib/hecks/adapters/driving/command_request.rb +121 -0
- data/lib/hecks/adapters/driving/handle.rb +214 -0
- data/lib/hecks/adapters/driving/json.rb +136 -0
- data/lib/hecks/adapters/driving/launcher_options.rb +267 -0
- data/lib/hecks/adapters/driving/mcp/properties.rb +49 -0
- data/lib/hecks/adapters/driving/mcp/tool_calls.rb +116 -0
- data/lib/hecks/adapters/driving/mcp/tool_definitions/auditing.rb +53 -0
- data/lib/hecks/adapters/driving/mcp/tool_definitions/calling.rb +71 -0
- data/lib/hecks/adapters/driving/mcp/tool_definitions/reading.rb +99 -0
- data/lib/hecks/adapters/driving/mcp/tool_definitions.rb +18 -0
- data/lib/hecks/adapters/driving/mcp.rb +159 -0
- data/lib/hecks/adapters/driving/mcp_guide_cache.rb +78 -0
- data/lib/hecks/adapters/driving/mcp_scope/argument_policy.rb +97 -0
- data/lib/hecks/adapters/driving/mcp_scope/env_problems.rb +69 -0
- data/lib/hecks/adapters/driving/mcp_scope/presenter.rb +83 -0
- data/lib/hecks/adapters/driving/mcp_scope.rb +213 -0
- data/lib/hecks/adapters/driving/newcomer.rb +50 -0
- data/lib/hecks/adapters/driving/ruby/aggregate_module.rb +171 -0
- data/lib/hecks/adapters/driving/ruby/chapter.rb +91 -0
- data/lib/hecks/adapters/driving/ruby.rb +83 -0
- data/lib/hecks/adapters/driving/usage_cache.rb +144 -0
- data/lib/hecks/{doors.rb → adapters/driving.rb} +10 -6
- data/lib/hecks/behaviors/expectations/boot.rb +1 -1
- data/lib/hecks/bench/ruby_runner.rb +1 -1
- data/lib/hecks/bluebook/ask_resolution.rb +130 -0
- data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +8 -2
- data/lib/hecks/bluebook/assembly/contracts/read_models.rb +1 -1
- data/lib/hecks/bluebook/assembly/contracts/rules.rb +4 -2
- data/lib/hecks/bluebook/assembly/contracts/structure.rb +3 -1
- data/lib/hecks/bluebook/assembly/marks.rb +4 -15
- data/lib/hecks/bluebook/assembly/scalars.rb +39 -0
- data/lib/hecks/bluebook/attribute.rb +1 -1
- data/lib/hecks/bluebook/behaviour/command.rb +13 -8
- data/lib/hecks/bluebook/behaviour/policy.rb +30 -0
- data/lib/hecks/bluebook/capabilities/attribute_values.rb +72 -0
- data/lib/hecks/bluebook/capabilities.rb +91 -9
- data/lib/hecks/bluebook/domain_port.rb +16 -4
- data/lib/hecks/bluebook/dsl/adapter_builder.rb +18 -6
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +13 -0
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation/computed_sources.rb +98 -0
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation/fan_out_rows.rb +42 -0
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation/projected_fields.rb +14 -2
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation/provisions.rb +69 -3
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation/with_projections.rb +17 -10
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +5 -0
- data/lib/hecks/bluebook/dsl/bootstrap_table.rb +2 -0
- data/lib/hecks/bluebook/dsl/command_builder/mutations.rb +8 -1
- data/lib/hecks/bluebook/dsl/command_builder/operands.rb +121 -0
- data/lib/hecks/bluebook/dsl/command_builder.rb +2 -0
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +10 -1
- data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +45 -2
- data/lib/hecks/bluebook/dsl/policy_builder.rb +47 -10
- data/lib/hecks/bluebook/dsl/rule_reference.rb +7 -4
- data/lib/hecks/bluebook/dsl/world_builder.rb +1 -1
- data/lib/hecks/bluebook/expression/ast_json/emitters.rb +6 -0
- data/lib/hecks/bluebook/expression/ast_json.rb +9 -6
- data/lib/hecks/bluebook/expression/ast_reader.rb +14 -9
- data/lib/hecks/bluebook/expression/projection.json +36 -0
- data/lib/hecks/bluebook/expression/resolver/operations.rb +31 -0
- data/lib/hecks/bluebook/expression/resolver/parsing.rb +38 -5
- data/lib/hecks/bluebook/expression/resolver/scanning.rb +22 -5
- data/lib/hecks/bluebook/expression/resolver/values.rb +10 -0
- data/lib/hecks/bluebook/expression/resolver.rb +20 -1
- data/lib/hecks/bluebook/hecksagon.rb +23 -3
- data/lib/hecks/bluebook/lifecycle.rb +22 -3
- data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +13 -1
- data/lib/hecks/bluebook/meta_validator/modes.rb +4 -1
- data/lib/hecks/bluebook/meta_validator/readings.rb +24 -1
- data/lib/hecks/bluebook/meta_validator/reconstruction/heads.rb +2 -1
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +6 -3
- data/lib/hecks/bluebook/meta_validator/shapes.rb +5 -0
- data/lib/hecks/bluebook/meta_validator/verdict_cache.rb +1 -1
- data/lib/hecks/bluebook/meta_validator.rb +12 -12
- data/lib/hecks/bluebook/model_check/ask_checks.rb +71 -0
- data/lib/hecks/bluebook/model_check/policy_checks.rb +2 -1
- data/lib/hecks/bluebook/model_check/reference_checks.rb +70 -0
- data/lib/hecks/bluebook/model_check.rb +8 -2
- data/lib/hecks/bluebook/policy.rb +4 -2
- data/lib/hecks/bluebook/smoke_test.rb +1 -1
- data/lib/hecks/bluebook/synthesizer.rb +2 -0
- data/lib/hecks/bluebook.rb +2 -0
- data/lib/hecks/cli/command_table.rb +1 -1
- data/lib/hecks/cli/console.rb +2 -1
- data/lib/hecks/cli/document.rb +1 -1
- data/lib/hecks/cli/launcher_words.rb +4 -4
- data/lib/hecks/cli/mcp.rb +4 -4
- data/lib/hecks/cli/project_cli/launcher_source.rb +2 -2
- data/lib/hecks/cli/project_cli/templates.rb +5 -5
- data/lib/hecks/cli/project_cli.rb +9 -6
- data/lib/hecks/cli/run.rb +2 -2
- data/lib/hecks/cli/smoke_http/checks.rb +4 -1
- data/lib/hecks/cli/smoke_http/transport.rb +3 -3
- data/lib/hecks/cli/smoke_http.rb +3 -0
- data/lib/hecks/codemod.rb +5 -2
- data/lib/hecks/corpus/routes.rb +7 -0
- data/lib/hecks/corpus/rust.rb +3 -2
- data/lib/hecks/deploy/bluebook/deploy.bluebook +34 -4
- data/lib/hecks/doc/reference/rendering.rb +6 -3
- data/lib/hecks/forms/app/commands.rb +8 -7
- data/lib/hecks/forms/app/queries.rb +2 -1
- data/lib/hecks/forms/app.rb +1 -1
- data/lib/hecks/forms/record_renderer.rb +1 -1
- data/lib/hecks/framework/bluebook/privacy.bluebook +11 -0
- data/lib/hecks/fuzzing/concurrent_dispatch.rb +4 -1
- data/lib/hecks/fuzzing/failure_triage.rb +2 -1
- data/lib/hecks/fuzzing/mutation/run.rb +11 -4
- data/lib/hecks/fuzzing/properties/catalog.rb +6 -4
- data/lib/hecks/fuzzing/properties/guaranteed_by_construction.rb +18 -11
- data/lib/hecks/fuzzing/properties/guards.rb +1 -1
- data/lib/hecks/fuzzing/self_consistency/saga_redelivery.rb +1 -1
- data/lib/hecks/fuzzing/self_consistency.rb +6 -2
- data/lib/hecks/fuzzing/shrinker.rb +3 -1
- data/lib/hecks/fuzzing/value_generator.rb +19 -5
- data/lib/hecks/gate/stages.yml +33 -7
- data/lib/hecks/grammar/evolve.rb +1 -1
- data/lib/hecks/grammar/expression_operators.json +372 -0
- data/lib/hecks/grammar/operators.rb +3 -1
- data/lib/hecks/hecks/adapters/codebase/corpus_tasks.rb +10 -10
- data/lib/hecks/hecks/adapters/codebase/inspection/rubycritic.rb +35 -0
- data/lib/hecks/hecks/adapters/codebase/inspection.rb +104 -0
- data/lib/hecks/hecks/adapters/codebase/publishing.rb +2 -1
- data/lib/hecks/hecks/adapters/codebase/release_facts.rb +12 -2
- data/lib/hecks/hecks/adapters/codebase/source_tree.rb +2 -1
- data/lib/hecks/hecks/adapters/codebase/test_runner.rb +1 -1
- data/lib/hecks/hecks/adapters/cost_explorer.rb +8 -5
- data/lib/hecks/hecks/adapters/deploy_toolchain/statuses.rb +2 -1
- data/lib/hecks/hecks/adapters/finding_github.rb +13 -6
- data/lib/hecks/hecks/adapters/first_admin.rb +2 -2
- data/lib/hecks/hecks/adapters/github_rulesets.rb +6 -3
- data/lib/hecks/hecks/adapters/in_process_boot.rb +1 -1
- data/lib/hecks/hecks/adapters/journal_store/compaction.rb +1 -1
- data/lib/hecks/hecks/adapters/journal_store/edge_audit.rb +10 -20
- data/lib/hecks/hecks/adapters/journal_store/held_domain.rb +28 -5
- data/lib/hecks/hecks/adapters/local_files.rb +2 -2
- data/lib/hecks/hecks/adapters/rehearsal/audit_report.rb +107 -0
- data/lib/hecks/hecks/adapters/rehearsal/census.rb +71 -0
- data/lib/hecks/hecks/adapters/rehearsal/declaration.rb +133 -0
- data/lib/hecks/hecks/adapters/rehearsal/environment.rb +83 -0
- data/lib/hecks/hecks/adapters/rehearsal/inquiry.rb +142 -0
- data/lib/hecks/hecks/adapters/rehearsal/memory_store.rb +96 -0
- data/lib/hecks/hecks/adapters/rehearsal/postgres_census.rb +100 -0
- data/lib/hecks/hecks/adapters/rehearsal/postgres_reading.rb +69 -0
- data/lib/hecks/hecks/adapters/rehearsal/postgres_store.rb +133 -0
- data/lib/hecks/hecks/adapters/rehearsal/rows.rb +41 -0
- data/lib/hecks/hecks/adapters/rehearsal/rust_audit.rb +83 -0
- data/lib/hecks/hecks/adapters/rehearsal/subject.rb +104 -0
- data/lib/hecks/hecks/adapters/rehearsal.rb +54 -0
- data/lib/hecks/hecks/adapters/rehearsal_store.adapter +7 -0
- data/lib/hecks/hecks/adapters/rehearsal_store.rb +81 -0
- data/lib/hecks/hecks/adapters/terminal.rb +13 -11
- data/lib/hecks/hecks/codebase.behaviors +13 -1
- data/lib/hecks/hecks/codebase.bluebook +239 -5
- data/lib/hecks/hecks/custodian.behaviors +24 -6
- data/lib/hecks/hecks/custodian.bluebook +6 -1227
- data/lib/hecks/hecks/deploy.behaviors +1 -1
- data/lib/hecks/hecks/deploy.world +1 -3
- data/lib/hecks/hecks/hecks.behaviors +1 -1
- data/lib/hecks/hecks/hecks.hecksagon +53 -18
- data/lib/hecks/hecks/hecks.world +13 -10
- data/lib/hecks/hecks/rehearsal.bluebook +477 -0
- data/lib/hecks/hecks/site.behaviors +1 -1
- data/lib/hecks/hecks/site.world +0 -1
- data/lib/hecks/hecks/tooling.bluebook +1238 -0
- data/lib/hecks/indifferent_key.rb +20 -0
- data/lib/hecks/language/adapter.bluebook +13 -3
- data/lib/hecks/language/bluebook/aggregate.bluebook +23 -0
- data/lib/hecks/language/bluebook/bluebook.behaviors +23 -0
- data/lib/hecks/language/bluebook/bluebook.bluebook +15 -0
- data/lib/hecks/language/bluebook/entity.bluebook +18 -0
- data/lib/hecks/language/bluebook/policy.behaviors +19 -0
- data/lib/hecks/language/bluebook/policy.bluebook +9 -1
- data/lib/hecks/language/bluebook/query.bluebook +1 -1
- data/lib/hecks/language/bluebook/vocabulary.bluebook +63 -4
- data/lib/hecks/language/hecksagon/domain_port.bluebook +1 -1
- data/lib/hecks/language/hecksagon/hecksagon.bluebook +22 -0
- data/lib/hecks/language/oidc.json +10 -0
- data/lib/hecks/language/port.bluebook +4 -4
- data/lib/hecks/literal.rb +12 -3
- data/lib/hecks/ports/key_vault.rb +7 -2
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +21 -10
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/boot.rb +4 -2
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +2 -1
- data/lib/hecks/ports/persistence/repository_factory.rb +6 -3
- data/lib/hecks/projection_files/expression_tables.rb +7 -2
- data/lib/hecks/projections/deploy/box/compose.rb +6 -4
- data/lib/hecks/projections/deploy/box/edge.rb +62 -0
- data/lib/hecks/projections/deploy/box/logging.rb +99 -0
- data/lib/hecks/projections/deploy/box/roll_scripts.rb +16 -8
- data/lib/hecks/projections/deploy/box/settings/cdn.rb +40 -0
- data/lib/hecks/projections/deploy/box/settings/logs.rb +40 -0
- data/lib/hecks/projections/deploy/box/settings/patterns.rb +11 -0
- data/lib/hecks/projections/deploy/box/settings/types.rb +16 -1
- data/lib/hecks/projections/deploy/box/settings.rb +6 -2
- data/lib/hecks/projections/deploy/box/stack_yaml.rb +5 -2
- data/lib/hecks/projections/deploy/box/templates/Makefile.tmpl +16 -0
- data/lib/hecks/projections/deploy/box/templates/box.yaml.tmpl +8 -4
- data/lib/hecks/projections/deploy/box/templates/deploy-box.sh.tmpl +32 -0
- data/lib/hecks/projections/deploy/box/templates/deploy-service.sh.tmpl +1 -1
- data/lib/hecks/projections/deploy/box/templates/edge.mk.tmpl +17 -0
- data/lib/hecks/projections/deploy/box/templates/edge.yaml.tmpl +65 -0
- data/lib/hecks/projections/deploy/box/templates/log-group.yaml.tmpl +12 -0
- data/lib/hecks/projections/deploy/box/templates/log-policy.yaml.tmpl +7 -0
- data/lib/hecks/projections/deploy/box/templates/render-compose-taskdef.sh.tmpl +5 -4
- data/lib/hecks/projections/deploy/box/templates/render-compose.sh.tmpl +5 -4
- data/lib/hecks/projections/deploy/box.rb +15 -3
- data/lib/hecks/projections/deploy/preview/preview.sh.erb +1 -1
- data/lib/hecks/projections/deploy/shared.rb +5 -2
- data/lib/hecks/projections/model/deviations.rb +2 -2
- data/lib/hecks/projections/model.rb +2 -2
- data/lib/hecks/projections/oidc.rb +13 -1
- data/lib/hecks/projections/shape.rb +5 -2
- data/lib/hecks/projections/site/cms_editor/attributes.rb +26 -6
- data/lib/hecks/projections/site/cms_editor/blocks.rb +157 -0
- data/lib/hecks/projections/site/cms_editor/checks.rb +14 -1
- data/lib/hecks/projections/site/cms_editor/clearing.rb +70 -0
- data/lib/hecks/projections/site/cms_editor/color.rb +124 -0
- data/lib/hecks/projections/site/cms_editor/composition.rb +42 -0
- data/lib/hecks/projections/site/cms_editor/destructive.rb +61 -0
- data/lib/hecks/projections/site/cms_editor/drafts.rb +102 -0
- data/lib/hecks/projections/site/cms_editor/hints.rb +74 -0
- data/lib/hecks/projections/site/cms_editor/icons.rb +86 -0
- data/lib/hecks/projections/site/cms_editor/key_kinds.rb +31 -0
- data/lib/hecks/projections/site/cms_editor/limits.rb +72 -0
- data/lib/hecks/projections/site/cms_editor/media.rb +1 -1
- data/lib/hecks/projections/site/cms_editor/moment.rb +34 -0
- data/lib/hecks/projections/site/cms_editor/palette.rb +115 -0
- data/lib/hecks/projections/site/cms_editor/pickers.rb +85 -0
- data/lib/hecks/projections/site/cms_editor/preview.rb +91 -0
- data/lib/hecks/projections/site/cms_editor/relations.rb +92 -0
- data/lib/hecks/projections/site/cms_editor/scale_checks.rb +66 -0
- data/lib/hecks/projections/site/cms_editor/scheduling.rb +150 -0
- data/lib/hecks/projections/site/cms_editor/schema.rb +96 -19
- data/lib/hecks/projections/site/cms_editor/setting.rb +52 -9
- data/lib/hecks/projections/site/cms_editor/targets.rb +86 -0
- data/lib/hecks/projections/site/cms_editor/templates/package.json.tmpl +18 -1
- data/lib/hecks/projections/site/cms_editor/templates/src/app.ts.tmpl +251 -57
- data/lib/hecks/projections/site/cms_editor/templates/src/assets.ts.tmpl +98 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/auth/membership.ts.tmpl +32 -24
- data/lib/hecks/projections/site/cms_editor/templates/src/browser/app.css.tmpl +412 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/browser/autosave.js.tmpl +144 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/browser/blocks.js.tmpl +500 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/browser/body_doc.js.tmpl +175 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/browser/body_editor.js.tmpl +418 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/browser/epoch.js.tmpl +61 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/browser/forms.js.tmpl +210 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/browser/lists.js.tmpl +40 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/browser/main.js.tmpl +26 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/browser/media_picker.js.tmpl +232 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/browser/moments.js.tmpl +73 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/browser/pickers.js.tmpl +152 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/browser/preview.js.tmpl +98 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/browser/shell.js.tmpl +140 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/browser/toast.js.tmpl +66 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/choices.ts.tmpl +99 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/commands.ts.tmpl +78 -24
- data/lib/hecks/projections/site/cms_editor/templates/src/config.ts.tmpl +6 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/flash.ts.tmpl +52 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/host.ts.tmpl +19 -2
- data/lib/hecks/projections/site/cms_editor/templates/src/media/handler.ts.tmpl +31 -8
- data/lib/hecks/projections/site/cms_editor/templates/src/names.ts.tmpl +36 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/schema.ts.tmpl +131 -2
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/availability.ts.tmpl +15 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/block_list.js.tmpl +213 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/blocks.ts.tmpl +275 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/body_model.js.tmpl +8 -5
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/detail.ts.tmpl +88 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/dialogs.ts.tmpl +43 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/fields.ts.tmpl +229 -34
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/html.ts.tmpl +40 -81
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/icons.ts.tmpl +15 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/input.ts.tmpl +29 -12
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/links.ts.tmpl +100 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/moments.ts.tmpl +44 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/outline.ts.tmpl +53 -17
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/pages.ts.tmpl +178 -88
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/paging.ts.tmpl +82 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/pickers.ts.tmpl +110 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/preview.ts.tmpl +87 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/related.ts.tmpl +97 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/schedule.ts.tmpl +160 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/shell.ts.tmpl +211 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/table.ts.tmpl +151 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/theme.js.tmpl +16 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/values.ts.tmpl +69 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/words.ts.tmpl +97 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/world.ts.tmpl +35 -0
- data/lib/hecks/projections/site/cms_editor/templates/tsconfig.json.tmpl +2 -1
- data/lib/hecks/projections/site/cms_editor/theme.rb +29 -0
- data/lib/hecks/projections/site/cms_editor/theme_checks.rb +57 -0
- data/lib/hecks/projections/site/cms_editor.rb +49 -23
- data/lib/hecks/projections/site/role_probe.rb +2 -0
- data/lib/hecks/projections/site/site_root/workflow.rb +5 -0
- data/lib/hecks/projector/cli_audience.rb +22 -1
- data/lib/hecks/projector/cli_projector/listing.rb +1 -1
- data/lib/hecks/projector/cli_projector/option_specs.rb +1 -1
- data/lib/hecks/projector/cli_projector/usage.rb +15 -4
- data/lib/hecks/projector/docs_projector/closing.rb +1 -1
- data/lib/hecks/projector/docs_projector.rb +1 -1
- data/lib/hecks/projector/exporter/bindings.rb +40 -10
- data/lib/hecks/projector/exporter/capability_values.rb +55 -0
- data/lib/hecks/projector/narrate_projector/reactions.rb +1 -1
- data/lib/hecks/projector/target.rb +11 -0
- data/lib/hecks/projector.rb +32 -5
- data/lib/hecks/quality_control/adapters/github_checks.rb +12 -8
- data/lib/hecks/quality_control/adapters/github_issues.rb +8 -4
- data/lib/hecks/quality_control/cli/qa_open_pr.rb +2 -2
- data/lib/hecks/quality_control/cli/qa_sweep/checks/shrinking.rb +1 -1
- data/lib/hecks/quality_control/cli/qa_sweep/finding_report.rb +1 -1
- data/lib/hecks/quality_control/cli/qa_sweep.rb +1 -1
- data/lib/hecks/quality_control/quality_control.bluebook +6 -2
- data/lib/hecks/release/approval.rb +64 -0
- data/lib/hecks/router/{door.rb → aggregate_reader.rb} +5 -4
- data/lib/hecks/router/namespace_installer.rb +16 -16
- data/lib/hecks/router.rb +1 -1
- data/lib/hecks/runtime/best_effort.rb +22 -0
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +12 -2
- data/lib/hecks/runtime/command_interpreter.rb +3 -2
- data/lib/hecks/runtime/command_rules/emission.rb +1 -1
- data/lib/hecks/runtime/dispatcher.rb +4 -4
- data/lib/hecks/runtime/entity_element/location.rb +1 -1
- data/lib/hecks/runtime/event.rb +5 -0
- data/lib/hecks/runtime/identity.rb +7 -6
- data/lib/hecks/runtime/interpreting.rb +2 -1
- data/lib/hecks/runtime/invocation_builder.rb +1 -1
- data/lib/hecks/runtime/loader/boot_steps.rb +8 -5
- data/lib/hecks/runtime/loader.rb +10 -10
- data/lib/hecks/runtime/policy_interpreter/arguments.rb +7 -4
- data/lib/hecks/runtime/policy_interpreter/fan_out.rb +3 -3
- data/lib/hecks/runtime/policy_interpreter.rb +6 -6
- data/lib/hecks/runtime/port_operation_interpreter.rb +1 -1
- data/lib/hecks/runtime/reaction_invocation.rb +1 -1
- data/lib/hecks/runtime/rebuild_sweep.rb +11 -1
- data/lib/hecks/runtime/registry/ask_verification.rb +35 -0
- data/lib/hecks/runtime/registry/hecksagon_rules.rb +10 -0
- data/lib/hecks/runtime/registry/merging.rb +1 -1
- data/lib/hecks/runtime/registry/providers.rb +16 -0
- data/lib/hecks/runtime/registry/read_repository.rb +6 -4
- data/lib/hecks/runtime/registry/verification.rb +4 -1
- data/lib/hecks/runtime/registry/world_defaults.rb +29 -0
- data/lib/hecks/runtime/registry.rb +1 -1
- data/lib/hecks/runtime/saga_interpreter/compensation.rb +1 -1
- data/lib/hecks/runtime/saga_interpreter/delivery.rb +4 -4
- data/lib/hecks/runtime/saga_interpreter/transitions.rb +1 -1
- data/lib/hecks/runtime/saga_interpreter.rb +3 -3
- data/lib/hecks/runtime/tenant_check.rb +5 -4
- data/lib/hecks/runtime/value/coercion.rb +2 -2
- data/lib/hecks/runtime/value/validation.rb +16 -4
- data/lib/hecks/runtime.rb +9 -9
- data/lib/hecks/rust_build/host.rb +11 -4
- data/lib/hecks/rust_build/prebuilt_binary.rb +101 -0
- data/lib/hecks/rust_build/project_rust/target_ir.rb +1 -1
- data/lib/hecks/rust_build/wasm.rb +1 -1
- data/lib/hecks/rust_build/wasm_browser.rb +1 -1
- data/lib/hecks/rust_build.rb +11 -0
- data/lib/hecks/served_options.rb +53 -0
- data/lib/hecks/site/bluebook/site.bluebook +1 -0
- data/lib/hecks/storehouse/audit.rb +1 -1
- data/lib/hecks/storehouse/dispatching.rb +8 -6
- data/lib/hecks/storehouse/introspection.rb +2 -2
- data/lib/hecks/storehouse/querying.rb +6 -4
- data/lib/hecks/storehouse.rb +4 -4
- data/lib/hecks/tools/ci_gate_decision.rb +35 -0
- data/lib/hecks/tools/ci_gates.rb +20 -1
- data/lib/hecks/tools/comment_style/source_file.rb +4 -1
- data/lib/hecks/tools/deploy_recipe_lint/fixtures.rb +4 -1
- data/lib/hecks/tools/lanes/files.rb +8 -0
- data/lib/hecks/tools/lanes/prebuilt_workflow.rb +81 -0
- data/lib/hecks/tools/lanes/rows.rb +6 -0
- data/lib/hecks/tools/lanes.rb +1 -0
- data/lib/hecks/tools/oidc_manifests.rb +4 -5
- data/lib/hecks/tools/regeneration_run.rb +11 -4
- data/lib/hecks/tools/release_only.rb +111 -0
- data/lib/hecks/tools/site_routes/editor_files.rb +12 -5
- data/lib/hecks/tools/tenant_provisioning.rb +2 -2
- data/lib/hecks/tools/tools_doc.rb +4 -4
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks/vocabulary.rb +19 -5
- data/lib/hecks.rb +15 -15
- data/lib/rubocop/cop/hecks/env_dump.rb +69 -0
- data/lib/rubocop/cop/hecks/monotonic_duration.rb +53 -0
- data/lib/rubocop/cop/hecks/no_wall_clock_in_runtime.rb +34 -0
- data/lib/rubocop/cop/hecks/raw_shell_interpolation.rb +79 -0
- data/lib/rubocop/cop/hecks/sleep_in_spec.rb +34 -0
- data/lib/rubocop/cop/hecks/swallowed_rescue.rb +37 -0
- data/lib/rubocop/cop/hecks/symbol_string_key_mix.rb +50 -0
- data/lib/rubocop/cop/hecks/untyped_raise.rb +37 -0
- data/prebuilt/9ee54e2f0d4d6d3fe046827779b221e02de665fba911a048ae5dd8b8fbfb7d90.marshal +0 -0
- data/prebuilt/d74a00e996222c1d8b676d5959092effc46b4dc59e0555f3bf9554ac58e3094a.marshal +0 -0
- data/prebuilt/verdicts-df19f35868d3284a70f55e42dcef87ef4e65e6a678e5135f72e1f6cff4c1357b.json +1 -0
- data/rust/.gitignore +6 -0
- data/rust/Cargo.toml +6 -0
- data/rust/build/src/binding_pass.rs +103 -0
- data/rust/build/src/cargo_sync.rs +6 -0
- data/rust/build/src/lineage_pass.rs +3 -3
- data/rust/build/src/main.rs +17 -5
- data/rust/build/src/pipeline.rs +48 -5
- data/rust/build/src/seams_pass.rs +123 -0
- data/rust/build/src/translation_pass.rs +167 -0
- data/rust/codegen/src/asks.rs +103 -0
- data/rust/codegen/src/commands.rs +13 -8
- data/rust/codegen/src/constraints.rs +25 -0
- data/rust/codegen/src/domain_generator.rs +2 -0
- data/rust/codegen/src/expr_emitter.rs +13 -0
- data/rust/codegen/src/json_codec.rs +54 -15
- data/rust/codegen/src/main.rs +1 -0
- data/rust/codegen/src/mutations.rs +128 -3
- data/rust/codegen/src/queries.rs +11 -2
- data/rust/codegen/src/reactions.rs +1 -1
- data/rust/codegen/src/types.rs +166 -8
- data/rust/host/HECKS_RELEASE +1 -1
- data/rust/host/src/api.rs +4 -4
- data/rust/host/src/bin/validate_harness.rs +43 -0
- data/rust/host/src/boundary_fuzz/expr_json.rs +2 -2
- data/rust/host/src/boundary_fuzz/ir.rs +1 -0
- data/rust/host/src/boundary_fuzz/server.rs +1 -1
- data/rust/host/src/checkout.rs +35 -19
- data/rust/host/src/commerce_ir.rs +408 -2
- data/rust/host/src/expr_json.rs +118 -0
- data/rust/host/src/extension.rs +13 -1
- data/rust/host/src/lib.rs +1 -0
- data/rust/host/src/mint.rs +1 -1
- data/rust/host/src/payments/tests.rs +27 -1
- data/rust/host/src/payments.rs +12 -4
- data/rust/host/src/platform_signin/attack_tests.rs +1100 -0
- data/rust/host/src/platform_signin/config.rs +512 -0
- data/rust/host/src/platform_signin/cookie.rs +237 -0
- data/rust/host/src/platform_signin/extension.rs +190 -0
- data/rust/host/src/platform_signin/fetcher.rs +322 -0
- data/rust/host/src/platform_signin/handler.rs +251 -0
- data/rust/host/src/platform_signin/handler_tests.rs +554 -0
- data/rust/host/src/platform_signin/jti.rs +196 -0
- data/rust/host/src/platform_signin/jwks.rs +542 -0
- data/rust/host/src/platform_signin/jws.rs +249 -0
- data/rust/host/src/platform_signin/known_answer_tests.rs +237 -0
- data/rust/host/src/platform_signin/mapping_tests.rs +254 -0
- data/rust/host/src/platform_signin/mod.rs +75 -0
- data/rust/host/src/platform_signin/mode_tests.rs +382 -0
- data/rust/host/src/platform_signin/pages.rs +204 -0
- data/rust/host/src/platform_signin/query.rs +233 -0
- data/rust/host/src/platform_signin/session.rs +151 -0
- data/rust/host/src/platform_signin/settings.rs +385 -0
- data/rust/host/src/platform_signin/strict_json.rs +351 -0
- data/rust/host/src/platform_signin/test_support.rs +235 -0
- data/rust/host/src/platform_signin/ttl_tests.rs +94 -0
- data/rust/host/src/platform_signin/verify.rs +604 -0
- data/rust/host/src/platform_signin/wiring_support.rs +319 -0
- data/rust/host/src/query_step.rs +22 -4
- data/rust/host/src/reference_validate.rs +653 -108
- data/rust/host/src/resend.rs +2 -0
- data/rust/host/src/server.rs +19 -6
- data/rust/host/src/ui_schema.rs +132 -5
- data/rust/host/src/web/newsletter.rs +7 -8
- data/rust/host/src/web/newsletter_send.rs +38 -13
- data/rust/host/src/web/registrations.rs +229 -26
- data/rust/host/src/web.rs +53 -9
- data/rust/parser/src/emit.rs +38 -6
- data/rust/parser/src/expr/ast_json.rs +7 -1
- data/rust/parser/src/expr/resolver.rs +171 -6
- data/rust/parser/src/ir.rs +7 -0
- data/rust/parser/src/keywords.rs +24 -0
- data/rust/parser/src/main.rs +3 -0
- data/rust/parser/src/parse/aggregate.rs +2 -1
- data/rust/parser/src/parse/chapter.rs +2 -2
- data/rust/parser/src/parse/command.rs +2 -0
- data/rust/parser/src/parse/hecksagon.rs +5 -2
- data/rust/parser/src/parse/lifecycle.rs +81 -0
- data/rust/parser/src/parse/mod.rs +15 -1
- data/rust/parser/src/parse/policy.rs +13 -2
- data/rust/parser/src/parse/value_object.rs +34 -17
- data/rust/parser/src/ruby_value.rs +1 -1
- data/rust/src/exemplar/commands.rs +4 -4
- data/rust/src/exemplar/constraints.rs +1 -1
- data/rust/src/exemplar/json.rs +43 -1
- data/rust/src/kernel/cli.rs +7 -5
- data/rust/src/kernel/dispatch.rs +2 -2
- data/rust/src/kernel/dispatch_guard_tests.rs +1 -1
- data/rust/src/kernel/expr.rs +66 -3
- data/rust/src/kernel/expression_operators/arithmetic.rs +130 -5
- data/rust/src/kernel/expression_operators/text.rs +26 -2
- data/rust/src/kernel/json.rs +15 -0
- data/rust/src/kernel/mod.rs +1 -1
- data/rust/src/kernel/orchestrate.rs +54 -5
- data/rust/src/kernel/orchestrate_guard_tests.rs +67 -0
- data/rust/src/kernel/pattern.rs +60 -0
- data/rust/src/kernel/reference_lookup.rs +90 -7
- metadata +178 -38
- data/lib/hecks/doors/cli_door/nesting.rb +0 -34
- data/lib/hecks/doors/cli_door/shorthand.rb +0 -85
- data/lib/hecks/doors/cli_door.rb +0 -124
- data/lib/hecks/doors/cli_runner/answers.rb +0 -138
- data/lib/hecks/doors/cli_runner/resolution.rb +0 -146
- data/lib/hecks/doors/cli_runner/settling.rb +0 -115
- data/lib/hecks/doors/cli_runner/streaming.rb +0 -90
- data/lib/hecks/doors/cli_runner/suggestions.rb +0 -43
- data/lib/hecks/doors/cli_runner.rb +0 -66
- data/lib/hecks/doors/command_request.rb +0 -119
- data/lib/hecks/doors/handle.rb +0 -208
- data/lib/hecks/doors/json_door.rb +0 -132
- data/lib/hecks/doors/launcher_options.rb +0 -247
- data/lib/hecks/doors/mcp_door/properties.rb +0 -47
- data/lib/hecks/doors/mcp_door/tool_calls.rb +0 -113
- data/lib/hecks/doors/mcp_door/tool_definitions/auditing.rb +0 -51
- data/lib/hecks/doors/mcp_door/tool_definitions/calling.rb +0 -69
- data/lib/hecks/doors/mcp_door/tool_definitions/reading.rb +0 -97
- data/lib/hecks/doors/mcp_door/tool_definitions.rb +0 -15
- data/lib/hecks/doors/mcp_door.rb +0 -156
- data/lib/hecks/doors/mcp_door_scope/argument_policy.rb +0 -92
- data/lib/hecks/doors/mcp_door_scope/env_problems.rb +0 -55
- data/lib/hecks/doors/mcp_door_scope/presenter.rb +0 -78
- data/lib/hecks/doors/mcp_door_scope.rb +0 -201
- data/lib/hecks/doors/mcp_guide_cache.rb +0 -75
- data/lib/hecks/doors/ruby_door/aggregate_door.rb +0 -162
- data/lib/hecks/doors/ruby_door/chapter.rb +0 -89
- data/lib/hecks/doors/ruby_door.rb +0 -80
- data/lib/hecks/doors/usage_cache.rb +0 -139
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/body_widget.js.tmpl +0 -282
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/media_picker.js.tmpl +0 -109
- data/prebuilt/c28d6d9db14eea3e17161e9d9c262efee30b256cf97693a7cfd552bce24b2f7f.marshal +0 -0
- data/prebuilt/cb375cba61ca0dbe8ddb9739c306510fe05e5f4b7a9b6955caff8224c63ea0e0.marshal +0 -0
- data/prebuilt/verdicts-d6d584ad43b5f3cebf88aafbbdbf024c38574acf18e06adffbc555f7786b1c2f.json +0 -1
|
@@ -7,25 +7,40 @@ __BANNER__
|
|
|
7
7
|
// (auth/membership.ts). A visitor with neither is sent to the site's login page; nothing in the
|
|
8
8
|
// domain is read or changed before that. Posts must come from this site (the cookie is SameSite=Lax
|
|
9
9
|
// and the Origin header is checked too).
|
|
10
|
-
import {
|
|
10
|
+
import { randomUUID } from "node:crypto";
|
|
11
11
|
|
|
12
|
-
import { DomainRefusal, DomainUnavailable
|
|
13
|
-
import type { HostClient } from "@hecks/client";
|
|
12
|
+
import { DomainRefusal, DomainUnavailable } from "@hecks/client";
|
|
13
|
+
import type { Answer, HostClient } from "@hecks/client";
|
|
14
14
|
|
|
15
|
+
import { serveAsset } from "./assets.ts";
|
|
15
16
|
import { authSecret, membershipCheck } from "./auth/membership.ts";
|
|
16
17
|
import type { Membership } from "./auth/membership.ts";
|
|
17
|
-
import { readSession } from "./auth/session.ts";
|
|
18
|
+
import { clearedCookie, readSession } from "./auth/session.ts";
|
|
18
19
|
import { signIn } from "./auth/sso.ts";
|
|
19
|
-
import {
|
|
20
|
+
import { choicesFor, unknownKey } from "./choices.ts";
|
|
21
|
+
import { idOf, instanceIn, reading, runCommand } from "./commands.ts";
|
|
20
22
|
import type { State } from "./commands.ts";
|
|
21
23
|
import { EDITOR } from "./config.ts";
|
|
22
|
-
import {
|
|
24
|
+
import { flashCookie, readFlash, spentFlash } from "./flash.ts";
|
|
25
|
+
import type { Flash } from "./flash.ts";
|
|
26
|
+
import { hostClient, rowsAnswered } from "./host.ts";
|
|
27
|
+
import { at, keyOf, locate, mayEdit, qualified } from "./names.ts";
|
|
23
28
|
import { SCHEMA } from "./schema.ts";
|
|
24
29
|
import type { Aggregate, Command } from "./schema.ts";
|
|
30
|
+
import { available, needsPage } from "./ui/detail.ts";
|
|
31
|
+
import { href } from "./ui/html.ts";
|
|
25
32
|
import { commandArguments, parseForm } from "./ui/input.ts";
|
|
26
|
-
import {
|
|
33
|
+
import { backOf, plainValue } from "./ui/links.ts";
|
|
34
|
+
import type { Back } from "./ui/links.ts";
|
|
35
|
+
import { commandPage, detailPage, home, instanceCommandPage, listPage, notFound, scheduledPage } from "./ui/pages.ts";
|
|
27
36
|
import type { Submitted } from "./ui/pages.ts";
|
|
28
|
-
import {
|
|
37
|
+
import { knownStatus, narrow, slice, viewOf } from "./ui/paging.ts";
|
|
38
|
+
import type { View } from "./ui/paging.ts";
|
|
39
|
+
import { schedules, subjectQuery } from "./ui/schedule.ts";
|
|
40
|
+
import { decorate } from "./ui/shell.ts";
|
|
41
|
+
import type { World } from "./ui/world.ts";
|
|
42
|
+
import { schedulingAggregate } from "./ui/world.ts";
|
|
43
|
+
import { done, commandLabel, plural } from "./ui/words.ts";
|
|
29
44
|
__MEDIA_IMPORT__
|
|
30
45
|
|
|
31
46
|
export interface AppOptions {
|
|
@@ -38,12 +53,39 @@ export interface AppOptions {
|
|
|
38
53
|
__MEDIA_OPTION__
|
|
39
54
|
}
|
|
40
55
|
|
|
41
|
-
|
|
42
|
-
|
|
56
|
+
/**
|
|
57
|
+
* What every page is sent with. The editor loads its own script, stylesheet, pictures and icons and
|
|
58
|
+
* nothing else, so the policy names no other origin and allows no inline script and no inline style
|
|
59
|
+
* (no `style` attribute, no style element: the stylesheet is a file, and the writing surface is told
|
|
60
|
+
* to inject none). `data:` in `img-src` is for the sprite-free case of a picture drawn from a data
|
|
61
|
+
* address; `blob:` shows a picture chosen for upload before it is sent. Script may still set styles
|
|
62
|
+
* through the page's object model, which the policy allows.
|
|
63
|
+
*/
|
|
64
|
+
const POLICY = [
|
|
65
|
+
"default-src 'none'",
|
|
66
|
+
"script-src 'self'",
|
|
67
|
+
"style-src 'self'",
|
|
68
|
+
"img-src 'self' data: blob:",
|
|
69
|
+
"connect-src 'self'",
|
|
70
|
+
"form-action 'self'",
|
|
71
|
+
__FRAME_POLICY__
|
|
72
|
+
"base-uri 'none'",
|
|
73
|
+
"frame-ancestors 'none'",
|
|
74
|
+
].join("; ");
|
|
75
|
+
|
|
76
|
+
const PAGE_HEADERS = {
|
|
77
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
78
|
+
"Cache-Control": "no-store",
|
|
79
|
+
"Content-Security-Policy": POLICY,
|
|
80
|
+
"Referrer-Policy": "same-origin",
|
|
81
|
+
"X-Content-Type-Options": "nosniff",
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const plain = (body: string, status: number): Response => new Response(body, { status, headers: { "Content-Type": "text/plain; charset=utf-8" } });
|
|
43
85
|
|
|
44
|
-
const redirect = (location: string, status = 303,
|
|
86
|
+
const redirect = (location: string, status = 303, ...cookies: string[]): Response => {
|
|
45
87
|
const headers = new Headers({ Location: location });
|
|
46
|
-
|
|
88
|
+
for (const cookie of cookies) headers.append("Set-Cookie", cookie);
|
|
47
89
|
return new Response(null, { status, headers });
|
|
48
90
|
};
|
|
49
91
|
|
|
@@ -54,6 +96,14 @@ function sameSite(request: Request): boolean {
|
|
|
54
96
|
return (request.headers.get("sec-fetch-site") ?? "same-origin") !== "cross-site";
|
|
55
97
|
}
|
|
56
98
|
|
|
99
|
+
/** What a route needs to answer: show a page for this person, or send them on with a notice. */
|
|
100
|
+
interface Scene {
|
|
101
|
+
/** The person's role, which decides the chapters they may edit. */
|
|
102
|
+
role: string | null;
|
|
103
|
+
show(body: string, status?: number, notice?: Flash): Promise<Response>;
|
|
104
|
+
sent(location: string, message: string): Response;
|
|
105
|
+
}
|
|
106
|
+
|
|
57
107
|
/** The editor, as a handler. */
|
|
58
108
|
export function createApp(options: AppOptions = {}): (request: Request) => Promise<Response> {
|
|
59
109
|
const secret = options.secret ?? authSecret();
|
|
@@ -62,94 +112,193 @@ export function createApp(options: AppOptions = {}): (request: Request) => Promi
|
|
|
62
112
|
const client = hostClient({ url: options.url, fetch: options.fetch });
|
|
63
113
|
__MEDIA_SETUP__
|
|
64
114
|
|
|
115
|
+
/**
|
|
116
|
+
* The instances each aggregate holds, for the navigation; a host that cannot say shows no counts.
|
|
117
|
+
* The host has no count to ask for, so they are the lengths of what one read answers, the same
|
|
118
|
+
* read a page of a list is made from.
|
|
119
|
+
*/
|
|
120
|
+
async function counts(from: HostClient): Promise<Record<string, number>> {
|
|
121
|
+
try {
|
|
122
|
+
const answer = await from.read();
|
|
123
|
+
return Object.fromEntries(SCHEMA.aggregates.map((agg) => [keyOf(agg), from.instancesOf(answer, qualified(agg)).length]));
|
|
124
|
+
} catch {
|
|
125
|
+
return {};
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function sceneFor(email: string, role: string | null, flash: Flash | null, from: HostClient): Scene {
|
|
130
|
+
return {
|
|
131
|
+
role,
|
|
132
|
+
async show(body, status = 200, notice) {
|
|
133
|
+
const text = decorate(body, { email, role, counts: await counts(from), flash: notice ?? flash });
|
|
134
|
+
const response = new Response(text, { status, headers: PAGE_HEADERS });
|
|
135
|
+
if (flash) response.headers.append("Set-Cookie", spentFlash());
|
|
136
|
+
return response;
|
|
137
|
+
},
|
|
138
|
+
sent: (location, message) => redirect(location, 303, flashCookie({ kind: "success", message }, secret, now)),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
65
142
|
async function handle(request: Request): Promise<Response> {
|
|
66
143
|
const url = new URL(request.url);
|
|
67
144
|
const path = url.pathname.replace(/\/+$/, "") || "/";
|
|
68
145
|
if (path === EDITOR.ssoPath) return enter(url, secret, membership, now);
|
|
69
|
-
if (path !== EDITOR.basePath && !path.startsWith(`${EDITOR.basePath}/`)) return
|
|
146
|
+
if (path !== EDITOR.basePath && !path.startsWith(`${EDITOR.basePath}/`)) return anonymous(notFound(path), 404);
|
|
147
|
+
if (path === href("logout") && request.method === "POST") return signOut(request);
|
|
70
148
|
|
|
71
149
|
const email = readSession(request.headers.get("cookie"), secret, now);
|
|
72
150
|
if (!email || !(await membership.isAdmitted(email))) {
|
|
73
|
-
return request.method === "GET" ? redirect(EDITOR.login, 302) :
|
|
151
|
+
return request.method === "GET" ? redirect(EDITOR.login, 302) : plain("Sign in again.", 403);
|
|
74
152
|
}
|
|
75
|
-
if (request.method === "POST" && !sameSite(request)) return
|
|
153
|
+
if (request.method === "POST" && !sameSite(request)) return plain("Cross-site post refused.", 403);
|
|
76
154
|
const segments = path.slice(EDITOR.basePath.length).split("/").filter(Boolean).map(decodeURIComponent);
|
|
155
|
+
const role = membership.roleOf ? await membership.roleOf(email) : null;
|
|
77
156
|
__MEDIA_ROUTE__
|
|
78
|
-
|
|
157
|
+
const reader = request.method === "GET" ? reading(client) : client;
|
|
158
|
+
const scene = sceneFor(email, role, readFlash(request.headers.get("cookie"), secret, now), reader);
|
|
159
|
+
return route(reader, request, url, segments, scene);
|
|
79
160
|
}
|
|
80
161
|
|
|
81
162
|
return async (request) => {
|
|
82
163
|
try {
|
|
83
164
|
return await handle(request);
|
|
84
165
|
} catch (err) {
|
|
85
|
-
if (err instanceof DomainUnavailable) return
|
|
166
|
+
if (err instanceof DomainUnavailable) return anonymous(notFound(`The domain could not be reached: ${err.message}`, "The domain is not answering"), 502);
|
|
86
167
|
throw err;
|
|
87
168
|
}
|
|
88
169
|
};
|
|
89
170
|
}
|
|
90
171
|
|
|
91
|
-
/**
|
|
92
|
-
|
|
172
|
+
/** A page for someone with no session to show, such as a path the editor does not serve. */
|
|
173
|
+
function anonymous(body: string, status: number): Response {
|
|
174
|
+
return new Response(decorate(body, { email: "", role: null, counts: {}, flash: null }), { status, headers: PAGE_HEADERS });
|
|
175
|
+
}
|
|
93
176
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
return
|
|
177
|
+
/** Ends the session: the cookie is cleared and the person is sent to the site's login page. */
|
|
178
|
+
function signOut(request: Request): Response {
|
|
179
|
+
if (!sameSite(request)) return plain("Cross-site post refused.", 403);
|
|
180
|
+
return redirect(EDITOR.login, 303, clearedCookie());
|
|
98
181
|
}
|
|
99
182
|
|
|
100
183
|
async function enter(url: URL, secret: string, membership: Membership, now: () => number): Promise<Response> {
|
|
101
184
|
const result = await signIn(url.searchParams.get("token"), secret, membership, now);
|
|
102
|
-
if (!result.ok) return
|
|
185
|
+
if (!result.ok) return plain(result.message, result.status);
|
|
103
186
|
return redirect(result.location, 302, result.cookie);
|
|
104
187
|
}
|
|
105
188
|
|
|
106
|
-
function route(client: HostClient, request: Request, url: URL, segments: string[]): Promise<Response> | Response {
|
|
107
|
-
|
|
108
|
-
if (segments.length ===
|
|
109
|
-
if (
|
|
110
|
-
const agg
|
|
111
|
-
if (!agg) return
|
|
112
|
-
if (
|
|
113
|
-
if (verb ===
|
|
114
|
-
if (request.method === "
|
|
115
|
-
if (
|
|
116
|
-
|
|
189
|
+
function route(client: HostClient, request: Request, url: URL, segments: string[], scene: Scene): Promise<Response> | Response {
|
|
190
|
+
if (segments.length === 0) return scene.show(home());
|
|
191
|
+
if (segments[0] === "assets" && segments.length === 2 && request.method === "GET") return serveAsset(segments[1], request);
|
|
192
|
+
if (segments[0] === "scheduled" && segments.length === 1 && request.method === "GET" && SCHEMA.scheduling) return scheduledView(client, scene);
|
|
193
|
+
const { agg, rest: [verb, ...rest] } = locate(segments);
|
|
194
|
+
if (!agg) return scene.show(notFound(`No aggregate ${segments.slice(0, SCHEMA.chapters ? 2 : 1).join("/")}.`), 404);
|
|
195
|
+
if (!mayEdit(agg, scene.role)) return scene.show(notFound(`${plural(agg.name)} are not open to your role.`, "Not open to your role"), 403);
|
|
196
|
+
if (verb === undefined) return list(client, agg, url, scene);
|
|
197
|
+
if (verb === "id" && rest.length === 1 && request.method === "GET") return detail(client, agg, rest[0], null, scene);
|
|
198
|
+
if (verb === "id" && rest.length === 2 && request.method === "GET") return instanceView(client, agg, rest[0], rest[1], url, scene);
|
|
199
|
+
if (request.method === "POST") return post(client, request, agg, verb, rest, scene);
|
|
200
|
+
if ((verb === "new" || verb === "run") && rest.length === 1) return commandView(client, agg, verb, rest[0], url, scene);
|
|
201
|
+
return scene.show(notFound(`No page ${segments.join("/")}.`), 404);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** The pairs of an address a list keeps on every link: its view and that view's arguments, not the filters or the page. */
|
|
205
|
+
const KEPT = (key: string): boolean => !["_page", "_q", "_status"].includes(key);
|
|
206
|
+
|
|
207
|
+
/** The filters and the page an address asks for. */
|
|
208
|
+
function asked(url: URL): { q: string; status: string; page: number; size: number; params: [string, string][] } {
|
|
209
|
+
const page = Number.parseInt(url.searchParams.get("_page") ?? "1", 10);
|
|
210
|
+
return {
|
|
211
|
+
q: (url.searchParams.get("_q") ?? "").trim().slice(0, 200),
|
|
212
|
+
status: url.searchParams.get("_status") ?? "",
|
|
213
|
+
page: Number.isFinite(page) ? page : 1,
|
|
214
|
+
size: EDITOR.pageSize,
|
|
215
|
+
params: [...url.searchParams].filter(([key]) => KEPT(key)),
|
|
216
|
+
};
|
|
117
217
|
}
|
|
118
218
|
|
|
119
|
-
async function list(client: HostClient, agg: Aggregate, url: URL): Promise<Response> {
|
|
219
|
+
async function list(client: HostClient, agg: Aggregate, url: URL, scene: Scene): Promise<Response> {
|
|
120
220
|
const query = agg.queries.find((candidate) => candidate.name === url.searchParams.get("query")) ?? null;
|
|
121
221
|
const form = parseForm(url.search.slice(1));
|
|
122
222
|
const args = query ? commandArguments(agg, query.attributes, form) : {};
|
|
123
|
-
const
|
|
124
|
-
|
|
223
|
+
const wanted = query !== null && (query.attributes.length === 0 || Object.keys(args).length > 0);
|
|
224
|
+
const raw = asked(url);
|
|
225
|
+
const filters = { ...raw, status: knownStatus(agg, raw.status) };
|
|
226
|
+
const choices = query ? await choicesFor(client, agg, query.attributes) : {};
|
|
227
|
+
const none = viewOf(0, 0, filters);
|
|
228
|
+
if (query && !wanted) return scene.show(listPage(agg, [], query, null, false, none, choices));
|
|
125
229
|
try {
|
|
126
|
-
const
|
|
127
|
-
|
|
230
|
+
const found = await rowsFor(client, agg, wanted ? query : null, args);
|
|
231
|
+
const matching = narrow(agg, found, filters.q, filters.status);
|
|
232
|
+
const view: View = viewOf(matching.length, found.length, filters);
|
|
233
|
+
return scene.show(listPage(agg, slice(matching, view), query, null, true, view, choices));
|
|
128
234
|
} catch (err) {
|
|
129
|
-
if (err instanceof DomainRefusal) return
|
|
235
|
+
if (err instanceof DomainRefusal) return scene.show(listPage(agg, [], query, err.message, true, none, choices), 200);
|
|
130
236
|
throw err;
|
|
131
237
|
}
|
|
132
238
|
}
|
|
133
239
|
|
|
134
240
|
async function rowsFor(client: HostClient, agg: Aggregate, query: { name: string } | null, args: State): Promise<[string, State][]> {
|
|
135
|
-
if (!query) return client.instancesOf(await client.read(), agg
|
|
136
|
-
const
|
|
241
|
+
if (!query) return client.instancesOf(await client.read(), qualified(agg));
|
|
242
|
+
const name = `${qualified(agg)}.${query.name}`;
|
|
243
|
+
const rows = rowsAnswered(await client.query(name, args), name);
|
|
137
244
|
return rows.flatMap((row): [string, State][] => {
|
|
138
245
|
const id = idOf(agg, row);
|
|
139
246
|
return id === null ? [] : [[id, row]];
|
|
140
247
|
});
|
|
141
248
|
}
|
|
142
249
|
|
|
143
|
-
|
|
250
|
+
const refused = (agg: Aggregate, command: Command): Flash => ({ kind: "error", message: `${commandLabel(agg, command)} was not applied. Fix what is marked and try again.` });
|
|
251
|
+
|
|
252
|
+
/** What a page of one record may read of the rest of the domain, from the read the page already made. */
|
|
253
|
+
async function worldFor(client: HostClient, answer: Answer, role: string | null, record?: { agg: Aggregate; id: string }): Promise<World> {
|
|
254
|
+
const world: World = { role, lookup: (agg, id) => instanceIn(client, answer, agg, id), rows: (agg) => client.instancesOf(answer, qualified(agg)) };
|
|
255
|
+
const asked = record && schedules(record.agg, role) ? subjectQuery(record.agg, record.id) : null;
|
|
256
|
+
const sched = schedulingAggregate();
|
|
257
|
+
if (!asked || !sched) return world;
|
|
258
|
+
const name = `${qualified(sched)}.${asked.name}`;
|
|
259
|
+
try {
|
|
260
|
+
const rows = rowsAnswered(await client.query(name, asked.args), name);
|
|
261
|
+
return { ...world, actions: rows.flatMap((row): [string, State][] => (idOf(sched, row) === null ? [] : [[idOf(sched, row) as string, row]])) };
|
|
262
|
+
} catch (err) {
|
|
263
|
+
if (err instanceof DomainRefusal || err instanceof DomainUnavailable) return world;
|
|
264
|
+
throw err;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** The screen of every pending action, for those who may edit them. */
|
|
269
|
+
async function scheduledView(client: HostClient, scene: Scene): Promise<Response> {
|
|
270
|
+
const sched = schedulingAggregate();
|
|
271
|
+
if (!sched) return scene.show(notFound("Nothing is scheduled here."), 404);
|
|
272
|
+
if (!mayEdit(sched, scene.role)) return scene.show(notFound("Scheduled actions are not open to your role.", "Not open to your role"), 403);
|
|
273
|
+
return scene.show(scheduledPage(await worldFor(client, await client.read(), scene.role)));
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
async function detail(client: HostClient, agg: Aggregate, id: string, shown: Submitted | null, scene: Scene, status = 200): Promise<Response> {
|
|
277
|
+
const answer = await client.read();
|
|
278
|
+
const values = instanceIn(client, answer, agg, id);
|
|
279
|
+
if (!values) return scene.show(notFound(`No ${agg.name} ${id}.`), 404);
|
|
280
|
+
const command = agg.commands.find((candidate) => candidate.name === shown?.command);
|
|
281
|
+
const world = await worldFor(client, answer, scene.role, { agg, id });
|
|
282
|
+
return scene.show(detailPage(agg, id, values, shown, world), status, shown && command ? refused(agg, command) : undefined);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** The page of a command that acts on one instance, when that instance is in a state it applies to. */
|
|
286
|
+
async function instanceView(client: HostClient, agg: Aggregate, id: string, name: string, url: URL, scene: Scene): Promise<Response> {
|
|
144
287
|
const values = instanceIn(client, await client.read(), agg, id);
|
|
145
|
-
if (!values) return
|
|
146
|
-
|
|
288
|
+
if (!values) return scene.show(notFound(`No ${agg.name} ${id}.`), 404);
|
|
289
|
+
const command = commandFor(agg, "id", name);
|
|
290
|
+
if (!command || !available(agg, command, values)) return scene.show(notFound(`${name} does not apply to ${id} now.`), 404);
|
|
291
|
+
const choices = await choicesFor(client, agg, command.attributes);
|
|
292
|
+
return scene.show(instanceCommandPage(agg, id, command, values, null, choices, backOf(url.searchParams.get("_back"))));
|
|
147
293
|
}
|
|
148
294
|
|
|
149
|
-
|
|
295
|
+
/** A command's page; a link may fill its inputs from the address and name the page to return to. */
|
|
296
|
+
async function commandView(client: HostClient, agg: Aggregate, verb: string, name: string, url: URL, scene: Scene): Promise<Response> {
|
|
150
297
|
const command = commandFor(agg, verb, name);
|
|
151
|
-
if (!command) return
|
|
152
|
-
|
|
298
|
+
if (!command) return scene.show(notFound(`No command ${name}.`), 404);
|
|
299
|
+
const state = commandArguments(agg, command.attributes, parseForm(url.search.slice(1)));
|
|
300
|
+
const back = backOf(url.searchParams.get("_back"));
|
|
301
|
+
return scene.show(commandPage(agg, command, href(...at(agg), verb, name), null, await choicesFor(client, agg, command.attributes), { state, back }));
|
|
153
302
|
}
|
|
154
303
|
|
|
155
304
|
/** The command a `new` (creating) or `run` (acting on another aggregate) path names, or null. */
|
|
@@ -161,16 +310,61 @@ function commandFor(agg: Aggregate, verb: string, name: string): Command | null
|
|
|
161
310
|
return command.on === agg.name ? command : null;
|
|
162
311
|
}
|
|
163
312
|
|
|
164
|
-
|
|
313
|
+
/** What a form is refused for before the command is sent: a key a picker names that is not there. */
|
|
314
|
+
async function unchecked(client: HostClient, agg: Aggregate, command: Command, args: State): Promise<{ ok: false; message: string; field: string } | null> {
|
|
315
|
+
const problem = await unknownKey(client, agg, command.attributes, args);
|
|
316
|
+
return problem ? { ok: false, message: problem.message, field: problem.field } : null;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/** The scheduled-action record the editor makes itself: a creating command gets an identity nobody typed. */
|
|
320
|
+
function withMadeIdentity(agg: Aggregate, command: Command, args: State): State {
|
|
321
|
+
const spec = SCHEMA.scheduling;
|
|
322
|
+
if (!spec?.generated || !command.creates || command.name !== spec.schedule || agg.name !== spec.aggregate || agg.chapter !== spec.chapter) return args;
|
|
323
|
+
const identity = agg.attributes.find((attr) => attr.name === agg.identity);
|
|
324
|
+
return identity ? { ...args, [identity.name]: plainValue(agg, identity, randomUUID()) } : args;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* A draft saved by the page's script while the person writes. It is answered in plain text, never
|
|
329
|
+
* with a redirect or a notice, and it only ever runs the aggregate's draft-saving command, so the
|
|
330
|
+
* live attribute is never written from here.
|
|
331
|
+
*/
|
|
332
|
+
async function autosave(client: HostClient, agg: Aggregate, command: Command, args: State, to: string | undefined): Promise<Response> {
|
|
333
|
+
if (!to || agg.drafts?.save !== command.name) return plain("Only a draft can be saved this way.", 400);
|
|
334
|
+
const outcome = (await unchecked(client, agg, command, args)) ?? (await runCommand(client, agg, command, args, to));
|
|
335
|
+
if (outcome.ok) return plain("Saved.", 200);
|
|
336
|
+
const refused = plain(outcome.message, 422);
|
|
337
|
+
if ("problems" in outcome && outcome.problems?.length) refused.headers.set("x-editor-problems", encodeURIComponent(JSON.stringify(outcome.problems)));
|
|
338
|
+
return refused;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** Where a command that worked leads: the page the person came from, another blank form, or the instance. */
|
|
342
|
+
function afterwards(agg: Aggregate, command: Command, again: boolean, id: string, back: Back | null): string {
|
|
343
|
+
if (back) return back.path;
|
|
344
|
+
return command.creates && again ? href(...at(agg), "new", command.name) : href(...at(agg), "id", id);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
async function post(client: HostClient, request: Request, agg: Aggregate, verb: string, rest: string[], scene: Scene): Promise<Response> {
|
|
165
348
|
const onInstance = verb === "id" && rest.length === 2;
|
|
166
349
|
const command = onInstance ? commandFor(agg, "id", rest[1]) : rest.length === 1 ? commandFor(agg, verb, rest[0]) : null;
|
|
167
|
-
if (!command) return
|
|
350
|
+
if (!command) return scene.show(notFound("No such command."), 404);
|
|
168
351
|
const form = parseForm(await request.text());
|
|
169
|
-
const args = commandArguments(agg, command.attributes, form);
|
|
352
|
+
const args = withMadeIdentity(agg, command, commandArguments(agg, command.attributes, form, command.empty));
|
|
170
353
|
const to = onInstance ? rest[0] : command.creates ? undefined : (form.get("__to") ?? "");
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
const
|
|
174
|
-
if (
|
|
175
|
-
|
|
354
|
+
if (request.headers.get("x-editor-autosave") === "1") return autosave(client, agg, command, args, to);
|
|
355
|
+
const outcome = to === "" ? { ok: false as const, message: "Name the instance to act on." } : ((await unchecked(client, agg, command, args)) ?? (await runCommand(client, agg, command, args, to)));
|
|
356
|
+
const back = backOf(form.get("__back"));
|
|
357
|
+
if (outcome.ok) return scene.sent(afterwards(agg, command, form.get("__again") === "1", outcome.id, back), done(agg, command));
|
|
358
|
+
const shown: Submitted = { command: command.name, message: outcome.message, values: { ...args, __to: to }, field: outcome.field, problems: "problems" in outcome ? outcome.problems : undefined };
|
|
359
|
+
if (onInstance) return refusedOnInstance(client, agg, rest[0], command, shown, scene, back);
|
|
360
|
+
const choices = await choicesFor(client, agg, command.attributes);
|
|
361
|
+
return scene.show(commandPage(agg, command, href(...at(agg), verb, command.name), shown, choices, { back }), 422, refused(agg, command));
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/** The refusal of a command run on one instance: shown on its form, or on the page it was run from. */
|
|
365
|
+
async function refusedOnInstance(client: HostClient, agg: Aggregate, id: string, command: Command, shown: Submitted, scene: Scene, back: Back | null): Promise<Response> {
|
|
366
|
+
if (!needsPage(command)) return detail(client, agg, id, shown, scene, 422);
|
|
367
|
+
const values = instanceIn(client, await client.read(), agg, id);
|
|
368
|
+
if (!values) return scene.show(notFound(`No ${agg.name} ${id}.`), 404);
|
|
369
|
+
return scene.show(instanceCommandPage(agg, id, command, values, shown, await choicesFor(client, agg, command.attributes), back), 422, refused(agg, command));
|
|
176
370
|
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
__BANNER__
|
|
2
|
+
|
|
3
|
+
// The editor's static files, for a signed-in editor only: its theme script (read from `src/ui/`),
|
|
4
|
+
// the stylesheet and script `npm run build` writes to `dist/`, and the Editor row's logo.
|
|
5
|
+
//
|
|
6
|
+
// Every file is sent with an ETag made from its bytes. A page asks for it as `name?v=<version>`
|
|
7
|
+
// where the version is the same hash, so a changed file has a new address and a request that names
|
|
8
|
+
// the current version may be kept for a year (`immutable`); any other request is revalidated and
|
|
9
|
+
// answered 304 when nothing changed. Files are `private`: they are behind the session, and a
|
|
10
|
+
// shared cache must not hold them for someone who has not signed in.
|
|
11
|
+
import { createHash } from "node:crypto";
|
|
12
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
13
|
+
import { extname } from "node:path";
|
|
14
|
+
|
|
15
|
+
import { EDITOR } from "./config.ts";
|
|
16
|
+
|
|
17
|
+
const SOURCE = new URL("./ui/", import.meta.url);
|
|
18
|
+
const DIST = new URL("../dist/", import.meta.url);
|
|
19
|
+
|
|
20
|
+
/** The files read from `src/ui/`; everything else comes from `dist/`. */
|
|
21
|
+
const SOURCES = ["theme.js"];
|
|
22
|
+
|
|
23
|
+
/** A built file's name: plain characters and a `.js` or `.css` ending, so no path leaves `dist/`. */
|
|
24
|
+
const BUILT = /^[A-Za-z0-9][A-Za-z0-9._-]*\.(js|css)$/;
|
|
25
|
+
|
|
26
|
+
const TYPES: Record<string, string> = {
|
|
27
|
+
js: "text/javascript; charset=utf-8",
|
|
28
|
+
css: "text/css; charset=utf-8",
|
|
29
|
+
png: "image/png",
|
|
30
|
+
jpg: "image/jpeg",
|
|
31
|
+
jpeg: "image/jpeg",
|
|
32
|
+
webp: "image/webp",
|
|
33
|
+
gif: "image/gif",
|
|
34
|
+
avif: "image/avif",
|
|
35
|
+
svg: "image/svg+xml",
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
interface Held {
|
|
39
|
+
bytes: Buffer;
|
|
40
|
+
type: string;
|
|
41
|
+
version: string;
|
|
42
|
+
stamp: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const held = new Map<string, Held>();
|
|
46
|
+
|
|
47
|
+
function locate(name: string): URL | null {
|
|
48
|
+
if (SOURCES.includes(name)) return new URL(name, SOURCE);
|
|
49
|
+
if (!BUILT.test(name)) return null;
|
|
50
|
+
const built = new URL(name, DIST);
|
|
51
|
+
return existsSync(built) ? built : null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function load(name: string): Held | null {
|
|
55
|
+
const where = name === "logo" ? logoPath() : locate(name);
|
|
56
|
+
if (!where || !existsSync(where)) return null;
|
|
57
|
+
const stamp = statSync(where).mtimeMs;
|
|
58
|
+
const known = held.get(name);
|
|
59
|
+
if (known && known.stamp === stamp) return known;
|
|
60
|
+
const bytes = readFileSync(where);
|
|
61
|
+
const type = TYPES[extname(String(where)).slice(1).toLowerCase()] ?? "application/octet-stream";
|
|
62
|
+
const made = { bytes, type, version: createHash("sha256").update(bytes).digest("hex").slice(0, 12), stamp };
|
|
63
|
+
held.set(name, made);
|
|
64
|
+
return made;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Where the row's logo is kept: a path from the server's working directory, or null when none is named. */
|
|
68
|
+
function logoPath(): string | null {
|
|
69
|
+
return EDITOR.logo === "" ? null : EDITOR.logo;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Whether `npm run build` has written the stylesheet and the script. */
|
|
73
|
+
export const isBuilt = (): boolean => existsSync(new URL("editor.css", DIST)) && existsSync(new URL("editor.js", DIST));
|
|
74
|
+
|
|
75
|
+
/** Whether the row names a logo that is there to be shown. */
|
|
76
|
+
export const hasLogo = (): boolean => load("logo") !== null;
|
|
77
|
+
|
|
78
|
+
/** The address of a file, with the version that makes it cacheable for good. */
|
|
79
|
+
export function assetUrl(name: string): string {
|
|
80
|
+
const version = load(name)?.version;
|
|
81
|
+
return `${EDITOR.basePath}/assets/${name}${version ? `?v=${version}` : ""}`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Answers a request for one asset: the bytes, a 304 when the browser holds them, or a 404. */
|
|
85
|
+
export function serveAsset(name: string, request: Request): Response {
|
|
86
|
+
const file = load(name);
|
|
87
|
+
if (!file) return new Response("Not found.", { status: 404, headers: { "Content-Type": "text/plain" } });
|
|
88
|
+
const etag = `"${file.version}"`;
|
|
89
|
+
const headers = new Headers({
|
|
90
|
+
"Content-Type": file.type,
|
|
91
|
+
ETag: etag,
|
|
92
|
+
"X-Content-Type-Options": "nosniff",
|
|
93
|
+
"Cache-Control": new URL(request.url).searchParams.get("v") === file.version ? "private, max-age=31536000, immutable" : "private, no-cache",
|
|
94
|
+
});
|
|
95
|
+
if (name === "logo") headers.set("Content-Security-Policy", "sandbox");
|
|
96
|
+
if (request.headers.get("if-none-match") === etag) return new Response(null, { status: 304, headers });
|
|
97
|
+
return new Response(new Uint8Array(file.bytes), { headers });
|
|
98
|
+
}
|
|
@@ -24,6 +24,8 @@ export function authSecret(): string {
|
|
|
24
24
|
export interface Membership {
|
|
25
25
|
/** True while `email` holds an editor role on the host; false when it does not, or cannot be asked. */
|
|
26
26
|
isAdmitted(email: string): Promise<boolean>;
|
|
27
|
+
/** The role `email` holds, for the header to show; null when it holds none. Answered from the same memory. */
|
|
28
|
+
roleOf?(email: string): Promise<string | null>;
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
export interface MembershipOptions {
|
|
@@ -33,47 +35,53 @@ export interface MembershipOptions {
|
|
|
33
35
|
now?: () => number;
|
|
34
36
|
}
|
|
35
37
|
|
|
36
|
-
/**
|
|
37
|
-
export function
|
|
38
|
+
/** The editor role the host's members list gives `email`, or null when it holds none or is disabled. */
|
|
39
|
+
export function roleHeld(rows: unknown, email: string): string | null {
|
|
38
40
|
const roles: readonly string[] = EDITOR.roles;
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
41
|
+
const row = Array.isArray(rows)
|
|
42
|
+
? rows.find(
|
|
43
|
+
(candidate: any) =>
|
|
44
|
+
typeof candidate?.email === "string" &&
|
|
45
|
+
candidate.email.trim().toLowerCase() === email &&
|
|
46
|
+
roles.includes(candidate.role) &&
|
|
47
|
+
candidate.disabled !== true,
|
|
48
|
+
)
|
|
49
|
+
: undefined;
|
|
50
|
+
return row ? String(row.role) : null;
|
|
46
51
|
}
|
|
47
52
|
|
|
53
|
+
/** True when the host's members list holds `email` with an editor role and not disabled. */
|
|
54
|
+
export const holdsRole = (rows: unknown, email: string): boolean => roleHeld(rows, email) !== null;
|
|
55
|
+
|
|
48
56
|
/** A membership check with its own one-minute memory. */
|
|
49
57
|
export function membershipCheck(options: MembershipOptions = {}): Membership {
|
|
50
|
-
const remembered = new Map<string, {
|
|
58
|
+
const remembered = new Map<string, { role: string | null; until: number }>();
|
|
51
59
|
const now = options.now ?? Date.now;
|
|
52
60
|
const send = options.fetch ?? fetch;
|
|
53
61
|
|
|
54
|
-
async function ask(email: string): Promise<
|
|
62
|
+
async function ask(email: string): Promise<string | null> {
|
|
55
63
|
const secret = options.secret ?? authSecret();
|
|
56
|
-
if (!secret) return
|
|
64
|
+
if (!secret) return null;
|
|
57
65
|
const cookieName = resolveAccountCookieName(process.env.HECKS_SESSION_COOKIE || EDITOR.hostCookie);
|
|
58
66
|
try {
|
|
59
67
|
const res = await send(`${hostUrl({ url: options.url })}${EDITOR.membersPath}`, {
|
|
60
68
|
headers: { Cookie: `${cookieName}=${accountToken(secret, email, TOKEN_TTL_SECONDS)}` },
|
|
61
69
|
signal: AbortSignal.timeout(EDITOR.timeoutMs),
|
|
62
70
|
});
|
|
63
|
-
return res.ok
|
|
71
|
+
return res.ok ? roleHeld(await res.json().catch(() => null), email) : null;
|
|
64
72
|
} catch {
|
|
65
|
-
return
|
|
73
|
+
return null;
|
|
66
74
|
}
|
|
67
75
|
}
|
|
68
76
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
};
|
|
77
|
+
async function roleOf(email: string): Promise<string | null> {
|
|
78
|
+
const key = email.trim().toLowerCase();
|
|
79
|
+
const hit = remembered.get(key);
|
|
80
|
+
if (hit && hit.until > now()) return hit.role;
|
|
81
|
+
const role = await ask(key);
|
|
82
|
+
remembered.set(key, { role, until: now() + EDITOR.rememberMs });
|
|
83
|
+
return role;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return { roleOf, isAdmitted: async (email: string): Promise<boolean> => (await roleOf(email)) !== null };
|
|
79
87
|
}
|