hecks 3.11.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 (85) hide show
  1. checksums.yaml +4 -4
  2. data/lib/hecks/adapters/driving/cli_runner/resolution.rb +6 -3
  3. data/lib/hecks/adapters/driving/cli_runner.rb +1 -0
  4. data/lib/hecks/adapters/driving/newcomer.rb +50 -0
  5. data/lib/hecks/bluebook/assembly/contracts/read_models.rb +1 -1
  6. data/lib/hecks/bluebook/assembly/marks.rb +3 -15
  7. data/lib/hecks/bluebook/assembly/scalars.rb +39 -0
  8. data/lib/hecks/bluebook/meta_validator/readings.rb +6 -0
  9. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +6 -3
  10. data/lib/hecks/bluebook.rb +1 -0
  11. data/lib/hecks/deploy/bluebook/deploy.bluebook +16 -2
  12. data/lib/hecks/gate/stages.yml +32 -0
  13. data/lib/hecks/hecks/adapters/rehearsal/postgres_store.rb +5 -1
  14. data/lib/hecks/hecks/hecks.world +3 -0
  15. data/lib/hecks/language/bluebook/bluebook.behaviors +23 -0
  16. data/lib/hecks/language/bluebook/policy.behaviors +19 -0
  17. data/lib/hecks/language/bluebook/vocabulary.bluebook +52 -0
  18. data/lib/hecks/projection_files/expression_tables.rb +7 -2
  19. data/lib/hecks/projections/deploy/box/compose.rb +6 -4
  20. data/lib/hecks/projections/deploy/box/edge.rb +62 -0
  21. data/lib/hecks/projections/deploy/box/logging.rb +99 -0
  22. data/lib/hecks/projections/deploy/box/roll_scripts.rb +16 -8
  23. data/lib/hecks/projections/deploy/box/settings/cdn.rb +40 -0
  24. data/lib/hecks/projections/deploy/box/settings/logs.rb +40 -0
  25. data/lib/hecks/projections/deploy/box/settings/patterns.rb +11 -0
  26. data/lib/hecks/projections/deploy/box/settings/types.rb +16 -1
  27. data/lib/hecks/projections/deploy/box/settings.rb +6 -2
  28. data/lib/hecks/projections/deploy/box/stack_yaml.rb +5 -2
  29. data/lib/hecks/projections/deploy/box/templates/Makefile.tmpl +16 -0
  30. data/lib/hecks/projections/deploy/box/templates/box.yaml.tmpl +8 -4
  31. data/lib/hecks/projections/deploy/box/templates/deploy-box.sh.tmpl +3 -0
  32. data/lib/hecks/projections/deploy/box/templates/edge.mk.tmpl +17 -0
  33. data/lib/hecks/projections/deploy/box/templates/edge.yaml.tmpl +65 -0
  34. data/lib/hecks/projections/deploy/box/templates/log-group.yaml.tmpl +12 -0
  35. data/lib/hecks/projections/deploy/box/templates/log-policy.yaml.tmpl +7 -0
  36. data/lib/hecks/projections/deploy/box/templates/render-compose-taskdef.sh.tmpl +5 -4
  37. data/lib/hecks/projections/deploy/box/templates/render-compose.sh.tmpl +5 -4
  38. data/lib/hecks/projections/deploy/box.rb +15 -3
  39. data/lib/hecks/projections/site/site_root/workflow.rb +5 -0
  40. data/lib/hecks/projector/cli_audience.rb +20 -0
  41. data/lib/hecks/projector/cli_projector/usage.rb +15 -4
  42. data/lib/hecks/rust_build/host.rb +11 -4
  43. data/lib/hecks/rust_build/prebuilt_binary.rb +101 -0
  44. data/lib/hecks/tools/ci_gate_decision.rb +35 -0
  45. data/lib/hecks/tools/ci_gates.rb +20 -1
  46. data/lib/hecks/tools/lanes/files.rb +8 -0
  47. data/lib/hecks/tools/lanes/prebuilt_workflow.rb +81 -0
  48. data/lib/hecks/tools/lanes.rb +1 -0
  49. data/lib/hecks/tools/release_only.rb +111 -0
  50. data/lib/hecks/version.rb +1 -1
  51. data/lib/hecks/vocabulary.rb +15 -1
  52. data/prebuilt/9ee54e2f0d4d6d3fe046827779b221e02de665fba911a048ae5dd8b8fbfb7d90.marshal +0 -0
  53. data/prebuilt/d74a00e996222c1d8b676d5959092effc46b4dc59e0555f3bf9554ac58e3094a.marshal +0 -0
  54. data/prebuilt/{verdicts-230ac18c25abfd3d92ede032bee2661a4c2cd2773ab0cf0e2ae52546ba2f7af9.json → verdicts-df19f35868d3284a70f55e42dcef87ef4e65e6a678e5135f72e1f6cff4c1357b.json} +1 -1
  55. data/rust/host/HECKS_RELEASE +1 -1
  56. data/rust/host/src/extension.rs +13 -1
  57. data/rust/host/src/lib.rs +1 -0
  58. data/rust/host/src/platform_signin/attack_tests.rs +1100 -0
  59. data/rust/host/src/platform_signin/config.rs +512 -0
  60. data/rust/host/src/platform_signin/cookie.rs +237 -0
  61. data/rust/host/src/platform_signin/extension.rs +190 -0
  62. data/rust/host/src/platform_signin/fetcher.rs +322 -0
  63. data/rust/host/src/platform_signin/handler.rs +251 -0
  64. data/rust/host/src/platform_signin/handler_tests.rs +554 -0
  65. data/rust/host/src/platform_signin/jti.rs +196 -0
  66. data/rust/host/src/platform_signin/jwks.rs +542 -0
  67. data/rust/host/src/platform_signin/jws.rs +249 -0
  68. data/rust/host/src/platform_signin/known_answer_tests.rs +237 -0
  69. data/rust/host/src/platform_signin/mapping_tests.rs +254 -0
  70. data/rust/host/src/platform_signin/mod.rs +75 -0
  71. data/rust/host/src/platform_signin/mode_tests.rs +382 -0
  72. data/rust/host/src/platform_signin/pages.rs +204 -0
  73. data/rust/host/src/platform_signin/query.rs +233 -0
  74. data/rust/host/src/platform_signin/session.rs +151 -0
  75. data/rust/host/src/platform_signin/settings.rs +385 -0
  76. data/rust/host/src/platform_signin/strict_json.rs +351 -0
  77. data/rust/host/src/platform_signin/test_support.rs +235 -0
  78. data/rust/host/src/platform_signin/ttl_tests.rs +94 -0
  79. data/rust/host/src/platform_signin/verify.rs +604 -0
  80. data/rust/host/src/platform_signin/wiring_support.rs +319 -0
  81. data/rust/host/src/web.rs +50 -6
  82. data/rust/parser/src/parse/value_object.rs +22 -10
  83. metadata +42 -4
  84. data/prebuilt/05c358d38d661a3774f813d49c66f5b68939af94432d66257a56f49ee2598994.marshal +0 -0
  85. data/prebuilt/26c40c61f09b09b11651c64668eda3ceb180f83cf2a3802d3033d2fd99ac841b.marshal +0 -0
@@ -0,0 +1,196 @@
1
+ //! The single-use record of token identifiers.
2
+ //!
3
+ //! A token may be redeemed once. The store remembers each `jti` until its time window has
4
+ //! closed (`exp` plus the skew), so a replay inside that window is refused and
5
+ //! nothing is kept longer than it must be.
6
+ //!
7
+ //! ## Per process, on purpose
8
+ //!
9
+ //! The record lives in this process's memory. A restart forgets it, which leaves a replay window of
10
+ //! at most the token's remaining life plus the skew (90 seconds with the default settings). More
11
+ //! than one running instance would each have their own record, so a token could be redeemed once
12
+ //! per instance. This store is therefore only right for a site served by a single long-running
13
+ //! process; a deployment with several instances or short-lived invocations needs a shared record.
14
+ //!
15
+ //! ## Bounded, and closed when full
16
+ //!
17
+ //! At the cap the store refuses new claims rather than grow, so a flood of valid-looking tokens
18
+ //! cannot exhaust memory. A refusal is reported like a replay: nobody gets in on a full store.
19
+
20
+ use std::collections::HashMap;
21
+ use std::sync::Mutex;
22
+
23
+ /// The default cap on remembered identifiers.
24
+ pub const DEFAULT_MAX_ENTRIES: usize = 100_000;
25
+
26
+ /// An in-memory, bounded, atomic set of spent token identifiers.
27
+ pub struct JtiStore {
28
+ max_entries: usize,
29
+ seen: Mutex<HashMap<String, i64>>,
30
+ }
31
+
32
+ impl JtiStore {
33
+ /// Builds an empty store with the default cap.
34
+ pub fn new() -> Self {
35
+ Self::with_capacity(DEFAULT_MAX_ENTRIES)
36
+ }
37
+
38
+ /// Builds an empty store that remembers at most `max_entries` identifiers.
39
+ pub fn with_capacity(max_entries: usize) -> Self {
40
+ Self {
41
+ max_entries,
42
+ seen: Mutex::new(HashMap::new()),
43
+ }
44
+ }
45
+
46
+ /// Records `jti` as spent until `expires_at` (Unix seconds) unless it is already remembered.
47
+ ///
48
+ /// Returns `true` the first time, and `false` while the identifier is remembered or the store
49
+ /// is full. Checking and recording are one step under one lock, so of any number of concurrent
50
+ /// claims of the same identifier exactly one wins.
51
+ pub fn claim(&self, jti: &str, expires_at: i64, now: i64) -> bool {
52
+ let mut seen = self.lock();
53
+ seen.retain(|_, expiry| *expiry > now);
54
+ if seen.contains_key(jti) || seen.len() >= self.max_entries {
55
+ return false;
56
+ }
57
+ seen.insert(jti.to_string(), expires_at);
58
+ true
59
+ }
60
+
61
+ fn lock(&self) -> std::sync::MutexGuard<'_, HashMap<String, i64>> {
62
+ // A panic in another thread cannot leave the map half-updated, and refusing everything
63
+ // forever would be a lock-out, so a poisoned lock is simply taken over.
64
+ self.seen
65
+ .lock()
66
+ .unwrap_or_else(|poisoned| poisoned.into_inner())
67
+ }
68
+
69
+ /// Counts the identifiers still remembered at `now`; expired ones are not counted.
70
+ pub fn len(&self, now: i64) -> usize {
71
+ self.lock().values().filter(|expiry| **expiry > now).count()
72
+ }
73
+
74
+ /// Tells whether nothing is remembered at `now`.
75
+ pub fn is_empty(&self, now: i64) -> bool {
76
+ self.len(now) == 0
77
+ }
78
+ }
79
+
80
+ impl Default for JtiStore {
81
+ fn default() -> Self {
82
+ Self::new()
83
+ }
84
+ }
85
+
86
+ #[cfg(test)]
87
+ mod tests {
88
+ use super::*;
89
+ use std::sync::atomic::{AtomicUsize, Ordering};
90
+ use std::sync::{Arc, Barrier};
91
+
92
+ const NOW: i64 = 1_800_000_000;
93
+
94
+ #[test]
95
+ fn an_identifier_is_claimed_once() {
96
+ let store = JtiStore::new();
97
+ assert!(store.claim("a", NOW + 90, NOW));
98
+ assert!(!store.claim("a", NOW + 90, NOW));
99
+ assert!(store.claim("b", NOW + 90, NOW));
100
+ assert_eq!(store.len(NOW), 2);
101
+ }
102
+
103
+ #[test]
104
+ fn an_entry_is_remembered_until_its_expiry_and_forgotten_at_it() {
105
+ let store = JtiStore::new();
106
+ assert!(store.claim("a", NOW + 90, NOW));
107
+ assert!(!store.claim("a", NOW + 90, NOW + 89));
108
+ assert_eq!(store.len(NOW + 89), 1);
109
+ assert!(
110
+ store.claim("a", NOW + 200, NOW + 90),
111
+ "forgotten exactly at expires_at"
112
+ );
113
+ assert_eq!(store.len(NOW + 90), 1);
114
+ }
115
+
116
+ #[test]
117
+ fn expired_entries_are_not_counted_and_free_room() {
118
+ let store = JtiStore::with_capacity(2);
119
+ assert!(store.claim("a", NOW + 10, NOW));
120
+ assert!(store.claim("b", NOW + 10, NOW));
121
+ assert!(
122
+ !store.claim("c", NOW + 100, NOW + 5),
123
+ "full while both are live"
124
+ );
125
+ assert!(
126
+ store.claim("c", NOW + 100, NOW + 10),
127
+ "expired entries make room"
128
+ );
129
+ assert_eq!(store.len(NOW + 10), 1);
130
+ assert!(store.is_empty(NOW + 100));
131
+ }
132
+
133
+ #[test]
134
+ fn the_store_is_bounded_and_fails_closed_at_the_cap() {
135
+ let store = JtiStore::with_capacity(3);
136
+ for id in ["a", "b", "c"] {
137
+ assert!(store.claim(id, NOW + 90, NOW));
138
+ }
139
+ assert!(
140
+ !store.claim("d", NOW + 90, NOW),
141
+ "a new identifier is refused, not stored"
142
+ );
143
+ assert_eq!(store.len(NOW), 3);
144
+ assert!(
145
+ !store.claim("a", NOW + 90, NOW),
146
+ "and an old one is still a replay"
147
+ );
148
+ }
149
+
150
+ #[test]
151
+ fn a_refused_claim_records_nothing() {
152
+ let store = JtiStore::with_capacity(1);
153
+ assert!(store.claim("a", NOW + 10, NOW));
154
+ assert!(!store.claim("b", NOW + 10, NOW));
155
+ assert!(
156
+ store.claim("b", NOW + 100, NOW + 10),
157
+ "b was never remembered"
158
+ );
159
+ }
160
+
161
+ #[test]
162
+ fn concurrent_claims_of_one_identifier_have_exactly_one_winner() {
163
+ const THREADS: usize = 16;
164
+ for round in 0..300 {
165
+ let store = Arc::new(JtiStore::new());
166
+ let gate = Arc::new(Barrier::new(THREADS));
167
+ let winners = Arc::new(AtomicUsize::new(0));
168
+ let handles: Vec<_> = (0..THREADS)
169
+ .map(|_| {
170
+ let (store, gate, winners) = (store.clone(), gate.clone(), winners.clone());
171
+ std::thread::spawn(move || {
172
+ gate.wait();
173
+ if store.claim("same-jti", NOW + 90, NOW) {
174
+ winners.fetch_add(1, Ordering::SeqCst);
175
+ }
176
+ })
177
+ })
178
+ .collect();
179
+ handles.into_iter().for_each(|h| h.join().unwrap());
180
+ assert_eq!(winners.load(Ordering::SeqCst), 1, "round {round}");
181
+ }
182
+ }
183
+
184
+ #[test]
185
+ fn a_poisoned_lock_does_not_turn_into_an_open_door() {
186
+ let store = Arc::new(JtiStore::new());
187
+ let clone = store.clone();
188
+ let _ = std::thread::spawn(move || {
189
+ let _guard = clone.seen.lock().unwrap();
190
+ panic!("poison the lock");
191
+ })
192
+ .join();
193
+ assert!(store.claim("a", NOW + 90, NOW));
194
+ assert!(!store.claim("a", NOW + 90, NOW));
195
+ }
196
+ }