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.
Files changed (542) hide show
  1. checksums.yaml +4 -4
  2. data/examples/pizzas/bluebook/pizzas.bluebook +29 -0
  3. data/examples/pizzas/bluebook/pizzas.hecksagon +7 -0
  4. data/exe/hecks +4 -4
  5. data/lib/hecks/adapters/driven/d1.rb +2 -10
  6. data/lib/hecks/adapters/driven/heki.adapter +1 -1
  7. data/lib/hecks/adapters/driven/heki.rb +2 -12
  8. data/lib/hecks/adapters/driven/lambda.adapter +1 -1
  9. data/lib/hecks/adapters/driven/lambda.rb +3 -18
  10. data/lib/hecks/adapters/driven/postgres.rb +4 -11
  11. data/lib/hecks/adapters/driven/sqlite.adapter +1 -1
  12. data/lib/hecks/adapters/driven/sqlite.rb +2 -10
  13. data/lib/hecks/adapters/driving/admission.rb +46 -0
  14. data/lib/hecks/adapters/driving/cli/nesting.rb +36 -0
  15. data/lib/hecks/adapters/driving/cli/shorthand.rb +88 -0
  16. data/lib/hecks/adapters/driving/cli.rb +127 -0
  17. data/lib/hecks/adapters/driving/cli_runner/answers.rb +160 -0
  18. data/lib/hecks/adapters/driving/cli_runner/resolution.rb +158 -0
  19. data/lib/hecks/adapters/driving/cli_runner/settling.rb +120 -0
  20. data/lib/hecks/adapters/driving/cli_runner/streaming.rb +93 -0
  21. data/lib/hecks/adapters/driving/cli_runner/suggestions.rb +47 -0
  22. data/lib/hecks/adapters/driving/cli_runner.rb +70 -0
  23. data/lib/hecks/adapters/driving/command_request.rb +121 -0
  24. data/lib/hecks/adapters/driving/handle.rb +214 -0
  25. data/lib/hecks/adapters/driving/json.rb +136 -0
  26. data/lib/hecks/adapters/driving/launcher_options.rb +267 -0
  27. data/lib/hecks/adapters/driving/mcp/properties.rb +49 -0
  28. data/lib/hecks/adapters/driving/mcp/tool_calls.rb +116 -0
  29. data/lib/hecks/adapters/driving/mcp/tool_definitions/auditing.rb +53 -0
  30. data/lib/hecks/adapters/driving/mcp/tool_definitions/calling.rb +71 -0
  31. data/lib/hecks/adapters/driving/mcp/tool_definitions/reading.rb +99 -0
  32. data/lib/hecks/adapters/driving/mcp/tool_definitions.rb +18 -0
  33. data/lib/hecks/adapters/driving/mcp.rb +159 -0
  34. data/lib/hecks/adapters/driving/mcp_guide_cache.rb +78 -0
  35. data/lib/hecks/adapters/driving/mcp_scope/argument_policy.rb +97 -0
  36. data/lib/hecks/adapters/driving/mcp_scope/env_problems.rb +69 -0
  37. data/lib/hecks/adapters/driving/mcp_scope/presenter.rb +83 -0
  38. data/lib/hecks/adapters/driving/mcp_scope.rb +213 -0
  39. data/lib/hecks/adapters/driving/newcomer.rb +50 -0
  40. data/lib/hecks/adapters/driving/ruby/aggregate_module.rb +171 -0
  41. data/lib/hecks/adapters/driving/ruby/chapter.rb +91 -0
  42. data/lib/hecks/adapters/driving/ruby.rb +83 -0
  43. data/lib/hecks/adapters/driving/usage_cache.rb +144 -0
  44. data/lib/hecks/{doors.rb → adapters/driving.rb} +10 -6
  45. data/lib/hecks/behaviors/expectations/boot.rb +1 -1
  46. data/lib/hecks/bench/ruby_runner.rb +1 -1
  47. data/lib/hecks/bluebook/ask_resolution.rb +130 -0
  48. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +8 -2
  49. data/lib/hecks/bluebook/assembly/contracts/read_models.rb +1 -1
  50. data/lib/hecks/bluebook/assembly/contracts/rules.rb +4 -2
  51. data/lib/hecks/bluebook/assembly/contracts/structure.rb +3 -1
  52. data/lib/hecks/bluebook/assembly/marks.rb +4 -15
  53. data/lib/hecks/bluebook/assembly/scalars.rb +39 -0
  54. data/lib/hecks/bluebook/attribute.rb +1 -1
  55. data/lib/hecks/bluebook/behaviour/command.rb +13 -8
  56. data/lib/hecks/bluebook/behaviour/policy.rb +30 -0
  57. data/lib/hecks/bluebook/capabilities/attribute_values.rb +72 -0
  58. data/lib/hecks/bluebook/capabilities.rb +91 -9
  59. data/lib/hecks/bluebook/domain_port.rb +16 -4
  60. data/lib/hecks/bluebook/dsl/adapter_builder.rb +18 -6
  61. data/lib/hecks/bluebook/dsl/binding_proxy.rb +13 -0
  62. data/lib/hecks/bluebook/dsl/bluebook_builder/validation/computed_sources.rb +98 -0
  63. data/lib/hecks/bluebook/dsl/bluebook_builder/validation/fan_out_rows.rb +42 -0
  64. data/lib/hecks/bluebook/dsl/bluebook_builder/validation/projected_fields.rb +14 -2
  65. data/lib/hecks/bluebook/dsl/bluebook_builder/validation/provisions.rb +69 -3
  66. data/lib/hecks/bluebook/dsl/bluebook_builder/validation/with_projections.rb +17 -10
  67. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +5 -0
  68. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +2 -0
  69. data/lib/hecks/bluebook/dsl/command_builder/mutations.rb +8 -1
  70. data/lib/hecks/bluebook/dsl/command_builder/operands.rb +121 -0
  71. data/lib/hecks/bluebook/dsl/command_builder.rb +2 -0
  72. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +10 -1
  73. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +45 -2
  74. data/lib/hecks/bluebook/dsl/policy_builder.rb +47 -10
  75. data/lib/hecks/bluebook/dsl/rule_reference.rb +7 -4
  76. data/lib/hecks/bluebook/dsl/world_builder.rb +1 -1
  77. data/lib/hecks/bluebook/expression/ast_json/emitters.rb +6 -0
  78. data/lib/hecks/bluebook/expression/ast_json.rb +9 -6
  79. data/lib/hecks/bluebook/expression/ast_reader.rb +14 -9
  80. data/lib/hecks/bluebook/expression/projection.json +36 -0
  81. data/lib/hecks/bluebook/expression/resolver/operations.rb +31 -0
  82. data/lib/hecks/bluebook/expression/resolver/parsing.rb +38 -5
  83. data/lib/hecks/bluebook/expression/resolver/scanning.rb +22 -5
  84. data/lib/hecks/bluebook/expression/resolver/values.rb +10 -0
  85. data/lib/hecks/bluebook/expression/resolver.rb +20 -1
  86. data/lib/hecks/bluebook/hecksagon.rb +23 -3
  87. data/lib/hecks/bluebook/lifecycle.rb +22 -3
  88. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +13 -1
  89. data/lib/hecks/bluebook/meta_validator/modes.rb +4 -1
  90. data/lib/hecks/bluebook/meta_validator/readings.rb +24 -1
  91. data/lib/hecks/bluebook/meta_validator/reconstruction/heads.rb +2 -1
  92. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +6 -3
  93. data/lib/hecks/bluebook/meta_validator/shapes.rb +5 -0
  94. data/lib/hecks/bluebook/meta_validator/verdict_cache.rb +1 -1
  95. data/lib/hecks/bluebook/meta_validator.rb +12 -12
  96. data/lib/hecks/bluebook/model_check/ask_checks.rb +71 -0
  97. data/lib/hecks/bluebook/model_check/policy_checks.rb +2 -1
  98. data/lib/hecks/bluebook/model_check/reference_checks.rb +70 -0
  99. data/lib/hecks/bluebook/model_check.rb +8 -2
  100. data/lib/hecks/bluebook/policy.rb +4 -2
  101. data/lib/hecks/bluebook/smoke_test.rb +1 -1
  102. data/lib/hecks/bluebook/synthesizer.rb +2 -0
  103. data/lib/hecks/bluebook.rb +2 -0
  104. data/lib/hecks/cli/command_table.rb +1 -1
  105. data/lib/hecks/cli/console.rb +2 -1
  106. data/lib/hecks/cli/document.rb +1 -1
  107. data/lib/hecks/cli/launcher_words.rb +4 -4
  108. data/lib/hecks/cli/mcp.rb +4 -4
  109. data/lib/hecks/cli/project_cli/launcher_source.rb +2 -2
  110. data/lib/hecks/cli/project_cli/templates.rb +5 -5
  111. data/lib/hecks/cli/project_cli.rb +9 -6
  112. data/lib/hecks/cli/run.rb +2 -2
  113. data/lib/hecks/cli/smoke_http/checks.rb +4 -1
  114. data/lib/hecks/cli/smoke_http/transport.rb +3 -3
  115. data/lib/hecks/cli/smoke_http.rb +3 -0
  116. data/lib/hecks/codemod.rb +5 -2
  117. data/lib/hecks/corpus/routes.rb +7 -0
  118. data/lib/hecks/corpus/rust.rb +3 -2
  119. data/lib/hecks/deploy/bluebook/deploy.bluebook +34 -4
  120. data/lib/hecks/doc/reference/rendering.rb +6 -3
  121. data/lib/hecks/forms/app/commands.rb +8 -7
  122. data/lib/hecks/forms/app/queries.rb +2 -1
  123. data/lib/hecks/forms/app.rb +1 -1
  124. data/lib/hecks/forms/record_renderer.rb +1 -1
  125. data/lib/hecks/framework/bluebook/privacy.bluebook +11 -0
  126. data/lib/hecks/fuzzing/concurrent_dispatch.rb +4 -1
  127. data/lib/hecks/fuzzing/failure_triage.rb +2 -1
  128. data/lib/hecks/fuzzing/mutation/run.rb +11 -4
  129. data/lib/hecks/fuzzing/properties/catalog.rb +6 -4
  130. data/lib/hecks/fuzzing/properties/guaranteed_by_construction.rb +18 -11
  131. data/lib/hecks/fuzzing/properties/guards.rb +1 -1
  132. data/lib/hecks/fuzzing/self_consistency/saga_redelivery.rb +1 -1
  133. data/lib/hecks/fuzzing/self_consistency.rb +6 -2
  134. data/lib/hecks/fuzzing/shrinker.rb +3 -1
  135. data/lib/hecks/fuzzing/value_generator.rb +19 -5
  136. data/lib/hecks/gate/stages.yml +33 -7
  137. data/lib/hecks/grammar/evolve.rb +1 -1
  138. data/lib/hecks/grammar/expression_operators.json +372 -0
  139. data/lib/hecks/grammar/operators.rb +3 -1
  140. data/lib/hecks/hecks/adapters/codebase/corpus_tasks.rb +10 -10
  141. data/lib/hecks/hecks/adapters/codebase/inspection/rubycritic.rb +35 -0
  142. data/lib/hecks/hecks/adapters/codebase/inspection.rb +104 -0
  143. data/lib/hecks/hecks/adapters/codebase/publishing.rb +2 -1
  144. data/lib/hecks/hecks/adapters/codebase/release_facts.rb +12 -2
  145. data/lib/hecks/hecks/adapters/codebase/source_tree.rb +2 -1
  146. data/lib/hecks/hecks/adapters/codebase/test_runner.rb +1 -1
  147. data/lib/hecks/hecks/adapters/cost_explorer.rb +8 -5
  148. data/lib/hecks/hecks/adapters/deploy_toolchain/statuses.rb +2 -1
  149. data/lib/hecks/hecks/adapters/finding_github.rb +13 -6
  150. data/lib/hecks/hecks/adapters/first_admin.rb +2 -2
  151. data/lib/hecks/hecks/adapters/github_rulesets.rb +6 -3
  152. data/lib/hecks/hecks/adapters/in_process_boot.rb +1 -1
  153. data/lib/hecks/hecks/adapters/journal_store/compaction.rb +1 -1
  154. data/lib/hecks/hecks/adapters/journal_store/edge_audit.rb +10 -20
  155. data/lib/hecks/hecks/adapters/journal_store/held_domain.rb +28 -5
  156. data/lib/hecks/hecks/adapters/local_files.rb +2 -2
  157. data/lib/hecks/hecks/adapters/rehearsal/audit_report.rb +107 -0
  158. data/lib/hecks/hecks/adapters/rehearsal/census.rb +71 -0
  159. data/lib/hecks/hecks/adapters/rehearsal/declaration.rb +133 -0
  160. data/lib/hecks/hecks/adapters/rehearsal/environment.rb +83 -0
  161. data/lib/hecks/hecks/adapters/rehearsal/inquiry.rb +142 -0
  162. data/lib/hecks/hecks/adapters/rehearsal/memory_store.rb +96 -0
  163. data/lib/hecks/hecks/adapters/rehearsal/postgres_census.rb +100 -0
  164. data/lib/hecks/hecks/adapters/rehearsal/postgres_reading.rb +69 -0
  165. data/lib/hecks/hecks/adapters/rehearsal/postgres_store.rb +133 -0
  166. data/lib/hecks/hecks/adapters/rehearsal/rows.rb +41 -0
  167. data/lib/hecks/hecks/adapters/rehearsal/rust_audit.rb +83 -0
  168. data/lib/hecks/hecks/adapters/rehearsal/subject.rb +104 -0
  169. data/lib/hecks/hecks/adapters/rehearsal.rb +54 -0
  170. data/lib/hecks/hecks/adapters/rehearsal_store.adapter +7 -0
  171. data/lib/hecks/hecks/adapters/rehearsal_store.rb +81 -0
  172. data/lib/hecks/hecks/adapters/terminal.rb +13 -11
  173. data/lib/hecks/hecks/codebase.behaviors +13 -1
  174. data/lib/hecks/hecks/codebase.bluebook +239 -5
  175. data/lib/hecks/hecks/custodian.behaviors +24 -6
  176. data/lib/hecks/hecks/custodian.bluebook +6 -1227
  177. data/lib/hecks/hecks/deploy.behaviors +1 -1
  178. data/lib/hecks/hecks/deploy.world +1 -3
  179. data/lib/hecks/hecks/hecks.behaviors +1 -1
  180. data/lib/hecks/hecks/hecks.hecksagon +53 -18
  181. data/lib/hecks/hecks/hecks.world +13 -10
  182. data/lib/hecks/hecks/rehearsal.bluebook +477 -0
  183. data/lib/hecks/hecks/site.behaviors +1 -1
  184. data/lib/hecks/hecks/site.world +0 -1
  185. data/lib/hecks/hecks/tooling.bluebook +1238 -0
  186. data/lib/hecks/indifferent_key.rb +20 -0
  187. data/lib/hecks/language/adapter.bluebook +13 -3
  188. data/lib/hecks/language/bluebook/aggregate.bluebook +23 -0
  189. data/lib/hecks/language/bluebook/bluebook.behaviors +23 -0
  190. data/lib/hecks/language/bluebook/bluebook.bluebook +15 -0
  191. data/lib/hecks/language/bluebook/entity.bluebook +18 -0
  192. data/lib/hecks/language/bluebook/policy.behaviors +19 -0
  193. data/lib/hecks/language/bluebook/policy.bluebook +9 -1
  194. data/lib/hecks/language/bluebook/query.bluebook +1 -1
  195. data/lib/hecks/language/bluebook/vocabulary.bluebook +63 -4
  196. data/lib/hecks/language/hecksagon/domain_port.bluebook +1 -1
  197. data/lib/hecks/language/hecksagon/hecksagon.bluebook +22 -0
  198. data/lib/hecks/language/oidc.json +10 -0
  199. data/lib/hecks/language/port.bluebook +4 -4
  200. data/lib/hecks/literal.rb +12 -3
  201. data/lib/hecks/ports/key_vault.rb +7 -2
  202. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +21 -10
  203. data/lib/hecks/ports/persistence/plugins/era/postgres_era/boot.rb +4 -2
  204. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +2 -1
  205. data/lib/hecks/ports/persistence/repository_factory.rb +6 -3
  206. data/lib/hecks/projection_files/expression_tables.rb +7 -2
  207. data/lib/hecks/projections/deploy/box/compose.rb +6 -4
  208. data/lib/hecks/projections/deploy/box/edge.rb +62 -0
  209. data/lib/hecks/projections/deploy/box/logging.rb +99 -0
  210. data/lib/hecks/projections/deploy/box/roll_scripts.rb +16 -8
  211. data/lib/hecks/projections/deploy/box/settings/cdn.rb +40 -0
  212. data/lib/hecks/projections/deploy/box/settings/logs.rb +40 -0
  213. data/lib/hecks/projections/deploy/box/settings/patterns.rb +11 -0
  214. data/lib/hecks/projections/deploy/box/settings/types.rb +16 -1
  215. data/lib/hecks/projections/deploy/box/settings.rb +6 -2
  216. data/lib/hecks/projections/deploy/box/stack_yaml.rb +5 -2
  217. data/lib/hecks/projections/deploy/box/templates/Makefile.tmpl +16 -0
  218. data/lib/hecks/projections/deploy/box/templates/box.yaml.tmpl +8 -4
  219. data/lib/hecks/projections/deploy/box/templates/deploy-box.sh.tmpl +32 -0
  220. data/lib/hecks/projections/deploy/box/templates/deploy-service.sh.tmpl +1 -1
  221. data/lib/hecks/projections/deploy/box/templates/edge.mk.tmpl +17 -0
  222. data/lib/hecks/projections/deploy/box/templates/edge.yaml.tmpl +65 -0
  223. data/lib/hecks/projections/deploy/box/templates/log-group.yaml.tmpl +12 -0
  224. data/lib/hecks/projections/deploy/box/templates/log-policy.yaml.tmpl +7 -0
  225. data/lib/hecks/projections/deploy/box/templates/render-compose-taskdef.sh.tmpl +5 -4
  226. data/lib/hecks/projections/deploy/box/templates/render-compose.sh.tmpl +5 -4
  227. data/lib/hecks/projections/deploy/box.rb +15 -3
  228. data/lib/hecks/projections/deploy/preview/preview.sh.erb +1 -1
  229. data/lib/hecks/projections/deploy/shared.rb +5 -2
  230. data/lib/hecks/projections/model/deviations.rb +2 -2
  231. data/lib/hecks/projections/model.rb +2 -2
  232. data/lib/hecks/projections/oidc.rb +13 -1
  233. data/lib/hecks/projections/shape.rb +5 -2
  234. data/lib/hecks/projections/site/cms_editor/attributes.rb +26 -6
  235. data/lib/hecks/projections/site/cms_editor/blocks.rb +157 -0
  236. data/lib/hecks/projections/site/cms_editor/checks.rb +14 -1
  237. data/lib/hecks/projections/site/cms_editor/clearing.rb +70 -0
  238. data/lib/hecks/projections/site/cms_editor/color.rb +124 -0
  239. data/lib/hecks/projections/site/cms_editor/composition.rb +42 -0
  240. data/lib/hecks/projections/site/cms_editor/destructive.rb +61 -0
  241. data/lib/hecks/projections/site/cms_editor/drafts.rb +102 -0
  242. data/lib/hecks/projections/site/cms_editor/hints.rb +74 -0
  243. data/lib/hecks/projections/site/cms_editor/icons.rb +86 -0
  244. data/lib/hecks/projections/site/cms_editor/key_kinds.rb +31 -0
  245. data/lib/hecks/projections/site/cms_editor/limits.rb +72 -0
  246. data/lib/hecks/projections/site/cms_editor/media.rb +1 -1
  247. data/lib/hecks/projections/site/cms_editor/moment.rb +34 -0
  248. data/lib/hecks/projections/site/cms_editor/palette.rb +115 -0
  249. data/lib/hecks/projections/site/cms_editor/pickers.rb +85 -0
  250. data/lib/hecks/projections/site/cms_editor/preview.rb +91 -0
  251. data/lib/hecks/projections/site/cms_editor/relations.rb +92 -0
  252. data/lib/hecks/projections/site/cms_editor/scale_checks.rb +66 -0
  253. data/lib/hecks/projections/site/cms_editor/scheduling.rb +150 -0
  254. data/lib/hecks/projections/site/cms_editor/schema.rb +96 -19
  255. data/lib/hecks/projections/site/cms_editor/setting.rb +52 -9
  256. data/lib/hecks/projections/site/cms_editor/targets.rb +86 -0
  257. data/lib/hecks/projections/site/cms_editor/templates/package.json.tmpl +18 -1
  258. data/lib/hecks/projections/site/cms_editor/templates/src/app.ts.tmpl +251 -57
  259. data/lib/hecks/projections/site/cms_editor/templates/src/assets.ts.tmpl +98 -0
  260. data/lib/hecks/projections/site/cms_editor/templates/src/auth/membership.ts.tmpl +32 -24
  261. data/lib/hecks/projections/site/cms_editor/templates/src/browser/app.css.tmpl +412 -0
  262. data/lib/hecks/projections/site/cms_editor/templates/src/browser/autosave.js.tmpl +144 -0
  263. data/lib/hecks/projections/site/cms_editor/templates/src/browser/blocks.js.tmpl +500 -0
  264. data/lib/hecks/projections/site/cms_editor/templates/src/browser/body_doc.js.tmpl +175 -0
  265. data/lib/hecks/projections/site/cms_editor/templates/src/browser/body_editor.js.tmpl +418 -0
  266. data/lib/hecks/projections/site/cms_editor/templates/src/browser/epoch.js.tmpl +61 -0
  267. data/lib/hecks/projections/site/cms_editor/templates/src/browser/forms.js.tmpl +210 -0
  268. data/lib/hecks/projections/site/cms_editor/templates/src/browser/lists.js.tmpl +40 -0
  269. data/lib/hecks/projections/site/cms_editor/templates/src/browser/main.js.tmpl +26 -0
  270. data/lib/hecks/projections/site/cms_editor/templates/src/browser/media_picker.js.tmpl +232 -0
  271. data/lib/hecks/projections/site/cms_editor/templates/src/browser/moments.js.tmpl +73 -0
  272. data/lib/hecks/projections/site/cms_editor/templates/src/browser/pickers.js.tmpl +152 -0
  273. data/lib/hecks/projections/site/cms_editor/templates/src/browser/preview.js.tmpl +98 -0
  274. data/lib/hecks/projections/site/cms_editor/templates/src/browser/shell.js.tmpl +140 -0
  275. data/lib/hecks/projections/site/cms_editor/templates/src/browser/toast.js.tmpl +66 -0
  276. data/lib/hecks/projections/site/cms_editor/templates/src/choices.ts.tmpl +99 -0
  277. data/lib/hecks/projections/site/cms_editor/templates/src/commands.ts.tmpl +78 -24
  278. data/lib/hecks/projections/site/cms_editor/templates/src/config.ts.tmpl +6 -0
  279. data/lib/hecks/projections/site/cms_editor/templates/src/flash.ts.tmpl +52 -0
  280. data/lib/hecks/projections/site/cms_editor/templates/src/host.ts.tmpl +19 -2
  281. data/lib/hecks/projections/site/cms_editor/templates/src/media/handler.ts.tmpl +31 -8
  282. data/lib/hecks/projections/site/cms_editor/templates/src/names.ts.tmpl +36 -0
  283. data/lib/hecks/projections/site/cms_editor/templates/src/schema.ts.tmpl +131 -2
  284. data/lib/hecks/projections/site/cms_editor/templates/src/ui/availability.ts.tmpl +15 -0
  285. data/lib/hecks/projections/site/cms_editor/templates/src/ui/block_list.js.tmpl +213 -0
  286. data/lib/hecks/projections/site/cms_editor/templates/src/ui/blocks.ts.tmpl +275 -0
  287. data/lib/hecks/projections/site/cms_editor/templates/src/ui/body_model.js.tmpl +8 -5
  288. data/lib/hecks/projections/site/cms_editor/templates/src/ui/detail.ts.tmpl +88 -0
  289. data/lib/hecks/projections/site/cms_editor/templates/src/ui/dialogs.ts.tmpl +43 -0
  290. data/lib/hecks/projections/site/cms_editor/templates/src/ui/fields.ts.tmpl +229 -34
  291. data/lib/hecks/projections/site/cms_editor/templates/src/ui/html.ts.tmpl +40 -81
  292. data/lib/hecks/projections/site/cms_editor/templates/src/ui/icons.ts.tmpl +15 -0
  293. data/lib/hecks/projections/site/cms_editor/templates/src/ui/input.ts.tmpl +29 -12
  294. data/lib/hecks/projections/site/cms_editor/templates/src/ui/links.ts.tmpl +100 -0
  295. data/lib/hecks/projections/site/cms_editor/templates/src/ui/moments.ts.tmpl +44 -0
  296. data/lib/hecks/projections/site/cms_editor/templates/src/ui/outline.ts.tmpl +53 -17
  297. data/lib/hecks/projections/site/cms_editor/templates/src/ui/pages.ts.tmpl +178 -88
  298. data/lib/hecks/projections/site/cms_editor/templates/src/ui/paging.ts.tmpl +82 -0
  299. data/lib/hecks/projections/site/cms_editor/templates/src/ui/pickers.ts.tmpl +110 -0
  300. data/lib/hecks/projections/site/cms_editor/templates/src/ui/preview.ts.tmpl +87 -0
  301. data/lib/hecks/projections/site/cms_editor/templates/src/ui/related.ts.tmpl +97 -0
  302. data/lib/hecks/projections/site/cms_editor/templates/src/ui/schedule.ts.tmpl +160 -0
  303. data/lib/hecks/projections/site/cms_editor/templates/src/ui/shell.ts.tmpl +211 -0
  304. data/lib/hecks/projections/site/cms_editor/templates/src/ui/table.ts.tmpl +151 -0
  305. data/lib/hecks/projections/site/cms_editor/templates/src/ui/theme.js.tmpl +16 -0
  306. data/lib/hecks/projections/site/cms_editor/templates/src/ui/values.ts.tmpl +69 -0
  307. data/lib/hecks/projections/site/cms_editor/templates/src/ui/words.ts.tmpl +97 -0
  308. data/lib/hecks/projections/site/cms_editor/templates/src/ui/world.ts.tmpl +35 -0
  309. data/lib/hecks/projections/site/cms_editor/templates/tsconfig.json.tmpl +2 -1
  310. data/lib/hecks/projections/site/cms_editor/theme.rb +29 -0
  311. data/lib/hecks/projections/site/cms_editor/theme_checks.rb +57 -0
  312. data/lib/hecks/projections/site/cms_editor.rb +49 -23
  313. data/lib/hecks/projections/site/role_probe.rb +2 -0
  314. data/lib/hecks/projections/site/site_root/workflow.rb +5 -0
  315. data/lib/hecks/projector/cli_audience.rb +22 -1
  316. data/lib/hecks/projector/cli_projector/listing.rb +1 -1
  317. data/lib/hecks/projector/cli_projector/option_specs.rb +1 -1
  318. data/lib/hecks/projector/cli_projector/usage.rb +15 -4
  319. data/lib/hecks/projector/docs_projector/closing.rb +1 -1
  320. data/lib/hecks/projector/docs_projector.rb +1 -1
  321. data/lib/hecks/projector/exporter/bindings.rb +40 -10
  322. data/lib/hecks/projector/exporter/capability_values.rb +55 -0
  323. data/lib/hecks/projector/narrate_projector/reactions.rb +1 -1
  324. data/lib/hecks/projector/target.rb +11 -0
  325. data/lib/hecks/projector.rb +32 -5
  326. data/lib/hecks/quality_control/adapters/github_checks.rb +12 -8
  327. data/lib/hecks/quality_control/adapters/github_issues.rb +8 -4
  328. data/lib/hecks/quality_control/cli/qa_open_pr.rb +2 -2
  329. data/lib/hecks/quality_control/cli/qa_sweep/checks/shrinking.rb +1 -1
  330. data/lib/hecks/quality_control/cli/qa_sweep/finding_report.rb +1 -1
  331. data/lib/hecks/quality_control/cli/qa_sweep.rb +1 -1
  332. data/lib/hecks/quality_control/quality_control.bluebook +6 -2
  333. data/lib/hecks/release/approval.rb +64 -0
  334. data/lib/hecks/router/{door.rb → aggregate_reader.rb} +5 -4
  335. data/lib/hecks/router/namespace_installer.rb +16 -16
  336. data/lib/hecks/router.rb +1 -1
  337. data/lib/hecks/runtime/best_effort.rb +22 -0
  338. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +12 -2
  339. data/lib/hecks/runtime/command_interpreter.rb +3 -2
  340. data/lib/hecks/runtime/command_rules/emission.rb +1 -1
  341. data/lib/hecks/runtime/dispatcher.rb +4 -4
  342. data/lib/hecks/runtime/entity_element/location.rb +1 -1
  343. data/lib/hecks/runtime/event.rb +5 -0
  344. data/lib/hecks/runtime/identity.rb +7 -6
  345. data/lib/hecks/runtime/interpreting.rb +2 -1
  346. data/lib/hecks/runtime/invocation_builder.rb +1 -1
  347. data/lib/hecks/runtime/loader/boot_steps.rb +8 -5
  348. data/lib/hecks/runtime/loader.rb +10 -10
  349. data/lib/hecks/runtime/policy_interpreter/arguments.rb +7 -4
  350. data/lib/hecks/runtime/policy_interpreter/fan_out.rb +3 -3
  351. data/lib/hecks/runtime/policy_interpreter.rb +6 -6
  352. data/lib/hecks/runtime/port_operation_interpreter.rb +1 -1
  353. data/lib/hecks/runtime/reaction_invocation.rb +1 -1
  354. data/lib/hecks/runtime/rebuild_sweep.rb +11 -1
  355. data/lib/hecks/runtime/registry/ask_verification.rb +35 -0
  356. data/lib/hecks/runtime/registry/hecksagon_rules.rb +10 -0
  357. data/lib/hecks/runtime/registry/merging.rb +1 -1
  358. data/lib/hecks/runtime/registry/providers.rb +16 -0
  359. data/lib/hecks/runtime/registry/read_repository.rb +6 -4
  360. data/lib/hecks/runtime/registry/verification.rb +4 -1
  361. data/lib/hecks/runtime/registry/world_defaults.rb +29 -0
  362. data/lib/hecks/runtime/registry.rb +1 -1
  363. data/lib/hecks/runtime/saga_interpreter/compensation.rb +1 -1
  364. data/lib/hecks/runtime/saga_interpreter/delivery.rb +4 -4
  365. data/lib/hecks/runtime/saga_interpreter/transitions.rb +1 -1
  366. data/lib/hecks/runtime/saga_interpreter.rb +3 -3
  367. data/lib/hecks/runtime/tenant_check.rb +5 -4
  368. data/lib/hecks/runtime/value/coercion.rb +2 -2
  369. data/lib/hecks/runtime/value/validation.rb +16 -4
  370. data/lib/hecks/runtime.rb +9 -9
  371. data/lib/hecks/rust_build/host.rb +11 -4
  372. data/lib/hecks/rust_build/prebuilt_binary.rb +101 -0
  373. data/lib/hecks/rust_build/project_rust/target_ir.rb +1 -1
  374. data/lib/hecks/rust_build/wasm.rb +1 -1
  375. data/lib/hecks/rust_build/wasm_browser.rb +1 -1
  376. data/lib/hecks/rust_build.rb +11 -0
  377. data/lib/hecks/served_options.rb +53 -0
  378. data/lib/hecks/site/bluebook/site.bluebook +1 -0
  379. data/lib/hecks/storehouse/audit.rb +1 -1
  380. data/lib/hecks/storehouse/dispatching.rb +8 -6
  381. data/lib/hecks/storehouse/introspection.rb +2 -2
  382. data/lib/hecks/storehouse/querying.rb +6 -4
  383. data/lib/hecks/storehouse.rb +4 -4
  384. data/lib/hecks/tools/ci_gate_decision.rb +35 -0
  385. data/lib/hecks/tools/ci_gates.rb +20 -1
  386. data/lib/hecks/tools/comment_style/source_file.rb +4 -1
  387. data/lib/hecks/tools/deploy_recipe_lint/fixtures.rb +4 -1
  388. data/lib/hecks/tools/lanes/files.rb +8 -0
  389. data/lib/hecks/tools/lanes/prebuilt_workflow.rb +81 -0
  390. data/lib/hecks/tools/lanes/rows.rb +6 -0
  391. data/lib/hecks/tools/lanes.rb +1 -0
  392. data/lib/hecks/tools/oidc_manifests.rb +4 -5
  393. data/lib/hecks/tools/regeneration_run.rb +11 -4
  394. data/lib/hecks/tools/release_only.rb +111 -0
  395. data/lib/hecks/tools/site_routes/editor_files.rb +12 -5
  396. data/lib/hecks/tools/tenant_provisioning.rb +2 -2
  397. data/lib/hecks/tools/tools_doc.rb +4 -4
  398. data/lib/hecks/version.rb +1 -1
  399. data/lib/hecks/vocabulary.rb +19 -5
  400. data/lib/hecks.rb +15 -15
  401. data/lib/rubocop/cop/hecks/env_dump.rb +69 -0
  402. data/lib/rubocop/cop/hecks/monotonic_duration.rb +53 -0
  403. data/lib/rubocop/cop/hecks/no_wall_clock_in_runtime.rb +34 -0
  404. data/lib/rubocop/cop/hecks/raw_shell_interpolation.rb +79 -0
  405. data/lib/rubocop/cop/hecks/sleep_in_spec.rb +34 -0
  406. data/lib/rubocop/cop/hecks/swallowed_rescue.rb +37 -0
  407. data/lib/rubocop/cop/hecks/symbol_string_key_mix.rb +50 -0
  408. data/lib/rubocop/cop/hecks/untyped_raise.rb +37 -0
  409. data/prebuilt/9ee54e2f0d4d6d3fe046827779b221e02de665fba911a048ae5dd8b8fbfb7d90.marshal +0 -0
  410. data/prebuilt/d74a00e996222c1d8b676d5959092effc46b4dc59e0555f3bf9554ac58e3094a.marshal +0 -0
  411. data/prebuilt/verdicts-df19f35868d3284a70f55e42dcef87ef4e65e6a678e5135f72e1f6cff4c1357b.json +1 -0
  412. data/rust/.gitignore +6 -0
  413. data/rust/Cargo.toml +6 -0
  414. data/rust/build/src/binding_pass.rs +103 -0
  415. data/rust/build/src/cargo_sync.rs +6 -0
  416. data/rust/build/src/lineage_pass.rs +3 -3
  417. data/rust/build/src/main.rs +17 -5
  418. data/rust/build/src/pipeline.rs +48 -5
  419. data/rust/build/src/seams_pass.rs +123 -0
  420. data/rust/build/src/translation_pass.rs +167 -0
  421. data/rust/codegen/src/asks.rs +103 -0
  422. data/rust/codegen/src/commands.rs +13 -8
  423. data/rust/codegen/src/constraints.rs +25 -0
  424. data/rust/codegen/src/domain_generator.rs +2 -0
  425. data/rust/codegen/src/expr_emitter.rs +13 -0
  426. data/rust/codegen/src/json_codec.rs +54 -15
  427. data/rust/codegen/src/main.rs +1 -0
  428. data/rust/codegen/src/mutations.rs +128 -3
  429. data/rust/codegen/src/queries.rs +11 -2
  430. data/rust/codegen/src/reactions.rs +1 -1
  431. data/rust/codegen/src/types.rs +166 -8
  432. data/rust/host/HECKS_RELEASE +1 -1
  433. data/rust/host/src/api.rs +4 -4
  434. data/rust/host/src/bin/validate_harness.rs +43 -0
  435. data/rust/host/src/boundary_fuzz/expr_json.rs +2 -2
  436. data/rust/host/src/boundary_fuzz/ir.rs +1 -0
  437. data/rust/host/src/boundary_fuzz/server.rs +1 -1
  438. data/rust/host/src/checkout.rs +35 -19
  439. data/rust/host/src/commerce_ir.rs +408 -2
  440. data/rust/host/src/expr_json.rs +118 -0
  441. data/rust/host/src/extension.rs +13 -1
  442. data/rust/host/src/lib.rs +1 -0
  443. data/rust/host/src/mint.rs +1 -1
  444. data/rust/host/src/payments/tests.rs +27 -1
  445. data/rust/host/src/payments.rs +12 -4
  446. data/rust/host/src/platform_signin/attack_tests.rs +1100 -0
  447. data/rust/host/src/platform_signin/config.rs +512 -0
  448. data/rust/host/src/platform_signin/cookie.rs +237 -0
  449. data/rust/host/src/platform_signin/extension.rs +190 -0
  450. data/rust/host/src/platform_signin/fetcher.rs +322 -0
  451. data/rust/host/src/platform_signin/handler.rs +251 -0
  452. data/rust/host/src/platform_signin/handler_tests.rs +554 -0
  453. data/rust/host/src/platform_signin/jti.rs +196 -0
  454. data/rust/host/src/platform_signin/jwks.rs +542 -0
  455. data/rust/host/src/platform_signin/jws.rs +249 -0
  456. data/rust/host/src/platform_signin/known_answer_tests.rs +237 -0
  457. data/rust/host/src/platform_signin/mapping_tests.rs +254 -0
  458. data/rust/host/src/platform_signin/mod.rs +75 -0
  459. data/rust/host/src/platform_signin/mode_tests.rs +382 -0
  460. data/rust/host/src/platform_signin/pages.rs +204 -0
  461. data/rust/host/src/platform_signin/query.rs +233 -0
  462. data/rust/host/src/platform_signin/session.rs +151 -0
  463. data/rust/host/src/platform_signin/settings.rs +385 -0
  464. data/rust/host/src/platform_signin/strict_json.rs +351 -0
  465. data/rust/host/src/platform_signin/test_support.rs +235 -0
  466. data/rust/host/src/platform_signin/ttl_tests.rs +94 -0
  467. data/rust/host/src/platform_signin/verify.rs +604 -0
  468. data/rust/host/src/platform_signin/wiring_support.rs +319 -0
  469. data/rust/host/src/query_step.rs +22 -4
  470. data/rust/host/src/reference_validate.rs +653 -108
  471. data/rust/host/src/resend.rs +2 -0
  472. data/rust/host/src/server.rs +19 -6
  473. data/rust/host/src/ui_schema.rs +132 -5
  474. data/rust/host/src/web/newsletter.rs +7 -8
  475. data/rust/host/src/web/newsletter_send.rs +38 -13
  476. data/rust/host/src/web/registrations.rs +229 -26
  477. data/rust/host/src/web.rs +53 -9
  478. data/rust/parser/src/emit.rs +38 -6
  479. data/rust/parser/src/expr/ast_json.rs +7 -1
  480. data/rust/parser/src/expr/resolver.rs +171 -6
  481. data/rust/parser/src/ir.rs +7 -0
  482. data/rust/parser/src/keywords.rs +24 -0
  483. data/rust/parser/src/main.rs +3 -0
  484. data/rust/parser/src/parse/aggregate.rs +2 -1
  485. data/rust/parser/src/parse/chapter.rs +2 -2
  486. data/rust/parser/src/parse/command.rs +2 -0
  487. data/rust/parser/src/parse/hecksagon.rs +5 -2
  488. data/rust/parser/src/parse/lifecycle.rs +81 -0
  489. data/rust/parser/src/parse/mod.rs +15 -1
  490. data/rust/parser/src/parse/policy.rs +13 -2
  491. data/rust/parser/src/parse/value_object.rs +34 -17
  492. data/rust/parser/src/ruby_value.rs +1 -1
  493. data/rust/src/exemplar/commands.rs +4 -4
  494. data/rust/src/exemplar/constraints.rs +1 -1
  495. data/rust/src/exemplar/json.rs +43 -1
  496. data/rust/src/kernel/cli.rs +7 -5
  497. data/rust/src/kernel/dispatch.rs +2 -2
  498. data/rust/src/kernel/dispatch_guard_tests.rs +1 -1
  499. data/rust/src/kernel/expr.rs +66 -3
  500. data/rust/src/kernel/expression_operators/arithmetic.rs +130 -5
  501. data/rust/src/kernel/expression_operators/text.rs +26 -2
  502. data/rust/src/kernel/json.rs +15 -0
  503. data/rust/src/kernel/mod.rs +1 -1
  504. data/rust/src/kernel/orchestrate.rs +54 -5
  505. data/rust/src/kernel/orchestrate_guard_tests.rs +67 -0
  506. data/rust/src/kernel/pattern.rs +60 -0
  507. data/rust/src/kernel/reference_lookup.rs +90 -7
  508. metadata +178 -38
  509. data/lib/hecks/doors/cli_door/nesting.rb +0 -34
  510. data/lib/hecks/doors/cli_door/shorthand.rb +0 -85
  511. data/lib/hecks/doors/cli_door.rb +0 -124
  512. data/lib/hecks/doors/cli_runner/answers.rb +0 -138
  513. data/lib/hecks/doors/cli_runner/resolution.rb +0 -146
  514. data/lib/hecks/doors/cli_runner/settling.rb +0 -115
  515. data/lib/hecks/doors/cli_runner/streaming.rb +0 -90
  516. data/lib/hecks/doors/cli_runner/suggestions.rb +0 -43
  517. data/lib/hecks/doors/cli_runner.rb +0 -66
  518. data/lib/hecks/doors/command_request.rb +0 -119
  519. data/lib/hecks/doors/handle.rb +0 -208
  520. data/lib/hecks/doors/json_door.rb +0 -132
  521. data/lib/hecks/doors/launcher_options.rb +0 -247
  522. data/lib/hecks/doors/mcp_door/properties.rb +0 -47
  523. data/lib/hecks/doors/mcp_door/tool_calls.rb +0 -113
  524. data/lib/hecks/doors/mcp_door/tool_definitions/auditing.rb +0 -51
  525. data/lib/hecks/doors/mcp_door/tool_definitions/calling.rb +0 -69
  526. data/lib/hecks/doors/mcp_door/tool_definitions/reading.rb +0 -97
  527. data/lib/hecks/doors/mcp_door/tool_definitions.rb +0 -15
  528. data/lib/hecks/doors/mcp_door.rb +0 -156
  529. data/lib/hecks/doors/mcp_door_scope/argument_policy.rb +0 -92
  530. data/lib/hecks/doors/mcp_door_scope/env_problems.rb +0 -55
  531. data/lib/hecks/doors/mcp_door_scope/presenter.rb +0 -78
  532. data/lib/hecks/doors/mcp_door_scope.rb +0 -201
  533. data/lib/hecks/doors/mcp_guide_cache.rb +0 -75
  534. data/lib/hecks/doors/ruby_door/aggregate_door.rb +0 -162
  535. data/lib/hecks/doors/ruby_door/chapter.rb +0 -89
  536. data/lib/hecks/doors/ruby_door.rb +0 -80
  537. data/lib/hecks/doors/usage_cache.rb +0 -139
  538. data/lib/hecks/projections/site/cms_editor/templates/src/ui/body_widget.js.tmpl +0 -282
  539. data/lib/hecks/projections/site/cms_editor/templates/src/ui/media_picker.js.tmpl +0 -109
  540. data/prebuilt/c28d6d9db14eea3e17161e9d9c262efee30b256cf97693a7cfd552bce24b2f7f.marshal +0 -0
  541. data/prebuilt/cb375cba61ca0dbe8ddb9739c306510fe05e5f4b7a9b6955caff8224c63ea0e0.marshal +0 -0
  542. data/prebuilt/verdicts-d6d584ad43b5f3cebf88aafbbdbf024c38574acf18e06adffbc555f7786b1c2f.json +0 -1
@@ -0,0 +1,351 @@
1
+ //! A JSON reader that accepts only what two parsers could not disagree about.
2
+ //!
3
+ //! The standard parsers keep the last of a repeated key and accept deep nesting. A token whose
4
+ //! header or claims repeat a key could be read one way here and another way by the issuer, so this
5
+ //! reader refuses it. It also keeps numbers apart from each other (an integer is not a float)
6
+ //! because
7
+ //! the time claims must be integers.
8
+
9
+ use std::collections::HashMap;
10
+
11
+ /// A parsed JSON value, reduced to the shapes a token check needs to tell apart.
12
+ #[derive(Debug, Clone, PartialEq)]
13
+ pub enum Json {
14
+ Null,
15
+ Bool(bool),
16
+ /// An integer; a digit string beyond the `i128` range saturates, which keeps every comparison
17
+ /// against a time window the same as an unbounded integer would give.
18
+ Int(i128),
19
+ /// Any number that is not an integer literal (a fraction or an exponent).
20
+ Number,
21
+ Str(String),
22
+ Array(Vec<Json>),
23
+ Object(JsonObject),
24
+ }
25
+
26
+ /// A JSON object with unique keys.
27
+ #[derive(Debug, Clone, PartialEq, Default)]
28
+ pub struct JsonObject(HashMap<String, Json>);
29
+
30
+ impl JsonObject {
31
+ /// Finds a member by name.
32
+ pub fn get(&self, name: &str) -> Option<&Json> {
33
+ self.0.get(name)
34
+ }
35
+
36
+ /// Finds a member that is a string.
37
+ pub fn get_str(&self, name: &str) -> Option<&str> {
38
+ match self.get(name) {
39
+ Some(Json::Str(text)) => Some(text),
40
+ _ => None,
41
+ }
42
+ }
43
+
44
+ /// Tells whether the member exists and is not JSON `null`.
45
+ pub fn is_present(&self, name: &str) -> bool {
46
+ !matches!(self.get(name), None | Some(Json::Null))
47
+ }
48
+
49
+ /// Lists the member names, in no particular order.
50
+ pub fn keys(&self) -> impl Iterator<Item = &String> {
51
+ self.0.keys()
52
+ }
53
+ }
54
+
55
+ /// The deepest nesting accepted, counting the outer object as depth 1.
56
+ pub const MAX_DEPTH: usize = 8;
57
+
58
+ /// Parses `bytes` as one JSON object and nothing else.
59
+ ///
60
+ /// Returns `None` for invalid UTF-8, invalid JSON, trailing text, a value that is not an object,
61
+ /// an object that repeats a key at any depth, and nesting deeper than [`MAX_DEPTH`].
62
+ pub fn parse_object(bytes: &[u8]) -> Option<JsonObject> {
63
+ let text = std::str::from_utf8(bytes).ok()?;
64
+ let mut parser = Parser {
65
+ bytes: text.as_bytes(),
66
+ at: 0,
67
+ };
68
+ parser.skip_whitespace();
69
+ let object = parser.object(0)?;
70
+ parser.skip_whitespace();
71
+ (parser.at == parser.bytes.len()).then_some(object)
72
+ }
73
+
74
+ /// A recursive-descent reader over validated UTF-8; every method returns `None` at the first fault.
75
+ struct Parser<'a> {
76
+ bytes: &'a [u8],
77
+ at: usize,
78
+ }
79
+
80
+ impl Parser<'_> {
81
+ fn peek(&self) -> Option<u8> {
82
+ self.bytes.get(self.at).copied()
83
+ }
84
+
85
+ fn skip_whitespace(&mut self) {
86
+ while matches!(self.peek(), Some(b' ' | b'\t' | b'\n' | b'\r')) {
87
+ self.at += 1;
88
+ }
89
+ }
90
+
91
+ fn expect(&mut self, byte: u8) -> Option<()> {
92
+ (self.peek() == Some(byte)).then(|| self.at += 1)
93
+ }
94
+
95
+ fn value(&mut self, depth: usize) -> Option<Json> {
96
+ match self.peek()? {
97
+ b'{' => self.object(depth).map(Json::Object),
98
+ b'[' => self.array(depth),
99
+ b'"' => self.string().map(Json::Str),
100
+ b't' => self.literal(b"true", Json::Bool(true)),
101
+ b'f' => self.literal(b"false", Json::Bool(false)),
102
+ b'n' => self.literal(b"null", Json::Null),
103
+ b'-' | b'0'..=b'9' => self.number(),
104
+ _ => None,
105
+ }
106
+ }
107
+
108
+ fn literal(&mut self, word: &[u8], value: Json) -> Option<Json> {
109
+ let end = self.at + word.len();
110
+ (self.bytes.get(self.at..end)? == word).then(|| {
111
+ self.at = end;
112
+ value
113
+ })
114
+ }
115
+
116
+ fn object(&mut self, depth: usize) -> Option<JsonObject> {
117
+ let depth = depth + 1;
118
+ if depth > MAX_DEPTH {
119
+ return None;
120
+ }
121
+ self.expect(b'{')?;
122
+ let mut members = HashMap::new();
123
+ self.skip_whitespace();
124
+ if self.peek() == Some(b'}') {
125
+ self.at += 1;
126
+ return Some(JsonObject(members));
127
+ }
128
+ loop {
129
+ self.skip_whitespace();
130
+ let key = self.string()?;
131
+ self.skip_whitespace();
132
+ self.expect(b':')?;
133
+ self.skip_whitespace();
134
+ let value = self.value(depth)?;
135
+ if members.insert(key, value).is_some() {
136
+ return None;
137
+ }
138
+ self.skip_whitespace();
139
+ match self.peek()? {
140
+ b',' => self.at += 1,
141
+ b'}' => {
142
+ self.at += 1;
143
+ return Some(JsonObject(members));
144
+ }
145
+ _ => return None,
146
+ }
147
+ }
148
+ }
149
+
150
+ fn array(&mut self, depth: usize) -> Option<Json> {
151
+ let depth = depth + 1;
152
+ if depth > MAX_DEPTH {
153
+ return None;
154
+ }
155
+ self.expect(b'[')?;
156
+ let mut items = Vec::new();
157
+ self.skip_whitespace();
158
+ if self.peek() == Some(b']') {
159
+ self.at += 1;
160
+ return Some(Json::Array(items));
161
+ }
162
+ loop {
163
+ self.skip_whitespace();
164
+ items.push(self.value(depth)?);
165
+ self.skip_whitespace();
166
+ match self.peek()? {
167
+ b',' => self.at += 1,
168
+ b']' => {
169
+ self.at += 1;
170
+ return Some(Json::Array(items));
171
+ }
172
+ _ => return None,
173
+ }
174
+ }
175
+ }
176
+
177
+ fn string(&mut self) -> Option<String> {
178
+ self.expect(b'"')?;
179
+ let mut out: Vec<u8> = Vec::new();
180
+ loop {
181
+ let byte = self.peek()?;
182
+ self.at += 1;
183
+ match byte {
184
+ b'"' => return String::from_utf8(out).ok(),
185
+ b'\\' => self.escape(&mut out)?,
186
+ 0x00..=0x1f => return None,
187
+ other => out.push(other),
188
+ }
189
+ }
190
+ }
191
+
192
+ fn escape(&mut self, out: &mut Vec<u8>) -> Option<()> {
193
+ let byte = self.peek()?;
194
+ self.at += 1;
195
+ let simple = match byte {
196
+ b'"' => b'"',
197
+ b'\\' => b'\\',
198
+ b'/' => b'/',
199
+ b'b' => 0x08,
200
+ b'f' => 0x0c,
201
+ b'n' => b'\n',
202
+ b'r' => b'\r',
203
+ b't' => b'\t',
204
+ b'u' => return self.unicode_escape(out),
205
+ _ => return None,
206
+ };
207
+ out.push(simple);
208
+ Some(())
209
+ }
210
+
211
+ fn unicode_escape(&mut self, out: &mut Vec<u8>) -> Option<()> {
212
+ let first = self.hex4()?;
213
+ let code = match first {
214
+ 0xD800..=0xDBFF => {
215
+ self.expect(b'\\')?;
216
+ self.expect(b'u')?;
217
+ let second = self.hex4()?;
218
+ if !(0xDC00..=0xDFFF).contains(&second) {
219
+ return None;
220
+ }
221
+ 0x10000 + ((first - 0xD800) << 10) + (second - 0xDC00)
222
+ }
223
+ 0xDC00..=0xDFFF => return None,
224
+ plain => plain,
225
+ };
226
+ let mut buffer = [0u8; 4];
227
+ out.extend_from_slice(char::from_u32(code)?.encode_utf8(&mut buffer).as_bytes());
228
+ Some(())
229
+ }
230
+
231
+ fn hex4(&mut self) -> Option<u32> {
232
+ let digits = self.bytes.get(self.at..self.at + 4)?;
233
+ let text = std::str::from_utf8(digits).ok()?;
234
+ if !text.bytes().all(|b| b.is_ascii_hexdigit()) {
235
+ return None;
236
+ }
237
+ self.at += 4;
238
+ u32::from_str_radix(text, 16).ok()
239
+ }
240
+
241
+ fn digits(&mut self) -> Option<usize> {
242
+ let start = self.at;
243
+ while matches!(self.peek(), Some(b'0'..=b'9')) {
244
+ self.at += 1;
245
+ }
246
+ (self.at > start).then_some(self.at - start)
247
+ }
248
+
249
+ fn number(&mut self) -> Option<Json> {
250
+ let negative = self.peek() == Some(b'-');
251
+ if negative {
252
+ self.at += 1;
253
+ }
254
+ let integer_start = self.at;
255
+ if self.peek() == Some(b'0') {
256
+ self.at += 1;
257
+ } else {
258
+ self.digits()?;
259
+ }
260
+ let integer_end = self.at;
261
+ let mut integral = true;
262
+ if self.peek() == Some(b'.') {
263
+ self.at += 1;
264
+ self.digits()?;
265
+ integral = false;
266
+ }
267
+ if matches!(self.peek(), Some(b'e' | b'E')) {
268
+ self.at += 1;
269
+ if matches!(self.peek(), Some(b'+' | b'-')) {
270
+ self.at += 1;
271
+ }
272
+ self.digits()?;
273
+ integral = false;
274
+ }
275
+ if !integral {
276
+ return Some(Json::Number);
277
+ }
278
+ let text = std::str::from_utf8(&self.bytes[integer_start..integer_end]).ok()?;
279
+ Some(Json::Int(integer_from(text, negative)))
280
+ }
281
+ }
282
+
283
+ /// Reads an integer literal's digits, saturating at the `i128` limits instead of failing.
284
+ fn integer_from(digits: &str, negative: bool) -> i128 {
285
+ match digits.parse::<i128>() {
286
+ Ok(value) if negative => -value,
287
+ Ok(value) => value,
288
+ Err(_) if negative => i128::MIN,
289
+ Err(_) => i128::MAX,
290
+ }
291
+ }
292
+
293
+ #[cfg(test)]
294
+ mod tests {
295
+ use super::*;
296
+
297
+ fn parse(text: &str) -> Option<JsonObject> {
298
+ parse_object(text.as_bytes())
299
+ }
300
+
301
+ #[test]
302
+ fn an_object_parses_and_its_members_are_readable() {
303
+ let object = parse(r#"{"a":"x","b":7,"c":true,"d":null}"#).expect("object");
304
+ assert_eq!(object.get_str("a"), Some("x"));
305
+ assert_eq!(object.get("b"), Some(&Json::Int(7)));
306
+ assert_eq!(object.get("c"), Some(&Json::Bool(true)));
307
+ assert!(!object.is_present("d"));
308
+ assert!(!object.is_present("missing"));
309
+ }
310
+
311
+ #[test]
312
+ fn a_repeated_key_is_refused_at_the_top_and_when_nested() {
313
+ assert!(parse(r#"{"a":1,"a":2}"#).is_none());
314
+ assert!(parse(r#"{"a":{"b":1,"b":1}}"#).is_none());
315
+ assert!(parse(r#"{"a":[{"b":1,"b":1}]}"#).is_none());
316
+ }
317
+
318
+ #[test]
319
+ fn only_an_object_is_accepted() {
320
+ for text in [
321
+ "[]", "\"s\"", "1", "true", "null", "", " ", "{", "{}x", "{} {}",
322
+ ] {
323
+ assert!(parse(text).is_none(), "{text:?} must be refused");
324
+ }
325
+ assert!(parse("{}").is_some());
326
+ assert!(parse(" {} ").is_some());
327
+ }
328
+
329
+ #[test]
330
+ fn invalid_utf8_is_refused() {
331
+ assert!(parse_object(b"{\"a\":\"\xff\"}").is_none());
332
+ }
333
+
334
+ #[test]
335
+ fn integers_and_other_numbers_are_told_apart() {
336
+ let object = parse(r#"{"i":-5,"f":1.5,"e":1e2,"z":1.0,"big":99999999999999999999999999999999999999999999}"#).expect("object");
337
+ assert_eq!(object.get("i"), Some(&Json::Int(-5)));
338
+ assert_eq!(object.get("f"), Some(&Json::Number));
339
+ assert_eq!(object.get("e"), Some(&Json::Number));
340
+ assert_eq!(object.get("z"), Some(&Json::Number));
341
+ assert_eq!(object.get("big"), Some(&Json::Int(i128::MAX)));
342
+ }
343
+
344
+ #[test]
345
+ fn nesting_is_capped_at_eight_levels() {
346
+ let ok = format!(r#"{{"a":{}{}}}"#, "[".repeat(7), "]".repeat(7));
347
+ assert!(parse(&ok).is_some(), "depth 8 is allowed");
348
+ let deep = format!(r#"{{"a":{}{}}}"#, "[".repeat(8), "]".repeat(8));
349
+ assert!(parse(&deep).is_none(), "depth 9 is refused");
350
+ }
351
+ }
@@ -0,0 +1,235 @@
1
+ //! Shared scaffolding for this module's tests: throwaway keys, a signer, and a scenario builder.
2
+ //!
3
+ //! The seeds are the public throwaway seeds of the known-answer vectors; they are used for nothing
4
+ //! else and protect nothing. Tokens are signed with `jsonwebtoken`'s own signer so the tests do not
5
+ //! need a second crypto dependency.
6
+
7
+ use std::collections::HashMap;
8
+
9
+ use jsonwebtoken::{Algorithm, EncodingKey};
10
+ use serde_json::{json, Map, Value};
11
+
12
+ use super::config::PlatformSigninConfig;
13
+ use super::jti::JtiStore;
14
+ use super::jwks::{thumbprint, KeySource, VerifyingKey};
15
+ use super::jws::encode_base64url;
16
+ use super::strict_json::{Json, JsonObject};
17
+ use super::verify::{Refusal, SigninCookie, SigninRequest, VerifiedClaims, Verifier};
18
+
19
+ pub const NOW: i64 = 1_800_000_000;
20
+ pub const ISSUER: &str = "https://issuer.example";
21
+ pub const SITE: &str = "site-a";
22
+ pub const ENVIRONMENT: &str = "production";
23
+ pub const EMAIL: &str = "person@example.test";
24
+ pub const NONCE: &str = "Nn0nceNn0nceNn0nceAA";
25
+ pub const STATE: &str = "StateStateState0123456789";
26
+
27
+ pub const X_A: &str = "BrQXFhWGK4Fmi8rRsnn1KzomYanZaWCsFWaS0Dgv62k";
28
+ pub const KID_A: &str = "SwpkMw_CknhH156gpWgzPJOQNEGnS25LhD1OqeNsQko";
29
+ pub const X_B: &str = "y4eb9h3HL-Ojhu6bp87lvE1Nl6b8YjIqM_AGXTRMeB0";
30
+
31
+ /// A throwaway Ed25519 key pair: its seed and its public `x`.
32
+ #[derive(Clone, Copy)]
33
+ pub struct TestKey {
34
+ seed: &'static [u8; 32],
35
+ pub x: &'static str,
36
+ }
37
+
38
+ pub const KEY_A: TestKey = TestKey {
39
+ seed: b"TEST-ONLY-KNOWN-ANSWER-SEED-0001",
40
+ x: X_A,
41
+ };
42
+ pub const KEY_B: TestKey = TestKey {
43
+ seed: b"TEST-ONLY-KNOWN-ANSWER-SEED-0002",
44
+ x: X_B,
45
+ };
46
+
47
+ impl TestKey {
48
+ /// Returns the key's thumbprint, which is its `kid`.
49
+ pub fn kid(&self) -> String {
50
+ thumbprint(self.x)
51
+ }
52
+
53
+ /// Returns the public half as a verifying key.
54
+ pub fn verifying(&self) -> VerifyingKey {
55
+ VerifyingKey::from_x(self.x).expect("a valid public key")
56
+ }
57
+
58
+ /// Signs `input` as EdDSA and returns the unpadded base64url signature.
59
+ pub fn sign(&self, input: &str) -> String {
60
+ let mut der = vec![
61
+ 0x30, 0x2e, 0x02, 0x01, 0x00, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x04, 0x22,
62
+ 0x04, 0x20,
63
+ ];
64
+ der.extend_from_slice(self.seed);
65
+ let key = EncodingKey::from_ed_der(&der);
66
+ jsonwebtoken::crypto::sign(input.as_bytes(), &key, Algorithm::EdDSA).expect("signs")
67
+ }
68
+ }
69
+
70
+ /// Builds `header.claims.signature` from raw JSON texts, signed by `key`.
71
+ pub fn assemble(header_text: &str, claims_text: &str, key: &TestKey) -> String {
72
+ let input = format!(
73
+ "{}.{}",
74
+ encode_base64url(header_text.as_bytes()),
75
+ encode_base64url(claims_text.as_bytes())
76
+ );
77
+ format!("{input}.{}", key.sign(&input))
78
+ }
79
+
80
+ /// A key source that holds a fixed set of keys, by computed thumbprint.
81
+ pub struct StaticKeys(pub HashMap<String, VerifyingKey>);
82
+
83
+ impl KeySource for StaticKeys {
84
+ fn key_for(&self, kid: &str, _now: i64) -> Option<VerifyingKey> {
85
+ self.0.get(kid).cloned()
86
+ }
87
+ }
88
+
89
+ /// Builds a key source holding exactly `keys`.
90
+ pub fn static_keys(keys: &[&TestKey]) -> StaticKeys {
91
+ StaticKeys(keys.iter().map(|k| (k.kid(), k.verifying())).collect())
92
+ }
93
+
94
+ /// Returns the pinned configuration the vectors use.
95
+ pub fn config() -> PlatformSigninConfig {
96
+ PlatformSigninConfig::new(
97
+ ISSUER,
98
+ "https://issuer.example/.well-known/jwks.json",
99
+ SITE,
100
+ ENVIRONMENT,
101
+ )
102
+ }
103
+
104
+ /// Builds a verifier that trusts exactly `keys`, with an empty single-use record.
105
+ pub fn verifier_for(keys: &[&TestKey]) -> Verifier<StaticKeys> {
106
+ Verifier::new(config(), static_keys(keys), JtiStore::new())
107
+ }
108
+
109
+ /// Returns the sign-in cookie that matches a good scenario.
110
+ pub fn good_cookie() -> SigninCookie {
111
+ SigninCookie {
112
+ nonce: NONCE.into(),
113
+ state: STATE.into(),
114
+ }
115
+ }
116
+
117
+ /// Turns a verified or raw claim object into a `serde_json::Value`, for comparisons.
118
+ pub fn to_value(json: &Json) -> Value {
119
+ match json {
120
+ Json::Null => Value::Null,
121
+ Json::Bool(b) => Value::Bool(*b),
122
+ Json::Int(i) => Value::from(i64::try_from(*i).unwrap_or(i64::MAX)),
123
+ Json::Number => Value::from(0.5),
124
+ Json::Str(s) => Value::String(s.clone()),
125
+ Json::Array(items) => Value::Array(items.iter().map(to_value).collect()),
126
+ Json::Object(object) => object_to_value(object),
127
+ }
128
+ }
129
+
130
+ /// Turns a parsed object into a `serde_json::Value`.
131
+ pub fn object_to_value(object: &JsonObject) -> Value {
132
+ Value::Object(
133
+ object
134
+ .keys()
135
+ .map(|k| (k.clone(), to_value(object.get(k).expect("member"))))
136
+ .collect(),
137
+ )
138
+ }
139
+
140
+ /// One sign-in attempt, built from a good one by mutating the pieces a test wants to break.
141
+ pub struct Scenario {
142
+ pub header: Map<String, Value>,
143
+ pub claims: Map<String, Value>,
144
+ pub signer: TestKey,
145
+ pub state: Option<String>,
146
+ pub cookie: Option<SigninCookie>,
147
+ pub flip_signature: bool,
148
+ pub replace_token: Option<String>,
149
+ pub spend_first: bool,
150
+ pub now: i64,
151
+ }
152
+
153
+ impl Scenario {
154
+ /// Builds a scenario that a verifier accepts.
155
+ pub fn good() -> Self {
156
+ let claims = json!({
157
+ "iss": ISSUER, "aud": SITE, "env": ENVIRONMENT, "sub": EMAIL,
158
+ "idp_iss": "https://idp.example", "idp_sub": "idp-subject-0001",
159
+ "email": EMAIL, "email_verified": true, "roles": ["Admin"], "nonce": NONCE,
160
+ "iat": NOW, "nbf": NOW, "exp": NOW + 60, "jti": "jti-scenario-0000000000"
161
+ });
162
+ let header = json!({"alg": "EdDSA", "typ": "JWT", "kid": KEY_A.kid()});
163
+ Self {
164
+ header: header.as_object().cloned().unwrap(),
165
+ claims: claims.as_object().cloned().unwrap(),
166
+ signer: KEY_A,
167
+ state: Some(STATE.into()),
168
+ cookie: Some(good_cookie()),
169
+ flip_signature: false,
170
+ replace_token: None,
171
+ spend_first: false,
172
+ now: NOW,
173
+ }
174
+ }
175
+
176
+ /// Builds the token text, signed over the (possibly mutated) header and claims.
177
+ pub fn token(&self) -> String {
178
+ if let Some(text) = &self.replace_token {
179
+ return text.clone();
180
+ }
181
+ let header = Value::Object(self.header.clone()).to_string();
182
+ let claims = Value::Object(self.claims.clone()).to_string();
183
+ let token = assemble(&header, &claims, &self.signer);
184
+ if self.flip_signature {
185
+ flip_last_signature_bit(&token)
186
+ } else {
187
+ token
188
+ }
189
+ }
190
+
191
+ /// Runs the scenario through `verifier`, spending the jti first when asked.
192
+ pub fn run(&self, verifier: &Verifier<StaticKeys>) -> Result<VerifiedClaims, Refusal> {
193
+ if self.spend_first {
194
+ let jti = self
195
+ .claims
196
+ .get("jti")
197
+ .and_then(Value::as_str)
198
+ .expect("a jti to spend");
199
+ assert!(verifier.jti_store().claim(jti, self.now + 90, self.now));
200
+ }
201
+ let token = self.token();
202
+ let request = SigninRequest {
203
+ token: Some(&token),
204
+ state: self.state.as_deref(),
205
+ cookie: self.cookie.as_ref(),
206
+ };
207
+ verifier.verify(&request, self.now)
208
+ }
209
+ }
210
+
211
+ /// Changes the first bit of the signature's first byte, keeping the segment canonical.
212
+ pub fn flip_last_signature_bit(token: &str) -> String {
213
+ let (input, signature) = token.rsplit_once('.').expect("three segments");
214
+ let mut bytes = super::jws::decode_base64url(signature).expect("canonical signature");
215
+ bytes[0] ^= 1;
216
+ format!("{input}.{}", encode_base64url(&bytes))
217
+ }
218
+
219
+ /// A key-set fetcher backed by a closure, for cache tests.
220
+ pub struct FnFetcher(pub Box<dyn Fn() -> Result<String, super::jwks::FetchFailed> + Send + Sync>);
221
+
222
+ impl super::jwks::JwksFetcher for FnFetcher {
223
+ fn fetch(&self) -> Result<String, super::jwks::FetchFailed> {
224
+ (self.0)()
225
+ }
226
+ }
227
+
228
+ /// A key set document listing `keys`.
229
+ pub fn jwks_document(keys: &[&TestKey]) -> String {
230
+ let entries: Vec<String> = keys
231
+ .iter()
232
+ .map(|k| format!(r#"{{"kty":"OKP","crv":"Ed25519","x":"{}"}}"#, k.x))
233
+ .collect();
234
+ format!(r#"{{"keys":[{}]}}"#, entries.join(","))
235
+ }
@@ -0,0 +1,94 @@
1
+ //! The key set cache honours how long the issuer says its answer may be kept, within bounds.
2
+
3
+ use std::sync::atomic::{AtomicI64, AtomicUsize, Ordering};
4
+ use std::sync::Arc;
5
+
6
+ use super::jwks::{FetchFailed, JwksCache, JwksFetcher, KeySource, MAX_TTL_SECONDS};
7
+ use super::test_support::{jwks_document, KEY_A};
8
+
9
+ struct Hinted {
10
+ fetches: Arc<AtomicUsize>,
11
+ hint: Arc<AtomicI64>,
12
+ }
13
+
14
+ impl JwksFetcher for Hinted {
15
+ fn fetch(&self) -> Result<String, FetchFailed> {
16
+ self.fetches.fetch_add(1, Ordering::SeqCst);
17
+ Ok(jwks_document(&[&KEY_A]))
18
+ }
19
+
20
+ fn ttl_hint(&self) -> Option<i64> {
21
+ let hint = self.hint.load(Ordering::SeqCst);
22
+ (hint >= 0).then_some(hint)
23
+ }
24
+ }
25
+
26
+ const START: i64 = 1_000;
27
+
28
+ fn cache(hint: i64) -> (JwksCache<Hinted>, Arc<AtomicUsize>, Arc<AtomicI64>) {
29
+ let fetches = Arc::new(AtomicUsize::new(0));
30
+ let hint = Arc::new(AtomicI64::new(hint));
31
+ let fetcher = Hinted {
32
+ fetches: fetches.clone(),
33
+ hint: hint.clone(),
34
+ };
35
+ (JwksCache::new(fetcher), fetches, hint)
36
+ }
37
+
38
+ /// Asks for the known key at `START + offset` and returns how many fetches have happened.
39
+ fn ask(cache: &JwksCache<Hinted>, fetches: &AtomicUsize, offset: i64) -> usize {
40
+ assert!(cache.key_for(&KEY_A.kid(), START + offset).is_some());
41
+ fetches.load(Ordering::SeqCst)
42
+ }
43
+
44
+ #[test]
45
+ fn without_a_hint_the_default_time_to_live_applies() {
46
+ let (cache, fetches, _) = cache(-1);
47
+ assert_eq!(ask(&cache, &fetches, 0), 1);
48
+ assert_eq!(ask(&cache, &fetches, 299), 1);
49
+ assert_eq!(ask(&cache, &fetches, 300), 2);
50
+ }
51
+
52
+ #[test]
53
+ fn a_shorter_hint_makes_the_keys_stale_sooner() {
54
+ let (cache, fetches, _) = cache(120);
55
+ assert_eq!(ask(&cache, &fetches, 0), 1);
56
+ assert_eq!(ask(&cache, &fetches, 119), 1);
57
+ assert_eq!(ask(&cache, &fetches, 120), 2);
58
+ }
59
+
60
+ #[test]
61
+ fn a_longer_hint_keeps_the_keys_longer() {
62
+ let (cache, fetches, _) = cache(900);
63
+ assert_eq!(ask(&cache, &fetches, 0), 1);
64
+ assert_eq!(ask(&cache, &fetches, 899), 1);
65
+ assert_eq!(ask(&cache, &fetches, 900), 2);
66
+ }
67
+
68
+ #[test]
69
+ fn a_huge_hint_is_capped() {
70
+ let (cache, fetches, _) = cache(10_000_000);
71
+ assert_eq!(ask(&cache, &fetches, 0), 1);
72
+ assert_eq!(ask(&cache, &fetches, MAX_TTL_SECONDS - 1), 1);
73
+ assert_eq!(ask(&cache, &fetches, MAX_TTL_SECONDS), 2);
74
+ }
75
+
76
+ #[test]
77
+ fn a_tiny_hint_cannot_turn_the_cache_into_a_fetch_amplifier() {
78
+ for hint in [0, 1, 29] {
79
+ let (cache, fetches, _) = cache(hint);
80
+ assert_eq!(ask(&cache, &fetches, 0), 1, "hint {hint}");
81
+ assert_eq!(ask(&cache, &fetches, 29), 1, "hint {hint}");
82
+ assert_eq!(ask(&cache, &fetches, 30), 2, "hint {hint}");
83
+ }
84
+ }
85
+
86
+ #[test]
87
+ fn the_next_fetch_without_a_hint_returns_to_the_default() {
88
+ let (cache, fetches, hint) = cache(60);
89
+ assert_eq!(ask(&cache, &fetches, 0), 1);
90
+ hint.store(-1, Ordering::SeqCst);
91
+ assert_eq!(ask(&cache, &fetches, 60), 2);
92
+ assert_eq!(ask(&cache, &fetches, 60 + 299), 2);
93
+ assert_eq!(ask(&cache, &fetches, 60 + 300), 3);
94
+ }