hecks 3.14.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (419) hide show
  1. checksums.yaml +4 -4
  2. data/examples/pizzas/adapters/fake_processor.rb +34 -0
  3. data/examples/pizzas/adapters/payment_signature.rb +66 -0
  4. data/examples/pizzas/adapters/payment_webhook.rb +87 -0
  5. data/examples/pizzas/bluebook/pizzas.behaviors +33 -0
  6. data/examples/pizzas/bluebook/pizzas.hecksagon +5 -3
  7. data/examples/pizzas/config.ru +22 -0
  8. data/lib/hecks/adapters/driven/prism.rb +0 -2
  9. data/lib/hecks/adapters/driven.rb +0 -2
  10. data/lib/hecks/adapters/driving/json.rb +1 -7
  11. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +2 -2
  12. data/lib/hecks/bluebook/dsl/derivation_words.rb +58 -0
  13. data/lib/hecks/bluebook/dsl/translation_builder.rb +6 -37
  14. data/lib/hecks/bluebook/expression/ast_json.rb +0 -9
  15. data/lib/hecks/bluebook/hecksagon.rb +1 -1
  16. data/lib/hecks/bluebook/meta_validator/translation_judge/rules.rb +7 -10
  17. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +4 -8
  18. data/lib/hecks/bluebook/meta_validator.rb +0 -11
  19. data/lib/hecks/bluebook/translation.rb +18 -10
  20. data/lib/hecks/bluebook/translation_expression/evaluator.rb +63 -0
  21. data/lib/hecks/bluebook/translation_expression/parser.rb +127 -0
  22. data/lib/hecks/bluebook/translation_expression.rb +39 -0
  23. data/lib/hecks/bluebook/translation_plan.rb +70 -0
  24. data/lib/hecks/bluebook.rb +2 -0
  25. data/lib/hecks/chapters.rb +3 -3
  26. data/lib/hecks/composer/adapters/claude_composer.adapter +3 -0
  27. data/lib/hecks/composer/adapters/claude_composer.rb +83 -0
  28. data/lib/hecks/composer/adapters/fake_agent.adapter +3 -0
  29. data/lib/hecks/composer/adapters/fake_agent.rb +55 -0
  30. data/lib/hecks/composer/adapters/jev.adapter +3 -0
  31. data/lib/hecks/composer/adapters/jev.rb +66 -0
  32. data/lib/hecks/composer/agent.rb +113 -0
  33. data/lib/hecks/composer/assistant.rb +110 -0
  34. data/lib/hecks/composer/behavior_run.rb +53 -0
  35. data/lib/hecks/composer/bluebook/composer.bluebook +178 -0
  36. data/lib/hecks/composer/bluebook/composer.hecksagon +16 -0
  37. data/lib/hecks/composer/board.rb +65 -0
  38. data/lib/hecks/composer/board_edit/command_text.rb +39 -0
  39. data/lib/hecks/composer/board_edit/source.rb +84 -0
  40. data/lib/hecks/composer/board_edit.rb +144 -0
  41. data/lib/hecks/composer/check.rb +89 -0
  42. data/lib/hecks/composer/domains.rb +81 -0
  43. data/lib/hecks/composer/jobs.rb +134 -0
  44. data/lib/hecks/composer/layout.rb +37 -0
  45. data/lib/hecks/composer/live.rb +150 -0
  46. data/lib/hecks/composer/outline.rb +48 -0
  47. data/lib/hecks/composer/page.html +989 -0
  48. data/lib/hecks/composer/panels.rb +142 -0
  49. data/lib/hecks/composer/parse.rb +83 -0
  50. data/lib/hecks/composer/plain.rb +28 -0
  51. data/lib/hecks/composer/ports/composer_agent.port +7 -0
  52. data/lib/hecks/composer/session.rb +141 -0
  53. data/lib/hecks/composer/toolbox.rb +166 -0
  54. data/lib/hecks/composer/web.rb +156 -0
  55. data/lib/hecks/composer/workbench.rb +51 -0
  56. data/lib/hecks/composer/workspace.rb +81 -0
  57. data/lib/hecks/composer.rb +17 -0
  58. data/lib/hecks/corpus/adapters.rb +22 -0
  59. data/lib/hecks/corpus/rust.rb +1 -1
  60. data/lib/hecks/corpus/worlds.rb +19 -0
  61. data/lib/hecks/corpus.rb +5 -0
  62. data/lib/hecks/forms/app/commands.rb +2 -3
  63. data/lib/hecks/forms/app/queries.rb +2 -3
  64. data/lib/hecks/forms/app/records.rb +17 -5
  65. data/lib/hecks/forms/command_form_renderer.rb +42 -16
  66. data/lib/hecks/forms/field_controls.rb +2 -2
  67. data/lib/hecks/forms/field_renderer.rb +1 -1
  68. data/lib/hecks/forms/field_shape.rb +36 -17
  69. data/lib/hecks/forms/index_renderer.rb +2 -1
  70. data/lib/hecks/forms/page.rb +1 -1
  71. data/lib/hecks/forms/params.rb +0 -13
  72. data/lib/hecks/forms/query_form_renderer.rb +7 -6
  73. data/lib/hecks/forms/record_renderer.rb +11 -7
  74. data/lib/hecks/forms/rule_words.rb +100 -0
  75. data/lib/hecks/forms/value_object_shape.rb +2 -1
  76. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +0 -3
  77. data/lib/hecks/fuzzing/isolated_boot/worlds.rb +3 -6
  78. data/lib/hecks/fuzzing/replay/filters.rb +2 -6
  79. data/lib/hecks/gate/stages.yml +25 -0
  80. data/lib/hecks/grammar/evolve/seeds.rb +0 -6
  81. data/lib/hecks/grammar/evolve.rb +0 -3
  82. data/lib/hecks/grammar/translation.bluebook +1 -1
  83. data/lib/hecks/hecks/adapters/codebase/composing.rb +56 -0
  84. data/lib/hecks/hecks/adapters/codebase/source_tree.rb +3 -1
  85. data/lib/hecks/hecks/adapters/journal_store/changes.rb +0 -26
  86. data/lib/hecks/hecks/adapters/journal_store/examination.rb +2 -23
  87. data/lib/hecks/hecks/adapters/journal_store/readings.rb +3 -11
  88. data/lib/hecks/hecks/adapters/journal_store.rb +0 -11
  89. data/lib/hecks/hecks/adapters/rehearsal/inquiry.rb +4 -11
  90. data/lib/hecks/hecks/adapters/rehearsal/memory_store.rb +0 -9
  91. data/lib/hecks/hecks/adapters/rehearsal/postgres_reading.rb +0 -4
  92. data/lib/hecks/hecks/codebase.behaviors +6 -0
  93. data/lib/hecks/hecks/codebase.bluebook +21 -0
  94. data/lib/hecks/hecks/composer.behaviors +68 -0
  95. data/lib/hecks/hecks/context_map.hecksagon +1 -1
  96. data/lib/hecks/hecks/custodian.behaviors +0 -6
  97. data/lib/hecks/hecks/custodian.bluebook +3 -80
  98. data/lib/hecks/hecks/hecks.hecksagon +2 -2
  99. data/lib/hecks/hecks/hecks.world +1 -1
  100. data/lib/hecks/hecks/rehearsal.bluebook +7 -13
  101. data/lib/hecks/kernel_capabilities/templates.rb +4 -4
  102. data/lib/hecks/kernel_capabilities.rb +1 -1
  103. data/lib/hecks/language/bluebook/vocabulary.bluebook +2 -3
  104. data/lib/hecks/language/translation/translation_aggregate.bluebook +21 -24
  105. data/lib/hecks/ports/access_control.rb +2 -2
  106. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +3 -41
  107. data/lib/hecks/ports/persistence/plugins/era/lineage/coverage.rb +7 -7
  108. data/lib/hecks/ports/persistence/plugins/era/lineage/rewrite.rb +25 -2
  109. data/lib/hecks/ports/persistence/plugins/era/lineage/rules.rb +4 -4
  110. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +17 -25
  111. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/base_sql.rb +0 -12
  112. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/domain_rename.rb +1 -1
  113. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +2 -22
  114. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +0 -1
  115. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +9 -3
  116. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +3 -29
  117. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/refusals.rb +2 -26
  118. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +6 -33
  119. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +3 -3
  120. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +2 -4
  121. data/lib/hecks/ports/persistence/plugins/era/translation/expression_sql.rb +52 -0
  122. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +23 -29
  123. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +1 -1
  124. data/lib/hecks/ports/persistence/plugins/era/translation.rb +0 -1
  125. data/lib/hecks/ports/persistence/plugins/era.rb +3 -6
  126. data/lib/hecks/ports.rb +0 -1
  127. data/lib/hecks/projection_files.rb +3 -1
  128. data/lib/hecks/projections/deploy/templates/lambda/nat_comment_oauth.tmpl +1 -1
  129. data/lib/hecks/projections/glossary/entries.rb +10 -4
  130. data/lib/hecks/projections/glossary/groups.rb +42 -0
  131. data/lib/hecks/projections/glossary/html/page.rb +7 -2
  132. data/lib/hecks/projections/glossary/html/parser.rb +1 -1
  133. data/lib/hecks/projections/glossary/html/sections.rb +31 -11
  134. data/lib/hecks/projections/glossary/html.rb +1 -2
  135. data/lib/hecks/projections/glossary/markdown.rb +31 -28
  136. data/lib/hecks/projections/glossary/mermaid.rb +3 -0
  137. data/lib/hecks/projections/glossary/page.css +143 -153
  138. data/lib/hecks/projections/glossary/page.js +63 -3
  139. data/lib/hecks/projections/glossary/sentences/actions.rb +88 -0
  140. data/lib/hecks/projections/glossary/sentences/paths.rb +77 -0
  141. data/lib/hecks/projections/glossary/sentences/queries.rb +90 -0
  142. data/lib/hecks/projections/glossary/sentences/value_objects.rb +32 -5
  143. data/lib/hecks/projections/glossary/sentences.rb +13 -3
  144. data/lib/hecks/projections/glossary/slugs.rb +4 -1
  145. data/lib/hecks/projections/glossary.rb +2 -1
  146. data/lib/hecks/projections/plain_english.rb +97 -0
  147. data/lib/hecks/projections/rust_vocabulary/arguments.rb +2 -2
  148. data/lib/hecks/projections/rust_vocabulary/enum_body.rb +20 -25
  149. data/lib/hecks/projections/rust_vocabulary/tests.rb +2 -2
  150. data/lib/hecks/projections/rust_vocabulary.rb +14 -6
  151. data/lib/hecks/projections/site/cms_editor/color.rb +0 -6
  152. data/lib/hecks/projections/statements/actions.rb +99 -0
  153. data/lib/hecks/projections/statements/relationships.rb +86 -0
  154. data/lib/hecks/projections/statements.rb +15 -24
  155. data/lib/hecks/projector/exporter/adapters.rb +112 -0
  156. data/lib/hecks/projector/exporter/translations.rb +18 -35
  157. data/lib/hecks/projector/exporter/worlds.rb +103 -0
  158. data/lib/hecks/projector/exporter.rb +5 -1
  159. data/lib/hecks/quality_control/adapters/agent.rb +6 -7
  160. data/lib/hecks/quality_control/adapters/agent_profile.rb +16 -114
  161. data/lib/hecks/quality_control/cli/qa_mine_combinations.rb +1 -1
  162. data/lib/hecks/runtime/loader/boot_steps.rb +0 -20
  163. data/lib/hecks/runtime/outbox.rb +2 -9
  164. data/lib/hecks/runtime/port_operation_interpreter.rb +1 -9
  165. data/lib/hecks/runtime/remote_dispatcher.rb +2 -2
  166. data/lib/hecks/runtime/value.rb +13 -0
  167. data/lib/hecks/rust_build/project_rust/target_ir.rb +23 -4
  168. data/lib/hecks/rust_build/wasm.rb +0 -12
  169. data/lib/hecks/rust_build/write_if_changed.rb +12 -12
  170. data/lib/hecks/rust_format.rb +77 -0
  171. data/lib/hecks/tools/fuzz_sweep/findings.rb +3 -9
  172. data/lib/hecks/tools/fuzz_sweep.rb +1 -3
  173. data/lib/hecks/tools/translation_audit/verdicts.rb +3 -28
  174. data/lib/hecks/tools/translation_audit.rb +8 -10
  175. data/lib/hecks/tools/translation_scaffold/reporting.rb +1 -1
  176. data/lib/hecks/version.rb +1 -1
  177. data/lib/hecks/vocabulary.rb +2 -1
  178. data/lib/hecks.rb +3 -0
  179. data/prebuilt/{a883a7e568667aa5ac904c82daff356bdf9e996252a36af6def4a4a43efb967e.marshal → 02c09c73b272c1fb5a9ae6d87bb5e5e529208e00f6b4b597dc2c0fd70a9c0964.marshal} +0 -0
  180. data/prebuilt/{1a6f596be124228be93eba1d86dbc9d2933190fa389b284331241cb9f27115a0.marshal → bb84a835cf6a15136bad5ca03aa5bf93a5b92f5f72444618d3ef2412b987fb32.marshal} +0 -0
  181. data/prebuilt/{verdicts-872993b1aa26d73fe48f28251374b8585fed145d02aab42be0f00de386b6ff80.json → verdicts-f1f10da14c8e8f01b4c9b7e3e2d74be8cd2bf4676364ed17fa16975d2a076d75.json} +1 -1
  182. data/rust/Cargo.toml +40 -0
  183. data/rust/build/Cargo.toml +36 -0
  184. data/rust/build/clippy.toml +4 -0
  185. data/rust/build/src/adapters_pass.rs +196 -0
  186. data/rust/build/src/binding_pass.rs +54 -13
  187. data/rust/build/src/build_artifact.rs +23 -5
  188. data/rust/build/src/cargo_sync.rs +69 -26
  189. data/rust/build/src/lineage_pass.rs +101 -87
  190. data/rust/build/src/main.rs +11 -2
  191. data/rust/build/src/pipeline.rs +80 -19
  192. data/rust/build/src/reserved_names.rs +6 -50
  193. data/rust/build/src/resolve.rs +82 -35
  194. data/rust/build/src/rustfmt.rs +100 -0
  195. data/rust/build/src/seams_pass.rs +85 -16
  196. data/rust/build/src/subprocess.rs +30 -6
  197. data/rust/build/src/tmp.rs +5 -2
  198. data/rust/build/src/translation_pass.rs +72 -22
  199. data/rust/build/src/world_pass.rs +109 -0
  200. data/rust/clippy.toml +4 -0
  201. data/rust/codegen/Cargo.toml +36 -0
  202. data/rust/codegen/clippy.toml +4 -0
  203. data/rust/codegen/src/asks.rs +40 -14
  204. data/rust/codegen/src/attr.rs +2 -2
  205. data/rust/codegen/src/bridging.rs +676 -117
  206. data/rust/codegen/src/commands.rs +979 -373
  207. data/rust/codegen/src/constraints.rs +88 -25
  208. data/rust/codegen/src/dependency_planning.rs +128 -34
  209. data/rust/codegen/src/domain_generator.rs +300 -170
  210. data/rust/codegen/src/exemplar.rs +87 -32
  211. data/rust/codegen/src/expr_emitter.rs +54 -6
  212. data/rust/codegen/src/fielded.rs +193 -41
  213. data/rust/codegen/src/hecks_naming.rs +124 -16
  214. data/rust/codegen/src/hecks_naming_tests.rs +81 -0
  215. data/rust/codegen/src/json.rs +80 -44
  216. data/rust/codegen/src/json_codec.rs +585 -149
  217. data/rust/codegen/src/literal.rs +104 -138
  218. data/rust/codegen/src/main.rs +145 -52
  219. data/rust/codegen/src/manifest.rs +76 -14
  220. data/rust/codegen/src/mutations.rs +1212 -239
  221. data/rust/codegen/src/naming.rs +111 -60
  222. data/rust/codegen/src/optional_pass.rs +90 -28
  223. data/rust/codegen/src/ports.rs +5 -4
  224. data/rust/codegen/src/prelude.rs +139 -49
  225. data/rust/codegen/src/queries.rs +601 -226
  226. data/rust/codegen/src/reactions.rs +268 -74
  227. data/rust/codegen/src/read_models.rs +606 -205
  228. data/rust/codegen/src/reference_specs.rs +16 -3
  229. data/rust/codegen/src/registry.rs +128 -79
  230. data/rust/codegen/src/reserved_names.rs +6 -50
  231. data/rust/codegen/src/shared.rs +71 -22
  232. data/rust/codegen/src/sidecars.rs +15 -5
  233. data/rust/codegen/src/skip_reason.rs +8 -2
  234. data/rust/codegen/src/types.rs +306 -108
  235. data/rust/host/Cargo.lock +1 -0
  236. data/rust/host/Cargo.toml +40 -4
  237. data/rust/host/HECKS_RELEASE +1 -1
  238. data/rust/host/clippy.toml +4 -0
  239. data/rust/host/src/api.rs +698 -218
  240. data/rust/host/src/auth.rs +491 -214
  241. data/rust/host/src/b64.rs +91 -0
  242. data/rust/host/src/bin/expr_harness.rs +6 -2
  243. data/rust/host/src/bin/lineage_harness.rs +47 -11
  244. data/rust/host/src/bin/mint_harness.rs +70 -37
  245. data/rust/host/src/bin/validate_harness.rs +16 -8
  246. data/rust/host/src/boot.rs +144 -68
  247. data/rust/host/src/boundary_fuzz/api.rs +130 -32
  248. data/rust/host/src/boundary_fuzz/auth.rs +108 -23
  249. data/rust/host/src/boundary_fuzz/expr_json.rs +85 -11
  250. data/rust/host/src/boundary_fuzz/ir.rs +164 -12
  251. data/rust/host/src/boundary_fuzz/server.rs +283 -36
  252. data/rust/host/src/boundary_fuzz/web.rs +56 -8
  253. data/rust/host/src/checkout.rs +78 -32
  254. data/rust/host/src/clock.rs +8 -0
  255. data/rust/host/src/commerce_ir.rs +333 -95
  256. data/rust/host/src/crypto.rs +33 -0
  257. data/rust/host/src/dispatch.rs +990 -331
  258. data/rust/host/src/expr_json.rs +1021 -273
  259. data/rust/host/src/extension.rs +24 -4
  260. data/rust/host/src/field_hints.rs +11 -7
  261. data/rust/host/src/fuzz_support.rs +85 -13
  262. data/rust/host/src/host.rs +100 -0
  263. data/rust/host/src/http.rs +22 -0
  264. data/rust/host/src/ir.rs +326 -16
  265. data/rust/host/src/journal.rs +277 -176
  266. data/rust/host/src/lambda_client.rs +219 -53
  267. data/rust/host/src/lib.rs +16 -4
  268. data/rust/host/src/log.rs +62 -11
  269. data/rust/host/src/mint.rs +1153 -326
  270. data/rust/host/src/needs.rs +185 -44
  271. data/rust/host/src/payments/keystore.rs +197 -45
  272. data/rust/host/src/payments/roles.rs +39 -7
  273. data/rust/host/src/payments/tests.rs +1427 -282
  274. data/rust/host/src/payments.rs +391 -112
  275. data/rust/host/src/platform_signin/attack_tests.rs +5 -5
  276. data/rust/host/src/platform_signin/config.rs +11 -11
  277. data/rust/host/src/platform_signin/cookie.rs +2 -2
  278. data/rust/host/src/platform_signin/extension.rs +2 -1
  279. data/rust/host/src/platform_signin/handler.rs +9 -8
  280. data/rust/host/src/platform_signin/jti.rs +4 -2
  281. data/rust/host/src/platform_signin/jwks.rs +2 -1
  282. data/rust/host/src/platform_signin/jws.rs +2 -37
  283. data/rust/host/src/platform_signin/known_answer_tests.rs +5 -1
  284. data/rust/host/src/platform_signin/mode_tests.rs +5 -3
  285. data/rust/host/src/platform_signin/pages.rs +20 -20
  286. data/rust/host/src/platform_signin/session.rs +8 -1
  287. data/rust/host/src/platform_signin/settings.rs +6 -6
  288. data/rust/host/src/platform_signin/strict_json.rs +12 -9
  289. data/rust/host/src/platform_signin/verify.rs +41 -41
  290. data/rust/host/src/platform_signin/wiring_support.rs +13 -18
  291. data/rust/host/src/presentation.rs +207 -63
  292. data/rust/host/src/presentation_write.rs +502 -187
  293. data/rust/host/src/query_step.rs +69 -18
  294. data/rust/host/src/rate_limit.rs +541 -142
  295. data/rust/host/src/reference_transform.rs +77 -25
  296. data/rust/host/src/reference_validate.rs +443 -104
  297. data/rust/host/src/resend.rs +190 -55
  298. data/rust/host/src/secrets.rs +116 -21
  299. data/rust/host/src/server.rs +483 -104
  300. data/rust/host/src/storage_shape.rs +88 -21
  301. data/rust/host/src/test_pg.rs +17 -7
  302. data/rust/host/src/ui_schema.rs +495 -207
  303. data/rust/host/src/wasm_runner.rs +21 -6
  304. data/rust/host/src/web/commerce.rs +101 -23
  305. data/rust/host/src/web/newsletter/cooldown.rs +10 -3
  306. data/rust/host/src/web/newsletter/template.rs +26 -7
  307. data/rust/host/src/web/newsletter/tests.rs +112 -23
  308. data/rust/host/src/web/newsletter.rs +209 -59
  309. data/rust/host/src/web/newsletter_send.rs +251 -79
  310. data/rust/host/src/web/registration_receipt/tests.rs +16 -4
  311. data/rust/host/src/web/registration_receipt.rs +100 -24
  312. data/rust/host/src/web/registrations.rs +1384 -332
  313. data/rust/host/src/web/resend_webhook.rs +138 -80
  314. data/rust/host/src/web.rs +2416 -508
  315. data/rust/lsp/Cargo.toml +36 -0
  316. data/rust/lsp/clippy.toml +4 -0
  317. data/rust/lsp/src/json.rs +29 -22
  318. data/rust/lsp/src/main.rs +81 -31
  319. data/rust/lsp/src/outline.rs +23 -7
  320. data/rust/lsp/src/rpc.rs +17 -4
  321. data/rust/parser/Cargo.toml +36 -0
  322. data/rust/parser/clippy.toml +4 -0
  323. data/rust/parser/src/build/closed_sets.rs +6 -1
  324. data/rust/parser/src/build/identity.rs +4 -3
  325. data/rust/parser/src/build/mod.rs +1 -1
  326. data/rust/parser/src/build/named_pattern.rs +4 -1
  327. data/rust/parser/src/build/naming.rs +2 -80
  328. data/rust/parser/src/build/pattern_subset.rs +6 -8
  329. data/rust/parser/src/build/query_derive.rs +3 -1
  330. data/rust/parser/src/build/query_inference.rs +14 -5
  331. data/rust/parser/src/build/read_model.rs +4 -1
  332. data/rust/parser/src/build/references.rs +4 -3
  333. data/rust/parser/src/canonical.rs +34 -15
  334. data/rust/parser/src/emit.rs +187 -105
  335. data/rust/parser/src/expr/ast_json.rs +134 -42
  336. data/rust/parser/src/expr/evaluator.rs +27 -9
  337. data/rust/parser/src/expr/mod.rs +54 -13
  338. data/rust/parser/src/expr/resolver.rs +299 -92
  339. data/rust/parser/src/ir.rs +35 -3
  340. data/rust/parser/src/keywords.rs +7168 -516
  341. data/rust/parser/src/lex.rs +11 -25
  342. data/rust/parser/src/lib.rs +1 -0
  343. data/rust/parser/src/main.rs +65 -6
  344. data/rust/parser/src/parse/adapter.rs +157 -0
  345. data/rust/parser/src/parse/aggregate.rs +19 -10
  346. data/rust/parser/src/parse/chapter.rs +16 -17
  347. data/rust/parser/src/parse/command.rs +35 -16
  348. data/rust/parser/src/parse/domain_port.rs +4 -7
  349. data/rust/parser/src/parse/entity.rs +21 -11
  350. data/rust/parser/src/parse/hecksagon.rs +10 -4
  351. data/rust/parser/src/parse/lifecycle.rs +9 -5
  352. data/rust/parser/src/parse/mod.rs +22 -91
  353. data/rust/parser/src/parse/needs.rs +21 -12
  354. data/rust/parser/src/parse/policy.rs +2 -2
  355. data/rust/parser/src/parse/process_manager.rs +9 -11
  356. data/rust/parser/src/parse/query.rs +8 -4
  357. data/rust/parser/src/parse/read_model.rs +57 -16
  358. data/rust/parser/src/parse/value_object.rs +7 -2
  359. data/rust/parser/src/parse/world.rs +322 -0
  360. data/rust/parser/src/ruby_value.rs +7 -6
  361. data/rust/src/exemplar/json.rs +4 -4
  362. data/rust/src/exemplar/mutations.rs +18 -0
  363. data/rust/src/exemplar/read_models.rs +1 -10
  364. data/rust/src/kernel/attribute_shapes/composite.rs +54 -14
  365. data/rust/src/kernel/attribute_shapes/list.rs +2 -2
  366. data/rust/src/kernel/attribute_shapes/mod.rs +8 -8
  367. data/rust/src/kernel/attribute_shapes/scalar.rs +2 -2
  368. data/rust/src/kernel/cli.rs +229 -112
  369. data/rust/src/kernel/dispatch.rs +323 -115
  370. data/rust/src/kernel/dispatch_guard_tests.rs +327 -65
  371. data/rust/src/kernel/expr.rs +203 -86
  372. data/rust/src/kernel/expression_operators/arithmetic.rs +138 -25
  373. data/rust/src/kernel/expression_operators/comparison.rs +87 -2
  374. data/rust/src/kernel/expression_operators/enumeration.rs +106 -22
  375. data/rust/src/kernel/expression_operators/logical.rs +9 -3
  376. data/rust/src/kernel/expression_operators/membership.rs +3 -1
  377. data/rust/src/kernel/expression_operators/mod.rs +34 -21
  378. data/rust/src/kernel/expression_operators/pattern_match.rs +46 -11
  379. data/rust/src/kernel/expression_operators/positional.rs +84 -30
  380. data/rust/src/kernel/expression_operators/presence.rs +52 -13
  381. data/rust/src/kernel/expression_operators/sign_test.rs +3 -1
  382. data/rust/src/kernel/expression_operators/sized.rs +24 -8
  383. data/rust/src/kernel/expression_operators/text.rs +120 -27
  384. data/rust/src/kernel/expression_operators/to_string.rs +10 -4
  385. data/rust/src/kernel/json.rs +230 -148
  386. data/rust/src/kernel/mod.rs +46 -33
  387. data/rust/src/kernel/named_query.rs +63 -15
  388. data/rust/src/kernel/needs.rs +112 -25
  389. data/rust/src/kernel/orchestrate.rs +980 -581
  390. data/rust/src/kernel/orchestrate_guard_tests.rs +538 -140
  391. data/rust/src/kernel/pattern.rs +107 -34
  392. data/rust/src/kernel/query_comparators.rs +118 -41
  393. data/rust/src/kernel/query_ordering.rs +13 -5
  394. data/rust/src/kernel/read_model.rs +479 -181
  395. data/rust/src/kernel/reference_lookup.rs +122 -26
  396. data/rust/src/kernel/repository.rs +293 -66
  397. data/rust/src/kernel/routing.rs +58 -42
  398. data/rust/src/kernel/ruby_float.rs +139 -0
  399. data/rust/src/kernel/vocab/aggregate_dispatch_order.rs +57 -57
  400. data/rust/src/kernel/vocab/entity_dispatch_order.rs +54 -54
  401. data/rust/src/kernel/vocab/field_hint.rs +19 -25
  402. data/rust/src/kernel/vocab/mod.rs +6 -6
  403. data/rust/src/kernel/vocab/query_comparator.rs +23 -24
  404. data/rust/src/kernel/vocab/refusal_template.rs +501 -297
  405. data/rust/src/lib.rs +50 -0
  406. data/rust/src/main.rs +4 -1
  407. data/rust/web/Cargo.toml +36 -0
  408. data/rust/web/clippy.toml +4 -0
  409. metadata +82 -15
  410. data/examples/pizzas/bluebook/hecksagon/mock_stripe_payment_adapter.rb +0 -36
  411. data/lib/hecks/adapters/driven/google_authentication.adapter +0 -3
  412. data/lib/hecks/adapters/driven/google_authentication.rb +0 -99
  413. data/lib/hecks/adapters/driven/mock_stripe_adapter.adapter +0 -3
  414. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +0 -32
  415. data/lib/hecks/ports/authentication.port +0 -6
  416. data/lib/hecks/ports/authentication.rb +0 -67
  417. data/lib/hecks/ports/persistence/plugins/era/translation/approval_file.rb +0 -255
  418. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +0 -22
  419. data/rust/host/src/approval.rs +0 -652
@@ -1,10 +1,11 @@
1
1
  //! Per-address sliding-window rate limits for the public write routes a stranger can abuse,
2
2
  //! enforced in `server.rs` before dispatch so a refused request costs no Postgres lock or run.
3
3
 
4
+ use crate::crypto::constant_time_eq;
5
+ use crate::extension::RateRule;
4
6
  use axum::http::header::{HeaderValue, CONTENT_TYPE, RETRY_AFTER};
5
7
  use axum::http::{HeaderMap, HeaderName, StatusCode};
6
8
  use axum::response::{IntoResponse, Response};
7
- use crate::extension::RateRule;
8
9
  use serde_json::json;
9
10
  use std::collections::{BTreeMap, HashMap, VecDeque};
10
11
  use std::net::{IpAddr, Ipv6Addr};
@@ -63,14 +64,24 @@ impl SlidingWindow {
63
64
  let window = self.window;
64
65
  match self.entries.get_mut(key) {
65
66
  Some(entry) => {
66
- while entry.stamps.front().is_some_and(|s| now.saturating_duration_since(*s) >= window) {
67
+ while entry
68
+ .stamps
69
+ .front()
70
+ .is_some_and(|s| now.saturating_duration_since(*s) >= window)
71
+ {
67
72
  entry.stamps.pop_front();
68
73
  }
69
74
  if entry.stamps.len() >= self.limit {
70
75
  let oldest = entry.stamps.front().copied().unwrap_or(now);
71
76
  let wait = window.saturating_sub(now.saturating_duration_since(oldest));
72
- let retry_after_secs = u64::try_from(wait.as_millis().div_ceil(1000)).unwrap_or(u64::MAX).max(1);
73
- return Decision { allowed: false, remaining: 0, retry_after_secs };
77
+ let retry_after_secs = u64::try_from(wait.as_millis().div_ceil(1000))
78
+ .unwrap_or(u64::MAX)
79
+ .max(1);
80
+ return Decision {
81
+ allowed: false,
82
+ remaining: 0,
83
+ retry_after_secs,
84
+ };
74
85
  }
75
86
  }
76
87
  None => self.make_room(now),
@@ -86,20 +97,27 @@ impl SlidingWindow {
86
97
  fn record(&mut self, key: &str, now: Instant) -> Decision {
87
98
  let seq = self.next_seq;
88
99
  self.next_seq += 1;
89
- let remaining = match self.entries.get_mut(key) {
90
- Some(entry) => {
91
- self.recency.remove(&entry.seq);
92
- entry.seq = seq;
93
- entry.stamps.push_back(now);
94
- self.limit - entry.stamps.len()
95
- }
96
- None => {
97
- self.entries.insert(key.to_string(), Entry { stamps: VecDeque::from([now]), seq });
98
- self.limit - 1
99
- }
100
+ let remaining = if let Some(entry) = self.entries.get_mut(key) {
101
+ self.recency.remove(&entry.seq);
102
+ entry.seq = seq;
103
+ entry.stamps.push_back(now);
104
+ self.limit - entry.stamps.len()
105
+ } else {
106
+ self.entries.insert(
107
+ key.to_string(),
108
+ Entry {
109
+ stamps: VecDeque::from([now]),
110
+ seq,
111
+ },
112
+ );
113
+ self.limit - 1
100
114
  };
101
115
  self.recency.insert(seq, key.to_string());
102
- Decision { allowed: true, remaining, retry_after_secs: 0 }
116
+ Decision {
117
+ allowed: true,
118
+ remaining,
119
+ retry_after_secs: 0,
120
+ }
103
121
  }
104
122
 
105
123
  // Called before adding a new key: sweeps keys whose last request left the window, and only
@@ -123,7 +141,9 @@ impl SlidingWindow {
123
141
  self.entries.remove(&key);
124
142
  }
125
143
  while self.entries.len() >= self.max_keys {
126
- let Some((_, key)) = self.recency.pop_first() else { break };
144
+ let Some((_, key)) = self.recency.pop_first() else {
145
+ break;
146
+ };
127
147
  self.entries.remove(&key);
128
148
  }
129
149
  }
@@ -155,11 +175,19 @@ impl IpNet {
155
175
  pub fn contains(&self, ip: IpAddr) -> bool {
156
176
  match (self.addr, ip.to_canonical()) {
157
177
  (IpAddr::V4(net), IpAddr::V4(ip)) => {
158
- let mask = if self.prefix == 0 { 0 } else { u32::MAX << (32 - u32::from(self.prefix)) };
178
+ let mask = if self.prefix == 0 {
179
+ 0
180
+ } else {
181
+ u32::MAX << (32 - u32::from(self.prefix))
182
+ };
159
183
  u32::from(net) & mask == u32::from(ip) & mask
160
184
  }
161
185
  (IpAddr::V6(net), IpAddr::V6(ip)) => {
162
- let mask = if self.prefix == 0 { 0 } else { u128::MAX << (128 - u32::from(self.prefix)) };
186
+ let mask = if self.prefix == 0 {
187
+ 0
188
+ } else {
189
+ u128::MAX << (128 - u32::from(self.prefix))
190
+ };
163
191
  u128::from(net) & mask == u128::from(ip) & mask
164
192
  }
165
193
  _ => false,
@@ -183,12 +211,22 @@ pub struct ProxyTrust {
183
211
  impl ProxyTrust {
184
212
  // `None` means the client address could not be determined safely, rather than guessed;
185
213
  // callers then share one bucket. The forwarded header is read only from a trusted source.
186
- pub fn client_ip(&self, peer: Option<IpAddr>, forwarded_for: Option<&str>, presented_secret: Option<&str>) -> Option<IpAddr> {
214
+ pub fn client_ip(
215
+ &self,
216
+ peer: Option<IpAddr>,
217
+ forwarded_for: Option<&str>,
218
+ presented_secret: Option<&str>,
219
+ ) -> Option<IpAddr> {
187
220
  let peer = peer.map(|p| p.to_canonical());
188
221
  if !self.trusts(peer, presented_secret) {
189
222
  return peer;
190
223
  }
191
- let entries: Vec<&str> = forwarded_for.unwrap_or("").split(',').map(str::trim).filter(|e| !e.is_empty()).collect();
224
+ let entries: Vec<&str> = forwarded_for
225
+ .unwrap_or("")
226
+ .split(',')
227
+ .map(str::trim)
228
+ .filter(|e| !e.is_empty())
229
+ .collect();
192
230
  if entries.is_empty() {
193
231
  return peer;
194
232
  }
@@ -203,17 +241,15 @@ impl ProxyTrust {
203
241
 
204
242
  fn trusts(&self, peer: Option<IpAddr>, presented_secret: Option<&str>) -> bool {
205
243
  let secret_matches = match (&self.auth, presented_secret) {
206
- (Some(auth), Some(presented)) => constant_time_eq(auth.secret.as_bytes(), presented.as_bytes()),
244
+ (Some(auth), Some(presented)) => {
245
+ constant_time_eq(auth.secret.as_bytes(), presented.as_bytes())
246
+ }
207
247
  _ => false,
208
248
  };
209
249
  secret_matches || peer.is_some_and(|p| self.networks.iter().any(|net| net.contains(p)))
210
250
  }
211
251
  }
212
252
 
213
- fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
214
- a.len() == b.len() && a.iter().zip(b).fold(0u8, |diff, (x, y)| diff | (x ^ y)) == 0
215
- }
216
-
217
253
  #[derive(Debug, Clone, PartialEq, Eq)]
218
254
  pub struct Config {
219
255
  enabled: bool,
@@ -230,28 +266,53 @@ impl Config {
230
266
  pub fn from_lookup(get: impl Fn(&str) -> Option<String>) -> (Self, Vec<String>) {
231
267
  let mut warnings = Vec::new();
232
268
  let mut number = |name: &str, default: u64| -> u64 {
233
- match get(name).map(|v| v.trim().to_string()).filter(|v| !v.is_empty()) {
234
- None => default,
235
- Some(raw) => raw.parse::<u64>().ok().filter(|n| *n >= 1).unwrap_or_else(|| {
236
- warnings.push(format!("{name}={raw:?} is not a positive whole number; using {default}"));
237
- default
238
- }),
239
- }
269
+ get(name)
270
+ .map(|v| v.trim().to_string())
271
+ .filter(|v| !v.is_empty())
272
+ .map_or(default, |raw| {
273
+ raw.parse::<u64>()
274
+ .ok()
275
+ .filter(|n| *n >= 1)
276
+ .unwrap_or_else(|| {
277
+ warnings.push(format!(
278
+ "{name}={raw:?} is not a positive whole number; using {default}"
279
+ ));
280
+ default
281
+ })
282
+ })
240
283
  };
241
284
  let window = number("HECKS_RATE_LIMIT_WINDOW_SECONDS", DEFAULT_WINDOW_SECONDS);
242
285
  let limits = crate::extension::rate_rules()
243
286
  .iter()
244
- .map(|rule| (rule.name, usize::try_from(number(rule.limit_env, rule.default_limit as u64)).unwrap_or(usize::MAX)))
287
+ .map(|rule| {
288
+ (
289
+ rule.name,
290
+ usize::try_from(number(rule.limit_env, rule.default_limit as u64))
291
+ .unwrap_or(usize::MAX),
292
+ )
293
+ })
245
294
  .collect();
246
295
  let max_keys = number("HECKS_RATE_LIMIT_MAX_KEYS", DEFAULT_MAX_KEYS as u64);
247
- let hops = get("HECKS_TRUSTED_PROXY_HOPS").map(|v| v.trim().to_string()).filter(|v| !v.is_empty()).map_or(0, |raw| {
248
- raw.parse::<usize>().unwrap_or_else(|_| {
249
- warnings.push(format!("HECKS_TRUSTED_PROXY_HOPS={raw:?} is not a whole number; using 0"));
250
- 0
251
- })
252
- });
253
- let networks = parse_networks(get("HECKS_TRUSTED_PROXIES").as_deref().unwrap_or(""), &mut warnings);
254
- let auth = parse_proxy_auth(get("HECKS_PROXY_AUTH_HEADER"), get("HECKS_PROXY_AUTH_SECRET"), &mut warnings);
296
+ let hops = get("HECKS_TRUSTED_PROXY_HOPS")
297
+ .map(|v| v.trim().to_string())
298
+ .filter(|v| !v.is_empty())
299
+ .map_or(0, |raw| {
300
+ raw.parse::<usize>().unwrap_or_else(|_| {
301
+ warnings.push(format!(
302
+ "HECKS_TRUSTED_PROXY_HOPS={raw:?} is not a whole number; using 0"
303
+ ));
304
+ 0
305
+ })
306
+ });
307
+ let networks = parse_networks(
308
+ get("HECKS_TRUSTED_PROXIES").as_deref().unwrap_or(""),
309
+ &mut warnings,
310
+ );
311
+ let auth = parse_proxy_auth(
312
+ get("HECKS_PROXY_AUTH_HEADER"),
313
+ get("HECKS_PROXY_AUTH_SECRET"),
314
+ &mut warnings,
315
+ );
255
316
  let enabled = !get("HECKS_RATE_LIMIT").is_some_and(|v| is_off(&v));
256
317
 
257
318
  let config = Self {
@@ -259,13 +320,20 @@ impl Config {
259
320
  window: Duration::from_secs(window),
260
321
  limits,
261
322
  max_keys: usize::try_from(max_keys).unwrap_or(usize::MAX),
262
- trust: ProxyTrust { networks, hops, auth },
323
+ trust: ProxyTrust {
324
+ networks,
325
+ hops,
326
+ auth,
327
+ },
263
328
  };
264
329
  (config, warnings)
265
330
  }
266
331
 
267
332
  fn limit_for(&self, rule: &RateRule) -> usize {
268
- self.limits.get(rule.name).copied().unwrap_or(rule.default_limit)
333
+ self.limits
334
+ .get(rule.name)
335
+ .copied()
336
+ .unwrap_or(rule.default_limit)
269
337
  }
270
338
 
271
339
  #[cfg(test)]
@@ -275,7 +343,10 @@ impl Config {
275
343
  }
276
344
 
277
345
  fn is_off(value: &str) -> bool {
278
- matches!(value.trim().to_ascii_lowercase().as_str(), "off" | "false" | "0" | "no" | "disabled")
346
+ matches!(
347
+ value.trim().to_ascii_lowercase().as_str(),
348
+ "off" | "false" | "0" | "no" | "disabled"
349
+ )
279
350
  }
280
351
 
281
352
  fn parse_networks(list: &str, warnings: &mut Vec<String>) -> Vec<IpNet> {
@@ -292,18 +363,26 @@ fn parse_networks(list: &str, warnings: &mut Vec<String>) -> Vec<IpNet> {
292
363
  .collect()
293
364
  }
294
365
 
295
- fn parse_proxy_auth(header: Option<String>, secret: Option<String>, warnings: &mut Vec<String>) -> Option<ProxyAuth> {
296
- let header = header.map(|v| v.trim().to_string()).filter(|v| !v.is_empty());
366
+ fn parse_proxy_auth(
367
+ header: Option<String>,
368
+ secret: Option<String>,
369
+ warnings: &mut Vec<String>,
370
+ ) -> Option<ProxyAuth> {
371
+ let header = header
372
+ .map(|v| v.trim().to_string())
373
+ .filter(|v| !v.is_empty());
297
374
  let secret = secret.filter(|v| !v.is_empty());
298
375
  match (header, secret) {
299
376
  (None, None) => None,
300
- (Some(header), Some(secret)) => match HeaderName::from_bytes(header.to_ascii_lowercase().as_bytes()) {
301
- Ok(header) => Some(ProxyAuth { header, secret }),
302
- Err(_) => {
303
- warnings.push(format!("HECKS_PROXY_AUTH_HEADER {header:?} is not a valid header name; proxy authentication is off"));
304
- None
305
- }
306
- },
377
+ (Some(header), Some(secret)) => {
378
+ HeaderName::from_bytes(header.to_ascii_lowercase().as_bytes()).map_or_else(
379
+ |_| {
380
+ warnings.push(format!("HECKS_PROXY_AUTH_HEADER {header:?} is not a valid header name; proxy authentication is off"));
381
+ None
382
+ },
383
+ |name| Some(ProxyAuth { header: name, secret }),
384
+ )
385
+ }
307
386
  _ => {
308
387
  warnings.push("HECKS_PROXY_AUTH_HEADER and HECKS_PROXY_AUTH_SECRET must both be set; proxy authentication is off".to_string());
309
388
  None
@@ -328,7 +407,11 @@ pub struct RateLimits {
328
407
 
329
408
  impl RateLimits {
330
409
  pub fn new(config: Config) -> Self {
331
- Self { config, windows: Mutex::new(HashMap::new()), warned_untrusted_proxy: AtomicBool::new(false) }
410
+ Self {
411
+ config,
412
+ windows: Mutex::new(HashMap::new()),
413
+ warned_untrusted_proxy: AtomicBool::new(false),
414
+ }
332
415
  }
333
416
 
334
417
  pub fn from_env() -> Self {
@@ -351,47 +434,85 @@ impl RateLimits {
351
434
  }
352
435
 
353
436
  // A route that is not limited is always `Allowed`.
354
- pub fn check(&self, method: &str, path: &str, headers: &HeaderMap, peer: Option<IpAddr>) -> Verdict {
437
+ pub fn check(
438
+ &self,
439
+ method: &str,
440
+ path: &str,
441
+ headers: &HeaderMap,
442
+ peer: Option<IpAddr>,
443
+ ) -> Verdict {
355
444
  self.check_at(method, path, headers, peer, Instant::now())
356
445
  }
357
446
 
358
447
  // `check` with the clock supplied, so tests need not sleep.
359
- pub fn check_at(&self, method: &str, path: &str, headers: &HeaderMap, peer: Option<IpAddr>, now: Instant) -> Verdict {
360
- let Some(rule) = crate::extension::rate_rule_for(method, path).filter(|_| self.config.enabled) else {
448
+ pub fn check_at(
449
+ &self,
450
+ method: &str,
451
+ path: &str,
452
+ headers: &HeaderMap,
453
+ peer: Option<IpAddr>,
454
+ now: Instant,
455
+ ) -> Verdict {
456
+ let Some(rule) =
457
+ crate::extension::rate_rule_for(method, path).filter(|_| self.config.enabled)
458
+ else {
361
459
  return Verdict::Allowed;
362
460
  };
363
461
  let key = self.client_key(headers, peer);
364
462
  let decision = {
365
- let mut windows = self.windows.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
463
+ let mut windows = self
464
+ .windows
465
+ .lock()
466
+ .unwrap_or_else(std::sync::PoisonError::into_inner);
366
467
  let config = &self.config;
367
468
  windows
368
469
  .entry(rule.name)
369
- .or_insert_with(|| SlidingWindow::new(config.limit_for(&rule), config.window, config.max_keys))
470
+ .or_insert_with(|| {
471
+ SlidingWindow::new(config.limit_for(&rule), config.window, config.max_keys)
472
+ })
370
473
  .check(&key, now)
371
474
  };
372
475
  if decision.allowed {
373
476
  Verdict::Allowed
374
477
  } else {
375
- Verdict::Limited { retry_after_secs: decision.retry_after_secs }
478
+ Verdict::Limited {
479
+ retry_after_secs: decision.retry_after_secs,
480
+ }
376
481
  }
377
482
  }
378
483
 
379
484
  fn client_key(&self, headers: &HeaderMap, peer: Option<IpAddr>) -> String {
380
485
  let forwarded_for = joined_header(headers, "x-forwarded-for");
381
- let presented = self.config.trust.auth.as_ref().and_then(|auth| headers.get(&auth.header)).and_then(|v| v.to_str().ok());
486
+ let presented = self
487
+ .config
488
+ .trust
489
+ .auth
490
+ .as_ref()
491
+ .and_then(|auth| headers.get(&auth.header))
492
+ .and_then(|v| v.to_str().ok());
382
493
  self.warn_if_proxy_untrusted(forwarded_for.as_deref(), peer, presented);
383
- match self.config.trust.client_ip(peer, forwarded_for.as_deref(), presented) {
384
- Some(ip) => bucket_key(ip),
385
- None => UNKNOWN_CLIENT.to_string(),
386
- }
494
+ self.config
495
+ .trust
496
+ .client_ip(peer, forwarded_for.as_deref(), presented)
497
+ .map_or_else(|| UNKNOWN_CLIENT.to_string(), bucket_key)
387
498
  }
388
499
 
389
500
  // A forwarded header from a private peer that nothing trusts means a proxy sits in front of
390
501
  // the host and every visitor is being counted as that proxy. Said once, since it repeats on
391
502
  // every request.
392
- fn warn_if_proxy_untrusted(&self, forwarded_for: Option<&str>, peer: Option<IpAddr>, presented: Option<&str>) {
393
- let Some(peer) = peer.map(|p| p.to_canonical()) else { return };
394
- if forwarded_for.is_none() || !is_private(peer) || self.config.trust.trusts(Some(peer), presented) {
503
+ fn warn_if_proxy_untrusted(
504
+ &self,
505
+ forwarded_for: Option<&str>,
506
+ peer: Option<IpAddr>,
507
+ presented: Option<&str>,
508
+ ) {
509
+ let Some(peer) = peer.map(|p| p.to_canonical()) else {
510
+ return;
511
+ };
512
+ if forwarded_for.is_none()
513
+ || !is_private(peer)
514
+ || self.config.trust.trusts(Some(peer), presented)
515
+ {
395
516
  return;
396
517
  }
397
518
  if !self.warned_untrusted_proxy.swap(true, Ordering::Relaxed) {
@@ -409,7 +530,11 @@ impl RateLimits {
409
530
 
410
531
  // Several X-Forwarded-For header lines mean the same as one comma-joined line.
411
532
  fn joined_header(headers: &HeaderMap, name: &str) -> Option<String> {
412
- let values: Vec<String> = headers.get_all(name).iter().map(|v| String::from_utf8_lossy(v.as_bytes()).into_owned()).collect();
533
+ let values: Vec<String> = headers
534
+ .get_all(name)
535
+ .iter()
536
+ .map(|v| String::from_utf8_lossy(v.as_bytes()).into_owned())
537
+ .collect();
413
538
  if values.is_empty() {
414
539
  None
415
540
  } else {
@@ -429,7 +554,7 @@ fn bucket_key(ip: IpAddr) -> String {
429
554
  }
430
555
  }
431
556
 
432
- fn is_private(ip: IpAddr) -> bool {
557
+ const fn is_private(ip: IpAddr) -> bool {
433
558
  match ip {
434
559
  IpAddr::V4(v4) => v4.is_private() || v4.is_loopback() || v4.is_link_local(),
435
560
  IpAddr::V6(v6) => v6.is_loopback() || (v6.segments()[0] & 0xfe00) == 0xfc00,
@@ -461,12 +586,18 @@ mod tests {
461
586
  ProxyTrust {
462
587
  networks: networks.iter().map(|n| IpNet::parse(n).unwrap()).collect(),
463
588
  hops,
464
- auth: auth.map(|(header, secret)| ProxyAuth { header: HeaderName::from_bytes(header.as_bytes()).unwrap(), secret: secret.to_string() }),
589
+ auth: auth.map(|(header, secret)| ProxyAuth {
590
+ header: HeaderName::from_bytes(header.as_bytes()).unwrap(),
591
+ secret: secret.to_string(),
592
+ }),
465
593
  }
466
594
  }
467
595
 
468
596
  fn config_from(pairs: &[(&str, &str)]) -> (Config, Vec<String>) {
469
- let map: HashMap<String, String> = pairs.iter().map(|(k, v)| (k.to_string(), v.to_string())).collect();
597
+ let map: HashMap<String, String> = pairs
598
+ .iter()
599
+ .map(|(k, v)| (k.to_string(), v.to_string()))
600
+ .collect();
470
601
  Config::from_lookup(|name| map.get(name).cloned())
471
602
  }
472
603
 
@@ -477,7 +608,10 @@ mod tests {
477
608
  fn headers(pairs: &[(&str, &str)]) -> HeaderMap {
478
609
  let mut map = HeaderMap::new();
479
610
  for (name, value) in pairs {
480
- map.append(HeaderName::from_bytes(name.as_bytes()).unwrap(), HeaderValue::from_str(value).unwrap());
611
+ map.append(
612
+ HeaderName::from_bytes(name.as_bytes()).unwrap(),
613
+ HeaderValue::from_str(value).unwrap(),
614
+ );
481
615
  }
482
616
  map
483
617
  }
@@ -486,9 +620,22 @@ mod tests {
486
620
  fn allows_up_to_the_limit_then_refuses_with_the_seconds_until_the_oldest_hit_expires() {
487
621
  let start = Instant::now();
488
622
  let mut window = SlidingWindow::new(3, HOUR, 100);
489
- assert_eq!(window.check("a", start), Decision { allowed: true, remaining: 2, retry_after_secs: 0 });
490
- assert_eq!(window.check("a", start + Duration::from_secs(10)).remaining, 1);
491
- assert_eq!(window.check("a", start + Duration::from_secs(20)).remaining, 0);
623
+ assert_eq!(
624
+ window.check("a", start),
625
+ Decision {
626
+ allowed: true,
627
+ remaining: 2,
628
+ retry_after_secs: 0
629
+ }
630
+ );
631
+ assert_eq!(
632
+ window.check("a", start + Duration::from_secs(10)).remaining,
633
+ 1
634
+ );
635
+ assert_eq!(
636
+ window.check("a", start + Duration::from_secs(20)).remaining,
637
+ 0
638
+ );
492
639
 
493
640
  let refused = window.check("a", start + Duration::from_secs(30));
494
641
  assert!(!refused.allowed);
@@ -500,12 +647,18 @@ mod tests {
500
647
  let start = Instant::now();
501
648
  let mut window = SlidingWindow::new(2, HOUR, 100);
502
649
  window.check("a", start);
503
- window.check("a", start + Duration::from_secs(1800));
650
+ window.check("a", start + Duration::from_mins(30));
504
651
  assert!(!window.check("a", start + Duration::from_secs(1801)).allowed);
505
652
 
506
653
  let later = start + Duration::from_secs(3601);
507
- assert!(window.check("a", later).allowed, "the first hit is past the window");
508
- assert!(!window.check("a", later).allowed, "the second is still inside it");
654
+ assert!(
655
+ window.check("a", later).allowed,
656
+ "the first hit is past the window"
657
+ );
658
+ assert!(
659
+ !window.check("a", later).allowed,
660
+ "the second is still inside it"
661
+ );
509
662
  }
510
663
 
511
664
  #[test]
@@ -523,9 +676,16 @@ mod tests {
523
676
  let mut window = SlidingWindow::new(1, HOUR, 100);
524
677
  window.check("a", start);
525
678
  for second in 1..=50 {
526
- assert!(!window.check("a", start + Duration::from_secs(second)).allowed);
679
+ assert!(
680
+ !window
681
+ .check("a", start + Duration::from_secs(second))
682
+ .allowed
683
+ );
527
684
  }
528
- assert!(window.check("a", start + HOUR).allowed, "the one recorded hit expired on schedule");
685
+ assert!(
686
+ window.check("a", start + HOUR).allowed,
687
+ "the one recorded hit expired on schedule"
688
+ );
529
689
  }
530
690
 
531
691
  #[test]
@@ -533,8 +693,18 @@ mod tests {
533
693
  let start = Instant::now();
534
694
  let mut window = SlidingWindow::new(1, Duration::from_millis(1500), 100);
535
695
  window.check("a", start);
536
- assert_eq!(window.check("a", start + Duration::from_millis(1)).retry_after_secs, 2);
537
- assert_eq!(window.check("a", start + Duration::from_millis(1499)).retry_after_secs, 1);
696
+ assert_eq!(
697
+ window
698
+ .check("a", start + Duration::from_millis(1))
699
+ .retry_after_secs,
700
+ 2
701
+ );
702
+ assert_eq!(
703
+ window
704
+ .check("a", start + Duration::from_millis(1499))
705
+ .retry_after_secs,
706
+ 1
707
+ );
538
708
  }
539
709
 
540
710
  #[test]
@@ -560,7 +730,10 @@ mod tests {
560
730
  }
561
731
  assert_eq!(window.len(), 3);
562
732
  let now = start + Duration::from_millis(10);
563
- assert!(window.check("a", now).allowed, "a was evicted, so it starts fresh");
733
+ assert!(
734
+ window.check("a", now).allowed,
735
+ "a was evicted, so it starts fresh"
736
+ );
564
737
  assert!(!window.check("e", now).allowed, "e is still tracked");
565
738
  }
566
739
 
@@ -587,15 +760,31 @@ mod tests {
587
760
  let v6 = IpNet::parse("fd00::/8").unwrap();
588
761
  assert!(v6.contains(ip("fd12::1")));
589
762
  assert!(!v6.contains(ip("2001:db8::1")));
590
- assert!(!v6.contains(ip("10.0.0.1")), "families never match each other");
763
+ assert!(
764
+ !v6.contains(ip("10.0.0.1")),
765
+ "families never match each other"
766
+ );
591
767
 
592
768
  assert!(IpNet::parse("0.0.0.0/0").unwrap().contains(ip("8.8.8.8")));
593
- assert!(IpNet::parse("10.0.0.0/8").unwrap().contains(ip("::ffff:10.1.1.1")), "a mapped address is its IPv4 form");
769
+ assert!(
770
+ IpNet::parse("10.0.0.0/8")
771
+ .unwrap()
772
+ .contains(ip("::ffff:10.1.1.1")),
773
+ "a mapped address is its IPv4 form"
774
+ );
594
775
  }
595
776
 
596
777
  #[test]
597
778
  fn rejects_malformed_ranges() {
598
- for bad in ["", "banana", "10.0.0.0/33", "::1/129", "10.0.0.0/", "10.0.0/8", "/8"] {
779
+ for bad in [
780
+ "",
781
+ "banana",
782
+ "10.0.0.0/33",
783
+ "::1/129",
784
+ "10.0.0.0/",
785
+ "10.0.0/8",
786
+ "/8",
787
+ ] {
599
788
  assert!(IpNet::parse(bad).is_none(), "{bad:?}");
600
789
  }
601
790
  }
@@ -603,14 +792,18 @@ mod tests {
603
792
  #[test]
604
793
  fn with_nothing_trusted_the_peer_is_the_client_and_the_header_is_ignored() {
605
794
  let none = trust(&[], 0, None);
606
- assert_eq!(none.client_ip(Some(ip("198.51.100.4")), Some("6.6.6.6"), None), Some(ip("198.51.100.4")));
795
+ assert_eq!(
796
+ none.client_ip(Some(ip("198.51.100.4")), Some("6.6.6.6"), None),
797
+ Some(ip("198.51.100.4"))
798
+ );
607
799
  assert_eq!(none.client_ip(None, Some("6.6.6.6"), None), None);
608
800
  }
609
801
 
610
802
  #[test]
611
803
  fn an_untrusted_peer_cannot_choose_its_own_key_by_sending_a_header() {
612
804
  let proxies = trust(&["10.0.0.0/8"], 0, None);
613
- let from_stranger = |spoof: &str| proxies.client_ip(Some(ip("198.51.100.4")), Some(spoof), None);
805
+ let from_stranger =
806
+ |spoof: &str| proxies.client_ip(Some(ip("198.51.100.4")), Some(spoof), None);
614
807
  assert_eq!(from_stranger("1.1.1.1"), from_stranger("2.2.2.2"));
615
808
  assert_eq!(from_stranger("1.1.1.1"), Some(ip("198.51.100.4")));
616
809
  }
@@ -618,20 +811,36 @@ mod tests {
618
811
  #[test]
619
812
  fn a_trusted_peer_is_believed_and_the_rightmost_entry_is_the_client() {
620
813
  let proxies = trust(&["10.0.0.0/8"], 0, None);
621
- assert_eq!(proxies.client_ip(Some(ip("10.0.0.5")), Some(&format!("6.6.6.6, {VISITOR}")), None), Some(ip(VISITOR)));
814
+ assert_eq!(
815
+ proxies.client_ip(
816
+ Some(ip("10.0.0.5")),
817
+ Some(&format!("6.6.6.6, {VISITOR}")),
818
+ None
819
+ ),
820
+ Some(ip(VISITOR))
821
+ );
622
822
  }
623
823
 
624
824
  #[test]
625
825
  fn a_spoofed_leading_entry_is_never_reached() {
626
826
  let proxies = trust(&["10.0.0.0/8"], 0, None);
627
827
  let forwarded = format!("6.6.6.6, 7.7.7.7, {VISITOR}, 10.0.0.9");
628
- assert_eq!(proxies.client_ip(Some(ip("10.0.0.5")), Some(&forwarded), None), Some(ip(VISITOR)));
828
+ assert_eq!(
829
+ proxies.client_ip(Some(ip("10.0.0.5")), Some(&forwarded), None),
830
+ Some(ip(VISITOR))
831
+ );
629
832
  }
630
833
 
631
834
  #[test]
632
835
  fn a_visitor_cannot_pick_their_key_by_varying_the_spoofed_part() {
633
836
  let proxies = trust(&["10.0.0.0/8"], 0, None);
634
- let key = |spoof: &str| proxies.client_ip(Some(ip("10.0.0.5")), Some(&format!("{spoof}, {VISITOR}")), None);
837
+ let key = |spoof: &str| {
838
+ proxies.client_ip(
839
+ Some(ip("10.0.0.5")),
840
+ Some(&format!("{spoof}, {VISITOR}")),
841
+ None,
842
+ )
843
+ };
635
844
  assert_eq!(key("1.1.1.1"), key("2.2.2.2"));
636
845
  }
637
846
 
@@ -639,68 +848,121 @@ mod tests {
639
848
  fn trusted_proxies_inside_the_header_are_skipped_from_the_right() {
640
849
  let proxies = trust(&["10.0.0.0/8", "172.16.0.0/12"], 0, None);
641
850
  let forwarded = format!("6.6.6.6, {VISITOR}, 172.16.0.3, 10.0.0.9");
642
- assert_eq!(proxies.client_ip(Some(ip("10.0.0.5")), Some(&forwarded), None), Some(ip(VISITOR)));
851
+ assert_eq!(
852
+ proxies.client_ip(Some(ip("10.0.0.5")), Some(&forwarded), None),
853
+ Some(ip(VISITOR))
854
+ );
643
855
  }
644
856
 
645
857
  #[test]
646
858
  fn the_shared_secret_authenticates_a_request_from_an_unlisted_peer() {
647
859
  let cdn = trust(&[], 1, Some(("x-origin-secret", "s3cret")));
648
860
  let forwarded = format!("6.6.6.6, {VISITOR}, {EDGE}");
649
- assert_eq!(cdn.client_ip(Some(ip("10.0.0.5")), Some(&forwarded), Some("s3cret")), Some(ip(VISITOR)));
650
- assert_eq!(cdn.client_ip(Some(ip("10.0.0.5")), Some(&forwarded), Some("wrong")), Some(ip("10.0.0.5")));
651
- assert_eq!(cdn.client_ip(Some(ip("10.0.0.5")), Some(&forwarded), None), Some(ip("10.0.0.5")));
652
- assert_eq!(cdn.client_ip(Some(ip("10.0.0.5")), Some(&forwarded), Some("s3cret-and-more")), Some(ip("10.0.0.5")));
861
+ assert_eq!(
862
+ cdn.client_ip(Some(ip("10.0.0.5")), Some(&forwarded), Some("s3cret")),
863
+ Some(ip(VISITOR))
864
+ );
865
+ assert_eq!(
866
+ cdn.client_ip(Some(ip("10.0.0.5")), Some(&forwarded), Some("wrong")),
867
+ Some(ip("10.0.0.5"))
868
+ );
869
+ assert_eq!(
870
+ cdn.client_ip(Some(ip("10.0.0.5")), Some(&forwarded), None),
871
+ Some(ip("10.0.0.5"))
872
+ );
873
+ assert_eq!(
874
+ cdn.client_ip(
875
+ Some(ip("10.0.0.5")),
876
+ Some(&forwarded),
877
+ Some("s3cret-and-more")
878
+ ),
879
+ Some(ip("10.0.0.5"))
880
+ );
653
881
  }
654
882
 
655
883
  #[test]
656
884
  fn hops_skip_the_rightmost_entries_added_by_unlisted_proxies() {
657
885
  let cdn = trust(&[], 1, Some(("x-origin-secret", "s")));
658
886
  let one = format!("6.6.6.6, 7.7.7.7, {VISITOR}, {EDGE}");
659
- assert_eq!(cdn.client_ip(None, Some(&one), Some("s")), Some(ip(VISITOR)));
887
+ assert_eq!(
888
+ cdn.client_ip(None, Some(&one), Some("s")),
889
+ Some(ip(VISITOR))
890
+ );
660
891
 
661
892
  let two = trust(&[], 2, Some(("x-origin-secret", "s")));
662
893
  let forwarded = format!("6.6.6.6, {VISITOR}, {EDGE}, 130.176.2.2");
663
- assert_eq!(two.client_ip(None, Some(&forwarded), Some("s")), Some(ip(VISITOR)));
894
+ assert_eq!(
895
+ two.client_ip(None, Some(&forwarded), Some("s")),
896
+ Some(ip(VISITOR))
897
+ );
664
898
  }
665
899
 
666
900
  #[test]
667
901
  fn a_header_shorter_than_the_walk_implies_yields_no_client() {
668
902
  let cdn = trust(&[], 1, Some(("x-origin-secret", "s")));
669
- assert_eq!(cdn.client_ip(Some(ip("127.0.0.1")), Some("6.6.6.6"), Some("s")), None);
903
+ assert_eq!(
904
+ cdn.client_ip(Some(ip("127.0.0.1")), Some("6.6.6.6"), Some("s")),
905
+ None
906
+ );
670
907
  let listed = trust(&["10.0.0.0/8"], 0, None);
671
- assert_eq!(listed.client_ip(Some(ip("10.0.0.5")), Some("10.0.0.9, 10.0.0.8"), None), None, "every entry is one of ours");
908
+ assert_eq!(
909
+ listed.client_ip(Some(ip("10.0.0.5")), Some("10.0.0.9, 10.0.0.8"), None),
910
+ None,
911
+ "every entry is one of ours"
912
+ );
672
913
  }
673
914
 
674
915
  #[test]
675
916
  fn a_client_entry_that_is_not_an_address_yields_no_client() {
676
917
  let proxies = trust(&["10.0.0.0/8"], 0, None);
677
- assert_eq!(proxies.client_ip(Some(ip("10.0.0.5")), Some("6.6.6.6, not-an-ip"), None), None);
918
+ assert_eq!(
919
+ proxies.client_ip(Some(ip("10.0.0.5")), Some("6.6.6.6, not-an-ip"), None),
920
+ None
921
+ );
678
922
  }
679
923
 
680
924
  #[test]
681
925
  fn a_trusted_peer_with_no_header_is_its_own_client() {
682
926
  let proxies = trust(&["127.0.0.1"], 0, None);
683
- assert_eq!(proxies.client_ip(Some(ip("127.0.0.1")), None, None), Some(ip("127.0.0.1")));
684
- assert_eq!(proxies.client_ip(Some(ip("127.0.0.1")), Some(" , "), None), Some(ip("127.0.0.1")));
927
+ assert_eq!(
928
+ proxies.client_ip(Some(ip("127.0.0.1")), None, None),
929
+ Some(ip("127.0.0.1"))
930
+ );
931
+ assert_eq!(
932
+ proxies.client_ip(Some(ip("127.0.0.1")), Some(" , "), None),
933
+ Some(ip("127.0.0.1"))
934
+ );
685
935
  }
686
936
 
687
937
  #[test]
688
938
  fn tolerates_spaces_empty_entries_and_ipv6() {
689
939
  let proxies = trust(&["10.0.0.0/8"], 0, None);
690
940
  let forwarded = " 9.9.9.9 ,, 2001:db8::1 , 10.0.0.9 ";
691
- assert_eq!(proxies.client_ip(Some(ip("10.0.0.5")), Some(forwarded), None), Some(ip("2001:db8::1")));
941
+ assert_eq!(
942
+ proxies.client_ip(Some(ip("10.0.0.5")), Some(forwarded), None),
943
+ Some(ip("2001:db8::1"))
944
+ );
692
945
  }
693
946
 
694
947
  #[test]
695
948
  fn a_mapped_peer_is_matched_as_its_ipv4_form() {
696
949
  let proxies = trust(&["10.0.0.0/8"], 0, None);
697
- assert_eq!(proxies.client_ip(Some(ip("::ffff:10.0.0.5")), Some(VISITOR), None), Some(ip(VISITOR)));
950
+ assert_eq!(
951
+ proxies.client_ip(Some(ip("::ffff:10.0.0.5")), Some(VISITOR), None),
952
+ Some(ip(VISITOR))
953
+ );
698
954
  }
699
955
 
700
956
  #[test]
701
957
  fn ipv6_callers_are_bucketed_by_their_64() {
702
- assert_eq!(bucket_key(ip("2001:db8:1:2:aaaa:bbbb:cccc:dddd")), bucket_key(ip("2001:db8:1:2::1")));
703
- assert_ne!(bucket_key(ip("2001:db8:1:2::1")), bucket_key(ip("2001:db8:1:3::1")));
958
+ assert_eq!(
959
+ bucket_key(ip("2001:db8:1:2:aaaa:bbbb:cccc:dddd")),
960
+ bucket_key(ip("2001:db8:1:2::1"))
961
+ );
962
+ assert_ne!(
963
+ bucket_key(ip("2001:db8:1:2::1")),
964
+ bucket_key(ip("2001:db8:1:3::1"))
965
+ );
704
966
  assert_eq!(bucket_key(ip("203.0.113.7")), "203.0.113.7");
705
967
  }
706
968
 
@@ -710,7 +972,14 @@ mod tests {
710
972
  assert!(warnings.is_empty());
711
973
  assert!(config.enabled);
712
974
  assert_eq!(config.window, HOUR);
713
- assert_eq!((config.limit("subscribe"), config.limit("register"), config.max_keys), (10, 15, 10_000));
975
+ assert_eq!(
976
+ (
977
+ config.limit("subscribe"),
978
+ config.limit("register"),
979
+ config.max_keys
980
+ ),
981
+ (10, 15, 10_000)
982
+ );
714
983
  assert_eq!(config.trust, ProxyTrust::default());
715
984
  }
716
985
 
@@ -728,14 +997,31 @@ mod tests {
728
997
  ]);
729
998
  assert!(warnings.is_empty(), "{warnings:?}");
730
999
  assert_eq!(config.window, Duration::from_secs(60));
731
- assert_eq!((config.limit("subscribe"), config.limit("register"), config.max_keys), (3, 4, 50));
732
- assert_eq!(config.trust, trust(&["10.0.0.0/8", "127.0.0.1"], 1, Some(("x-origin-secret", "s3cret"))));
1000
+ assert_eq!(
1001
+ (
1002
+ config.limit("subscribe"),
1003
+ config.limit("register"),
1004
+ config.max_keys
1005
+ ),
1006
+ (3, 4, 50)
1007
+ );
1008
+ assert_eq!(
1009
+ config.trust,
1010
+ trust(
1011
+ &["10.0.0.0/8", "127.0.0.1"],
1012
+ 1,
1013
+ Some(("x-origin-secret", "s3cret"))
1014
+ )
1015
+ );
733
1016
  }
734
1017
 
735
1018
  #[test]
736
1019
  fn off_values_turn_the_limits_off_and_anything_else_leaves_them_on() {
737
1020
  for off in ["off", "OFF", " false ", "0", "no", "disabled"] {
738
- assert!(!config_from(&[("HECKS_RATE_LIMIT", off)]).0.enabled, "{off:?}");
1021
+ assert!(
1022
+ !config_from(&[("HECKS_RATE_LIMIT", off)]).0.enabled,
1023
+ "{off:?}"
1024
+ );
739
1025
  }
740
1026
  for on in ["on", "true", "1", ""] {
741
1027
  assert!(config_from(&[("HECKS_RATE_LIMIT", on)]).0.enabled, "{on:?}");
@@ -751,7 +1037,14 @@ mod tests {
751
1037
  ("HECKS_TRUSTED_PROXY_HOPS", "two"),
752
1038
  ("HECKS_TRUSTED_PROXIES", "10.0.0.0/8, banana"),
753
1039
  ]);
754
- assert_eq!((config.limit("subscribe"), config.limit("register"), config.window), (10, 15, HOUR));
1040
+ assert_eq!(
1041
+ (
1042
+ config.limit("subscribe"),
1043
+ config.limit("register"),
1044
+ config.window
1045
+ ),
1046
+ (10, 15, HOUR)
1047
+ );
755
1048
  assert_eq!(config.trust.hops, 0);
756
1049
  assert_eq!(config.trust.networks.len(), 1);
757
1050
  assert_eq!(warnings.len(), 5, "{warnings:?}");
@@ -759,19 +1052,26 @@ mod tests {
759
1052
 
760
1053
  #[test]
761
1054
  fn proxy_authentication_needs_both_a_header_and_a_secret() {
762
- for pairs in [&[("HECKS_PROXY_AUTH_HEADER", "x-secret")][..], &[("HECKS_PROXY_AUTH_SECRET", "s")][..]] {
1055
+ for pairs in [
1056
+ &[("HECKS_PROXY_AUTH_HEADER", "x-secret")][..],
1057
+ &[("HECKS_PROXY_AUTH_SECRET", "s")][..],
1058
+ ] {
763
1059
  let (config, warnings) = config_from(pairs);
764
1060
  assert!(config.trust.auth.is_none());
765
1061
  assert_eq!(warnings.len(), 1);
766
1062
  }
767
- let (config, warnings) = config_from(&[("HECKS_PROXY_AUTH_HEADER", "not a header"), ("HECKS_PROXY_AUTH_SECRET", "s")]);
1063
+ let (config, warnings) = config_from(&[
1064
+ ("HECKS_PROXY_AUTH_HEADER", "not a header"),
1065
+ ("HECKS_PROXY_AUTH_SECRET", "s"),
1066
+ ]);
768
1067
  assert!(config.trust.auth.is_none());
769
1068
  assert_eq!(warnings.len(), 1);
770
1069
  }
771
1070
 
772
1071
  #[test]
773
1072
  fn only_the_public_write_routes_are_limited() {
774
- let name = |method, path| crate::extension::rate_rule_for(method, path).map(|rule| rule.name);
1073
+ let name =
1074
+ |method, path| crate::extension::rate_rule_for(method, path).map(|rule| rule.name);
775
1075
  assert_eq!(name("POST", "/newsletter/subscribers"), Some("subscribe"));
776
1076
  assert_eq!(name("POST", "/registrations"), Some("register"));
777
1077
  for (method, path) in [
@@ -786,31 +1086,68 @@ mod tests {
786
1086
  ("POST", "/members"),
787
1087
  ("POST", "/newsletter/subscribers/"),
788
1088
  ] {
789
- assert_eq!(crate::extension::rate_rule_for(method, path).map(|rule| rule.name), None, "{method} {path}");
1089
+ assert_eq!(
1090
+ crate::extension::rate_rule_for(method, path).map(|rule| rule.name),
1091
+ None,
1092
+ "{method} {path}"
1093
+ );
790
1094
  }
791
1095
  }
792
1096
 
793
1097
  #[test]
794
1098
  fn each_route_has_its_own_budget_per_client() {
795
- let limits = limits(&[("HECKS_RATE_LIMIT_SUBSCRIBE", "2"), ("HECKS_RATE_LIMIT_REGISTER", "1")]);
1099
+ let limits = limits(&[
1100
+ ("HECKS_RATE_LIMIT_SUBSCRIBE", "2"),
1101
+ ("HECKS_RATE_LIMIT_REGISTER", "1"),
1102
+ ]);
796
1103
  let (none, peer, now) = (HeaderMap::new(), Some(ip("198.51.100.4")), Instant::now());
797
1104
  let hit = |path: &str| limits.check_at("POST", path, &none, peer, now);
798
1105
 
799
1106
  assert_eq!(hit("/newsletter/subscribers"), Verdict::Allowed);
800
1107
  assert_eq!(hit("/newsletter/subscribers"), Verdict::Allowed);
801
- assert_eq!(hit("/newsletter/subscribers"), Verdict::Limited { retry_after_secs: 3600 });
802
- assert_eq!(hit("/registrations"), Verdict::Allowed, "subscribe being spent does not touch register");
803
- assert_eq!(hit("/registrations"), Verdict::Limited { retry_after_secs: 3600 });
1108
+ assert_eq!(
1109
+ hit("/newsletter/subscribers"),
1110
+ Verdict::Limited {
1111
+ retry_after_secs: 3600
1112
+ }
1113
+ );
1114
+ assert_eq!(
1115
+ hit("/registrations"),
1116
+ Verdict::Allowed,
1117
+ "subscribe being spent does not touch register"
1118
+ );
1119
+ assert_eq!(
1120
+ hit("/registrations"),
1121
+ Verdict::Limited {
1122
+ retry_after_secs: 3600
1123
+ }
1124
+ );
804
1125
  }
805
1126
 
806
1127
  #[test]
807
1128
  fn the_window_rolls_over() {
808
- let limits = limits(&[("HECKS_RATE_LIMIT_REGISTER", "1"), ("HECKS_RATE_LIMIT_WINDOW_SECONDS", "60")]);
1129
+ let limits = limits(&[
1130
+ ("HECKS_RATE_LIMIT_REGISTER", "1"),
1131
+ ("HECKS_RATE_LIMIT_WINDOW_SECONDS", "60"),
1132
+ ]);
809
1133
  let (none, peer, start) = (HeaderMap::new(), Some(ip("198.51.100.4")), Instant::now());
810
- let at = |secs: u64| limits.check_at("POST", "/registrations", &none, peer, start + Duration::from_secs(secs));
1134
+ let at = |secs: u64| {
1135
+ limits.check_at(
1136
+ "POST",
1137
+ "/registrations",
1138
+ &none,
1139
+ peer,
1140
+ start + Duration::from_secs(secs),
1141
+ )
1142
+ };
811
1143
 
812
1144
  assert_eq!(at(0), Verdict::Allowed);
813
- assert_eq!(at(10), Verdict::Limited { retry_after_secs: 50 });
1145
+ assert_eq!(
1146
+ at(10),
1147
+ Verdict::Limited {
1148
+ retry_after_secs: 50
1149
+ }
1150
+ );
814
1151
  assert_eq!(at(60), Verdict::Allowed);
815
1152
  }
816
1153
 
@@ -819,21 +1156,44 @@ mod tests {
819
1156
  let on = limits(&[("HECKS_RATE_LIMIT_SUBSCRIBE", "1")]);
820
1157
  let (none, peer, now) = (HeaderMap::new(), Some(ip("198.51.100.4")), Instant::now());
821
1158
  for _ in 0..5 {
822
- assert_eq!(on.check_at("GET", "/newsletter/subscribers", &none, peer, now), Verdict::Allowed);
823
- assert_eq!(on.check_at("POST", "/webhooks/stripe", &none, peer, now), Verdict::Allowed);
1159
+ assert_eq!(
1160
+ on.check_at("GET", "/newsletter/subscribers", &none, peer, now),
1161
+ Verdict::Allowed
1162
+ );
1163
+ assert_eq!(
1164
+ on.check_at("POST", "/webhooks/stripe", &none, peer, now),
1165
+ Verdict::Allowed
1166
+ );
824
1167
  }
825
1168
 
826
- let off = limits(&[("HECKS_RATE_LIMIT", "off"), ("HECKS_RATE_LIMIT_SUBSCRIBE", "1")]);
1169
+ let off = limits(&[
1170
+ ("HECKS_RATE_LIMIT", "off"),
1171
+ ("HECKS_RATE_LIMIT_SUBSCRIBE", "1"),
1172
+ ]);
827
1173
  for _ in 0..5 {
828
- assert_eq!(off.check_at("POST", "/newsletter/subscribers", &none, peer, now), Verdict::Allowed);
1174
+ assert_eq!(
1175
+ off.check_at("POST", "/newsletter/subscribers", &none, peer, now),
1176
+ Verdict::Allowed
1177
+ );
829
1178
  }
830
1179
  }
831
1180
 
832
1181
  #[test]
833
1182
  fn a_spoofed_header_from_an_untrusted_peer_does_not_reset_the_count() {
834
- let limits = limits(&[("HECKS_RATE_LIMIT_SUBSCRIBE", "1"), ("HECKS_TRUSTED_PROXIES", "10.0.0.0/8")]);
1183
+ let limits = limits(&[
1184
+ ("HECKS_RATE_LIMIT_SUBSCRIBE", "1"),
1185
+ ("HECKS_TRUSTED_PROXIES", "10.0.0.0/8"),
1186
+ ]);
835
1187
  let (peer, now) = (Some(ip("198.51.100.4")), Instant::now());
836
- let send = |spoof: &str| limits.check_at("POST", "/newsletter/subscribers", &headers(&[("x-forwarded-for", spoof)]), peer, now);
1188
+ let send = |spoof: &str| {
1189
+ limits.check_at(
1190
+ "POST",
1191
+ "/newsletter/subscribers",
1192
+ &headers(&[("x-forwarded-for", spoof)]),
1193
+ peer,
1194
+ now,
1195
+ )
1196
+ };
837
1197
 
838
1198
  assert_eq!(send("1.1.1.1"), Verdict::Allowed);
839
1199
  assert!(matches!(send("2.2.2.2"), Verdict::Limited { .. }));
@@ -850,31 +1210,65 @@ mod tests {
850
1210
  let (alb, now) = (Some(ip("10.0.0.5")), Instant::now());
851
1211
  let from = |spoof: &str, visitor: &str| {
852
1212
  let forwarded = format!("{spoof}, {visitor}, {EDGE}");
853
- limits.check_at("POST", "/registrations", &headers(&[("x-origin-secret", "s3cret"), ("x-forwarded-for", &forwarded)]), alb, now)
1213
+ limits.check_at(
1214
+ "POST",
1215
+ "/registrations",
1216
+ &headers(&[
1217
+ ("x-origin-secret", "s3cret"),
1218
+ ("x-forwarded-for", &forwarded),
1219
+ ]),
1220
+ alb,
1221
+ now,
1222
+ )
854
1223
  };
855
1224
 
856
1225
  assert_eq!(from("1.1.1.1", VISITOR), Verdict::Allowed);
857
- assert!(matches!(from("2.2.2.2", VISITOR), Verdict::Limited { .. }), "a new spoofed entry is not a new visitor");
858
- assert_eq!(from("2.2.2.2", "198.51.100.4"), Verdict::Allowed, "another visitor has their own count");
1226
+ assert!(
1227
+ matches!(from("2.2.2.2", VISITOR), Verdict::Limited { .. }),
1228
+ "a new spoofed entry is not a new visitor"
1229
+ );
1230
+ assert_eq!(
1231
+ from("2.2.2.2", "198.51.100.4"),
1232
+ Verdict::Allowed,
1233
+ "another visitor has their own count"
1234
+ );
859
1235
  }
860
1236
 
861
1237
  #[test]
862
1238
  fn requests_whose_client_cannot_be_determined_share_one_bucket() {
863
- let limits = limits(&[("HECKS_RATE_LIMIT_REGISTER", "1"), ("HECKS_TRUSTED_PROXIES", "10.0.0.0/8")]);
1239
+ let limits = limits(&[
1240
+ ("HECKS_RATE_LIMIT_REGISTER", "1"),
1241
+ ("HECKS_TRUSTED_PROXIES", "10.0.0.0/8"),
1242
+ ]);
864
1243
  let now = Instant::now();
865
1244
  let short = headers(&[("x-forwarded-for", "10.0.0.9")]);
866
- assert_eq!(limits.check_at("POST", "/registrations", &short, Some(ip("10.0.0.5")), now), Verdict::Allowed);
867
- assert!(matches!(limits.check_at("POST", "/registrations", &short, Some(ip("10.0.0.6")), now), Verdict::Limited { .. }));
1245
+ assert_eq!(
1246
+ limits.check_at("POST", "/registrations", &short, Some(ip("10.0.0.5")), now),
1247
+ Verdict::Allowed
1248
+ );
1249
+ assert!(matches!(
1250
+ limits.check_at("POST", "/registrations", &short, Some(ip("10.0.0.6")), now),
1251
+ Verdict::Limited { .. }
1252
+ ));
868
1253
  }
869
1254
 
870
1255
  #[test]
871
1256
  fn repeated_forwarded_header_lines_read_as_one_list() {
872
- let limits = limits(&[("HECKS_RATE_LIMIT_REGISTER", "1"), ("HECKS_TRUSTED_PROXIES", "10.0.0.0/8")]);
1257
+ let limits = limits(&[
1258
+ ("HECKS_RATE_LIMIT_REGISTER", "1"),
1259
+ ("HECKS_TRUSTED_PROXIES", "10.0.0.0/8"),
1260
+ ]);
873
1261
  let (peer, now) = (Some(ip("10.0.0.5")), Instant::now());
874
1262
  let split = headers(&[("x-forwarded-for", "6.6.6.6"), ("x-forwarded-for", VISITOR)]);
875
1263
  let joined = headers(&[("x-forwarded-for", &format!("9.9.9.9, {VISITOR}"))]);
876
- assert_eq!(limits.check_at("POST", "/registrations", &split, peer, now), Verdict::Allowed);
877
- assert!(matches!(limits.check_at("POST", "/registrations", &joined, peer, now), Verdict::Limited { .. }));
1264
+ assert_eq!(
1265
+ limits.check_at("POST", "/registrations", &split, peer, now),
1266
+ Verdict::Allowed
1267
+ );
1268
+ assert!(matches!(
1269
+ limits.check_at("POST", "/registrations", &joined, peer, now),
1270
+ Verdict::Limited { .. }
1271
+ ));
878
1272
  }
879
1273
 
880
1274
  #[tokio::test]
@@ -883,8 +1277,13 @@ mod tests {
883
1277
  assert_eq!(response.status(), StatusCode::TOO_MANY_REQUESTS);
884
1278
  assert_eq!(response.headers()[RETRY_AFTER], "120");
885
1279
  assert_eq!(response.headers()[CONTENT_TYPE], "application/json");
886
- let bytes = axum::body::to_bytes(response.into_body(), 4096).await.unwrap();
1280
+ let bytes = axum::body::to_bytes(response.into_body(), 4096)
1281
+ .await
1282
+ .unwrap();
887
1283
  let body: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
888
- assert_eq!(body, json!({ "error": "too many requests, please try again later" }));
1284
+ assert_eq!(
1285
+ body,
1286
+ json!({ "error": "too many requests, please try again later" })
1287
+ );
889
1288
  }
890
1289
  }