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,319 @@
1
+ //! Shared scaffolding for the wiring tests: settings, a recording log, a fake member directory,
2
+ //! and the known-answer vectors turned into callback requests.
3
+
4
+ use std::collections::HashMap;
5
+ use std::sync::{Arc, Mutex};
6
+
7
+ use async_trait::async_trait;
8
+ use serde_json::{json, Value};
9
+
10
+ use super::config::PlatformSigninConfig;
11
+ use super::handler::{CallbackRequest, Random, SigninHandler, SigninLog};
12
+ use super::jti::JtiStore;
13
+ use super::jwks::parse_jwks;
14
+ use super::session::MemberDirectory;
15
+ use super::settings::SigninSettings;
16
+ use super::test_support::StaticKeys;
17
+ use super::verify::Verifier;
18
+ use crate::auth::Session;
19
+
20
+ pub const SITE_URL: &str = "https://site.example";
21
+ pub const SECRET: &str = "a-long-session-secret-for-tests";
22
+ pub const ISSUER: &str = "https://issuer.example";
23
+ pub const NONCE_A: &str = "NonceNonceNonce0123456789abcdefghijklmn";
24
+ pub const STATE_A: &str = "StateStateState0123456789abcdefghijklmn";
25
+ pub const NONCE_B: &str = "NonceBNonceBNonceB0123456789abcdefghijk";
26
+ pub const STATE_B: &str = "StateBStateBStateB0123456789abcdefghijk";
27
+
28
+ /// The environment map a site with `mode` switched on would have.
29
+ pub fn env_map(mode: &str) -> HashMap<String, String> {
30
+ [
31
+ ("PLATFORM_SIGNIN", mode),
32
+ ("PLATFORM_ISSUER_URL", ISSUER),
33
+ ("PLATFORM_SITE_ID", "site-a"),
34
+ ("PLATFORM_ENVIRONMENT", "production"),
35
+ ("SITE_URL", SITE_URL),
36
+ ("SESSION_SECRET", SECRET),
37
+ ("HECKS_SERVE_MODE", "1"),
38
+ ]
39
+ .into_iter()
40
+ .map(|(k, v)| (k.to_string(), v.to_string()))
41
+ .collect()
42
+ }
43
+
44
+ /// Validated settings for `mode`.
45
+ pub fn settings(mode: &str) -> SigninSettings {
46
+ SigninSettings::from_env_map(&env_map(mode))
47
+ .expect("valid")
48
+ .expect("switched on")
49
+ }
50
+
51
+ /// A log that keeps every event.
52
+ #[derive(Default)]
53
+ pub struct Recorder(Mutex<Vec<(String, Value)>>);
54
+
55
+ impl SigninLog for Recorder {
56
+ fn record(&self, event: &str, fields: Value) {
57
+ self.0.lock().unwrap().push((event.to_string(), fields));
58
+ }
59
+ }
60
+
61
+ impl Recorder {
62
+ /// Every recorded event as one text, for searching.
63
+ pub fn text(&self) -> String {
64
+ self.0
65
+ .lock()
66
+ .unwrap()
67
+ .iter()
68
+ .map(|(e, f)| format!("{e} {f}"))
69
+ .collect::<Vec<_>>()
70
+ .join("\n")
71
+ }
72
+
73
+ /// The names of the recorded events, in order.
74
+ pub fn events(&self) -> Vec<String> {
75
+ self.0
76
+ .lock()
77
+ .unwrap()
78
+ .iter()
79
+ .map(|(e, _)| e.clone())
80
+ .collect()
81
+ }
82
+
83
+ /// The fields of the first event called `name`.
84
+ pub fn fields_of(&self, name: &str) -> Option<Value> {
85
+ self.0
86
+ .lock()
87
+ .unwrap()
88
+ .iter()
89
+ .find(|(e, _)| e == name)
90
+ .map(|(_, f)| f.clone())
91
+ }
92
+ }
93
+
94
+ /// A person the fake store knows.
95
+ #[derive(Clone)]
96
+ pub struct Person {
97
+ pub email: String,
98
+ pub role: Option<String>,
99
+ }
100
+
101
+ impl Person {
102
+ /// Builds a person with this email and local role.
103
+ pub fn new(email: &str, role: Option<&str>) -> Self {
104
+ Self {
105
+ email: email.into(),
106
+ role: role.map(str::to_string),
107
+ }
108
+ }
109
+
110
+ fn session(&self) -> Session {
111
+ Session {
112
+ identity_id: "identity-1".into(),
113
+ email: self.email.clone(),
114
+ name: "Person".into(),
115
+ role: self.role.clone(),
116
+ }
117
+ }
118
+ }
119
+
120
+ /// A member store with scripted answers that remembers what it was asked.
121
+ #[derive(Default)]
122
+ pub struct FakeMembers {
123
+ /// `(issuer, subject)` -> identity id.
124
+ pub identities: HashMap<(String, String), String>,
125
+ /// identity id -> the linked member.
126
+ pub linked: HashMap<String, Person>,
127
+ /// What linking by email answers.
128
+ pub provisions: Option<Person>,
129
+ pub fail: bool,
130
+ pub calls: Mutex<Vec<String>>,
131
+ }
132
+
133
+ impl FakeMembers {
134
+ /// A store where the vectors' identity resolves to `person`.
135
+ pub fn resolving(person: Person) -> Self {
136
+ let mut store = Self::default();
137
+ store.identities.insert(
138
+ ("https://idp.example".into(), "idp-subject-0001".into()),
139
+ "identity-1".into(),
140
+ );
141
+ store.linked.insert("identity-1".into(), person);
142
+ store
143
+ }
144
+
145
+ /// A store where nothing resolves and linking by email yields `person`.
146
+ pub fn provisioning(person: Person) -> Self {
147
+ Self {
148
+ provisions: Some(person),
149
+ ..Self::default()
150
+ }
151
+ }
152
+
153
+ /// Lists the calls made so far, in order.
154
+ pub fn called(&self) -> Vec<String> {
155
+ self.calls.lock().unwrap().clone()
156
+ }
157
+
158
+ fn note(&self, call: &str) -> anyhow::Result<()> {
159
+ self.calls.lock().unwrap().push(call.to_string());
160
+ if self.fail {
161
+ anyhow::bail!("the store is down");
162
+ }
163
+ Ok(())
164
+ }
165
+ }
166
+
167
+ #[async_trait]
168
+ impl MemberDirectory for FakeMembers {
169
+ async fn identity_for(&self, issuer: &str, subject: &str) -> anyhow::Result<Option<String>> {
170
+ self.note("identity_for")?;
171
+ Ok(self
172
+ .identities
173
+ .get(&(issuer.to_string(), subject.to_string()))
174
+ .cloned())
175
+ }
176
+
177
+ async fn session_for_identity(&self, identity_id: &str) -> anyhow::Result<Option<Session>> {
178
+ self.note("session_for_identity")?;
179
+ Ok(self.linked.get(identity_id).map(Person::session))
180
+ }
181
+
182
+ async fn provision(
183
+ &self,
184
+ email: &str,
185
+ issuer: &str,
186
+ subject: &str,
187
+ ) -> anyhow::Result<Option<Session>> {
188
+ self.note(&format!("provision {email} {issuer} {subject}"))?;
189
+ Ok(self.provisions.as_ref().map(Person::session))
190
+ }
191
+ }
192
+
193
+ /// The known-answer file.
194
+ pub fn vectors() -> Value {
195
+ let path = concat!(
196
+ env!("CARGO_MANIFEST_DIR"),
197
+ "/tests/fixtures/platform_signin_vectors.json"
198
+ );
199
+ serde_json::from_slice(&std::fs::read(path).expect("vectors")).expect("json")
200
+ }
201
+
202
+ /// A verifier that trusts the vectors' key set and pins its issuer, site and environment.
203
+ pub fn vector_verifier(doc: &Value) -> Verifier<StaticKeys> {
204
+ let pins = &doc["config"];
205
+ let text = |name: &str| pins[name].as_str().expect("string").to_string();
206
+ let config = PlatformSigninConfig::new(
207
+ &text("iss"),
208
+ &format!("{}/.well-known/jwks.json", text("iss")),
209
+ &text("aud"),
210
+ &text("env"),
211
+ );
212
+ let parsed = parse_jwks(&doc["jwks"].to_string()).expect("key set");
213
+ Verifier::new(config, StaticKeys(parsed.keys), JtiStore::new())
214
+ }
215
+
216
+ /// Percent-encodes everything outside the unreserved set.
217
+ pub fn encode(text: &str) -> String {
218
+ text.bytes()
219
+ .map(|b| {
220
+ if b.is_ascii_alphanumeric() || matches!(b, b'-' | b'_' | b'.' | b'~') {
221
+ (b as char).to_string()
222
+ } else {
223
+ format!("%{b:02X}")
224
+ }
225
+ })
226
+ .collect()
227
+ }
228
+
229
+ /// The callback request a browser would make for one vector step.
230
+ pub fn request_for(step: &Value) -> CallbackRequest {
231
+ let mut query = Vec::new();
232
+ if let Some(token) = step["token"].as_str() {
233
+ query.push(format!("token={}", encode(token)));
234
+ }
235
+ if let Some(state) = step["state"].as_str() {
236
+ query.push(format!("state={}", encode(state)));
237
+ }
238
+ let cookie_pairs = match step.get("cookie").filter(|c| !c.is_null()) {
239
+ None => vec![],
240
+ Some(cookie) => {
241
+ let part = |n: &str| cookie.get(n).and_then(Value::as_str);
242
+ let value = match part("state") {
243
+ Some(state) => format!("{}.{state}", part("nonce").unwrap_or("")),
244
+ None => part("nonce").unwrap_or("").to_string(),
245
+ };
246
+ vec![format!("__Host-signin={value}")]
247
+ }
248
+ };
249
+ CallbackRequest {
250
+ raw_query: query.join("&"),
251
+ cookie_pairs,
252
+ }
253
+ }
254
+
255
+ /// A random source that hands out `values` in order.
256
+ pub fn scripted_random(values: &[&str]) -> Random {
257
+ let queue = Mutex::new(
258
+ values
259
+ .iter()
260
+ .map(|v| v.to_string())
261
+ .collect::<std::collections::VecDeque<_>>(),
262
+ );
263
+ Box::new(move || queue.lock().unwrap().pop_front().expect("a scripted value"))
264
+ }
265
+
266
+ /// A handler over the vectors' verifier with a fixed clock and a recording log.
267
+ pub fn vector_handler(mode: &str, log: Arc<Recorder>) -> SigninHandler {
268
+ let doc = vectors();
269
+ let now = doc["now"].as_i64().expect("now");
270
+ SigninHandler::new(settings(mode), Arc::new(vector_verifier(&doc)), log)
271
+ .with_clock(Box::new(move || now))
272
+ }
273
+
274
+ /// The verified claims of an accepted vector.
275
+ pub fn vector_claims(doc: &Value, name: &str) -> super::verify::VerifiedClaims {
276
+ use super::verify::{SigninCookie, SigninRequest};
277
+ let step = single(doc, name);
278
+ let cookie = SigninCookie {
279
+ nonce: step["cookie"]["nonce"].as_str().unwrap().to_string(),
280
+ state: step["cookie"]["state"].as_str().unwrap().to_string(),
281
+ };
282
+ let request = SigninRequest {
283
+ token: step["token"].as_str(),
284
+ state: step["state"].as_str(),
285
+ cookie: Some(&cookie),
286
+ };
287
+ vector_verifier(doc)
288
+ .verify(&request, doc["now"].as_i64().unwrap())
289
+ .expect("an accepted vector")
290
+ }
291
+
292
+ /// The cookies of a response.
293
+ pub fn cookies_of(response: &Value) -> Vec<String> {
294
+ response
295
+ .get("cookies")
296
+ .and_then(Value::as_array)
297
+ .map(|a| {
298
+ a.iter()
299
+ .filter_map(|c| c.as_str().map(str::to_string))
300
+ .collect()
301
+ })
302
+ .unwrap_or_default()
303
+ }
304
+
305
+ /// The vector with this name.
306
+ pub fn single(doc: &Value, name: &str) -> Value {
307
+ doc["vectors"]
308
+ .as_array()
309
+ .unwrap()
310
+ .iter()
311
+ .find(|v| v["name"] == name)
312
+ .unwrap_or_else(|| panic!("vector {name}"))
313
+ .clone()
314
+ }
315
+
316
+ #[allow(dead_code)]
317
+ fn _unused() -> Value {
318
+ json!({})
319
+ }
data/rust/host/src/web.rs CHANGED
@@ -1686,17 +1686,29 @@ fn redirect(location: &str) -> Value {
1686
1686
  json!({"statusCode": 302, "headers": {"location": location}, "body": "", "isBase64Encoded": false})
1687
1687
  }
1688
1688
 
1689
- // HttpOnly + SameSite always; Secure only when GOOGLE_REDIRECT_URI is
1690
- // https — a localhost http:// callback can't set a Secure cookie.
1689
+ // HttpOnly + SameSite always; Secure unless the host's own address is plain http — a localhost
1690
+ // http:// callback can't set a Secure cookie. See `account_cookie_flags` for which address counts.
1691
1691
  fn cookie_flags() -> &'static str {
1692
- let uri = std::env::var("GOOGLE_REDIRECT_URI").unwrap_or_default();
1693
- if uri.starts_with("http://") {
1694
- "; Path=/; HttpOnly; SameSite=Lax"
1692
+ let redirect_uri = std::env::var("GOOGLE_REDIRECT_URI").unwrap_or_default();
1693
+ let site_url = std::env::var("SITE_URL").unwrap_or_default();
1694
+ account_cookie_flags(&redirect_uri, &site_url)
1695
+ }
1696
+
1697
+ // The flags for the account cookie given the Google redirect URI and the site's public address.
1698
+ // A host that sets the redirect URI is judged by it, as ever; a host that has stopped setting it
1699
+ // is judged by SITE_URL, and with neither set the cookie is Secure.
1700
+ pub(crate) fn account_cookie_flags(redirect_uri: &str, site_url: &str) -> &'static str {
1701
+ let judged_by = if redirect_uri.is_empty() { site_url } else { redirect_uri };
1702
+ if judged_by.starts_with("http://") {
1703
+ COOKIE_FLAGS_PLAIN
1695
1704
  } else {
1696
- "; Path=/; HttpOnly; Secure; SameSite=Lax"
1705
+ COOKIE_FLAGS_SECURE
1697
1706
  }
1698
1707
  }
1699
1708
 
1709
+ pub(crate) const COOKIE_FLAGS_SECURE: &str = "; Path=/; HttpOnly; Secure; SameSite=Lax";
1710
+ const COOKIE_FLAGS_PLAIN: &str = "; Path=/; HttpOnly; SameSite=Lax";
1711
+
1700
1712
  fn redirect_with_cookie(location: &str, cookie: &str) -> Value {
1701
1713
  json!({"statusCode": 302, "headers": {"location": location}, "cookies": [cookie], "body": "", "isBase64Encoded": false})
1702
1714
  }
@@ -2985,6 +2997,38 @@ mod tests {
2985
2997
 
2986
2998
  }
2987
2999
 
3000
+ #[cfg(test)]
3001
+ mod cookie_flag_tests {
3002
+ use super::account_cookie_flags;
3003
+
3004
+ const SECURE: &str = "; Path=/; HttpOnly; Secure; SameSite=Lax";
3005
+ const PLAIN: &str = "; Path=/; HttpOnly; SameSite=Lax";
3006
+
3007
+ #[test]
3008
+ fn a_host_that_sets_the_redirect_uri_is_judged_by_it_as_before() {
3009
+ assert_eq!(account_cookie_flags("https://site.example/auth/google/callback", ""), SECURE);
3010
+ assert_eq!(account_cookie_flags("http://localhost:4567/auth/google/callback", ""), PLAIN);
3011
+ assert_eq!(account_cookie_flags("https://site.example/cb", "http://localhost:4321"), SECURE);
3012
+ assert_eq!(account_cookie_flags("http://localhost:4567/cb", "https://site.example"), PLAIN);
3013
+ }
3014
+
3015
+ #[test]
3016
+ fn a_host_with_no_redirect_uri_and_an_https_site_url_still_sets_secure() {
3017
+ assert_eq!(account_cookie_flags("", "https://site.example"), SECURE);
3018
+ assert_eq!(account_cookie_flags("", "https://site.example/"), SECURE);
3019
+ }
3020
+
3021
+ #[test]
3022
+ fn a_host_with_no_redirect_uri_and_an_http_site_url_does_not_demand_secure() {
3023
+ assert_eq!(account_cookie_flags("", "http://localhost:4321"), PLAIN);
3024
+ }
3025
+
3026
+ #[test]
3027
+ fn a_host_with_neither_setting_stays_secure() {
3028
+ assert_eq!(account_cookie_flags("", ""), SECURE);
3029
+ }
3030
+ }
3031
+
2988
3032
  #[cfg(test)]
2989
3033
  #[path = "boundary_fuzz/web.rs"]
2990
3034
  mod boundary_fuzz;
@@ -140,7 +140,7 @@ pub fn parse_body(
140
140
  fn build_value_object_attribute(
141
141
  file: &str,
142
142
  args: &super::ArgumentGateResult,
143
- ) -> ParseResult<(ir::Attribute, Option<Vec<String>>, Option<ir::ValueObject>)> {
143
+ ) -> ParseResult<(ir::Attribute, Option<Vec<ruby_value::Value>>, Option<ir::ValueObject>)> {
144
144
  let (attribute, closed_set) = super::build_attribute(file, 0, "attribute", args)?;
145
145
  let one_of = super::named_raw(args, "one_of").map(|raw| {
146
146
  let trimmed = raw.trim();
@@ -150,7 +150,7 @@ fn build_value_object_attribute(
150
150
  .unwrap_or(trimmed);
151
151
  ruby_value::split_items(inner)
152
152
  .into_iter()
153
- .map(|segment| ruby_value::to_s(&ruby_value::read(segment.trim())))
153
+ .map(|segment| cell_value(segment.trim()))
154
154
  .collect()
155
155
  });
156
156
  Ok((attribute, one_of, closed_set))
@@ -164,7 +164,7 @@ fn install_inline_closed_set(
164
164
  vo_name: &str,
165
165
  vo: &mut ir::ValueObject,
166
166
  field: &str,
167
- values: &[String],
167
+ values: &[ruby_value::Value],
168
168
  ) -> ParseResult<()> {
169
169
  if !vo.attributes.is_empty() {
170
170
  return Err(Diagnostic::new(
@@ -182,11 +182,11 @@ fn install_inline_closed_set(
182
182
  }
183
183
 
184
184
  vo.closed_set = true;
185
- // Unmarked like a bare `member` row (`push_member`): Ruby round-trips these through
186
- // `Marks#member`, so `true`/`false` export as JSON booleans, not strings.
185
+ // Read like a `member` row (`push_member`): a quoted value stays a String and a bare
186
+ // `true`, `false` or number keeps its type, as Ruby's builder holds them.
187
187
  vo.members = values
188
188
  .iter()
189
- .map(|value| vec![(field.to_string(), unmark_scalar(value))])
189
+ .map(|value| vec![(field.to_string(), value.clone())])
190
190
  .collect();
191
191
  Ok(())
192
192
  }
@@ -207,15 +207,27 @@ fn push_member(
207
207
  let fields = args
208
208
  .named
209
209
  .iter()
210
- .map(|(field, raw)| (field.clone(), unmark_scalar(&ruby_value::to_s(&ruby_value::read(raw)))))
210
+ .map(|(field, raw)| (field.clone(), cell_value(raw)))
211
211
  .collect();
212
212
  members.push(fields);
213
213
  Ok(())
214
214
  }
215
215
 
216
- /// Port of `Marks#unmark_scalar` (lib/hecks/bluebook/assembly/marks.rb).
217
- /// Ruby stores every member field as text and infers Bool/Integer/Float from its shape, so a
218
- /// quoted "true" or "1" unmarks to a real `true` or `1`.
216
+ /// Reads one member cell from its source spelling, as `Scalars.unmark` reads a stored one
217
+ /// (lib/hecks/bluebook/assembly/scalars.rb): a quoted cell is a String whatever it spells, and a
218
+ /// bare boolean or number keeps its type. Anything else is read through its text.
219
+ fn cell_value(raw: &str) -> ruby_value::Value {
220
+ match ruby_value::read(raw) {
221
+ held @ (ruby_value::Value::Str(_)
222
+ | ruby_value::Value::Bool(_)
223
+ | ruby_value::Value::Int(_)
224
+ | ruby_value::Value::Float(_)) => held,
225
+ other => unmark_scalar(&ruby_value::to_s(&other)),
226
+ }
227
+ }
228
+
229
+ /// The text-shape half of `Scalars.unmark`: for a cell that is neither a quoted String nor a
230
+ /// bare boolean or number, `true`, `false` and number-shaped text read back typed.
219
231
  fn unmark_scalar(text: &str) -> ruby_value::Value {
220
232
  if text == "true" {
221
233
  return ruby_value::Value::Bool(true);
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hecks
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.11.0
4
+ version: 3.12.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chris Young
@@ -145,6 +145,7 @@ files:
145
145
  - lib/hecks/adapters/driving/mcp_scope/argument_policy.rb
146
146
  - lib/hecks/adapters/driving/mcp_scope/env_problems.rb
147
147
  - lib/hecks/adapters/driving/mcp_scope/presenter.rb
148
+ - lib/hecks/adapters/driving/newcomer.rb
148
149
  - lib/hecks/adapters/driving/ruby.rb
149
150
  - lib/hecks/adapters/driving/ruby/aggregate_module.rb
150
151
  - lib/hecks/adapters/driving/ruby/chapter.rb
@@ -183,6 +184,7 @@ files:
183
184
  - lib/hecks/bluebook/assembly/contracts/sagas.rb
184
185
  - lib/hecks/bluebook/assembly/contracts/structure.rb
185
186
  - lib/hecks/bluebook/assembly/marks.rb
187
+ - lib/hecks/bluebook/assembly/scalars.rb
186
188
  - lib/hecks/bluebook/assembly/specializer.rb
187
189
  - lib/hecks/bluebook/attribute.rb
188
190
  - lib/hecks/bluebook/behaviour/aggregate.rb
@@ -699,10 +701,12 @@ files:
699
701
  - lib/hecks/language/adapter.bluebook
700
702
  - lib/hecks/language/bluebook/aggregate.bluebook
701
703
  - lib/hecks/language/bluebook/attaches/paging.bluebook
704
+ - lib/hecks/language/bluebook/bluebook.behaviors
702
705
  - lib/hecks/language/bluebook/bluebook.bluebook
703
706
  - lib/hecks/language/bluebook/bluebook.hecksagon
704
707
  - lib/hecks/language/bluebook/command.bluebook
705
708
  - lib/hecks/language/bluebook/entity.bluebook
709
+ - lib/hecks/language/bluebook/policy.behaviors
706
710
  - lib/hecks/language/bluebook/policy.bluebook
707
711
  - lib/hecks/language/bluebook/process_manager.bluebook
708
712
  - lib/hecks/language/bluebook/projection.bluebook
@@ -837,14 +841,18 @@ files:
837
841
  - lib/hecks/projections/deploy/box/access.rb
838
842
  - lib/hecks/projections/deploy/box/caddy.rb
839
843
  - lib/hecks/projections/deploy/box/compose.rb
844
+ - lib/hecks/projections/deploy/box/edge.rb
840
845
  - lib/hecks/projections/deploy/box/hosting.rb
841
846
  - lib/hecks/projections/deploy/box/hosting_parts.rb
842
847
  - lib/hecks/projections/deploy/box/hosting_settings.rb
848
+ - lib/hecks/projections/deploy/box/logging.rb
843
849
  - lib/hecks/projections/deploy/box/roll_recipe.rb
844
850
  - lib/hecks/projections/deploy/box/roll_scripts.rb
845
851
  - lib/hecks/projections/deploy/box/settings.rb
852
+ - lib/hecks/projections/deploy/box/settings/cdn.rb
846
853
  - lib/hecks/projections/deploy/box/settings/checks.rb
847
854
  - lib/hecks/projections/deploy/box/settings/containers.rb
855
+ - lib/hecks/projections/deploy/box/settings/logs.rb
848
856
  - lib/hecks/projections/deploy/box/settings/origin.rb
849
857
  - lib/hecks/projections/deploy/box/settings/patterns.rb
850
858
  - lib/hecks/projections/deploy/box/settings/types.rb
@@ -861,8 +869,12 @@ files:
861
869
  - lib/hecks/projections/deploy/box/templates/deploy-usage-services.tmpl
862
870
  - lib/hecks/projections/deploy/box/templates/deploy-usage-taskdef.tmpl
863
871
  - lib/hecks/projections/deploy/box/templates/ecr-repository.yaml.tmpl
872
+ - lib/hecks/projections/deploy/box/templates/edge.mk.tmpl
873
+ - lib/hecks/projections/deploy/box/templates/edge.yaml.tmpl
864
874
  - lib/hecks/projections/deploy/box/templates/fetch-secrets.sh
865
875
  - lib/hecks/projections/deploy/box/templates/hosting.mk.tmpl
876
+ - lib/hecks/projections/deploy/box/templates/log-group.yaml.tmpl
877
+ - lib/hecks/projections/deploy/box/templates/log-policy.yaml.tmpl
866
878
  - lib/hecks/projections/deploy/box/templates/mode-summary-services.tmpl
867
879
  - lib/hecks/projections/deploy/box/templates/mode-summary-taskdef.tmpl
868
880
  - lib/hecks/projections/deploy/box/templates/provision-database.sh.tmpl
@@ -1481,6 +1493,7 @@ files:
1481
1493
  - lib/hecks/rust_build/host.rb
1482
1494
  - lib/hecks/rust_build/kernel_input.rb
1483
1495
  - lib/hecks/rust_build/native_build.rb
1496
+ - lib/hecks/rust_build/prebuilt_binary.rb
1484
1497
  - lib/hecks/rust_build/project_rust.rb
1485
1498
  - lib/hecks/rust_build/project_rust/codegen_run.rb
1486
1499
  - lib/hecks/rust_build/project_rust/domain_loader.rb
@@ -1559,6 +1572,7 @@ files:
1559
1572
  - lib/hecks/tools/lanes.rb
1560
1573
  - lib/hecks/tools/lanes/files.rb
1561
1574
  - lib/hecks/tools/lanes/live.rb
1575
+ - lib/hecks/tools/lanes/prebuilt_workflow.rb
1562
1576
  - lib/hecks/tools/lanes/rows.rb
1563
1577
  - lib/hecks/tools/lanes/ruleset.rb
1564
1578
  - lib/hecks/tools/lanes/watch_workflow.rb
@@ -1567,6 +1581,7 @@ files:
1567
1581
  - lib/hecks/tools/oidc_manifests.rb
1568
1582
  - lib/hecks/tools/query_ir_run.rb
1569
1583
  - lib/hecks/tools/regeneration_run.rb
1584
+ - lib/hecks/tools/release_only.rb
1570
1585
  - lib/hecks/tools/rust_comment_style.rb
1571
1586
  - lib/hecks/tools/rust_comment_style/caps_words.rb
1572
1587
  - lib/hecks/tools/rust_comment_style/fixing.rb
@@ -1607,9 +1622,9 @@ files:
1607
1622
  - lib/rubocop/cop/hecks/symbol_string_key_mix.rb
1608
1623
  - lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb
1609
1624
  - lib/rubocop/cop/hecks/untyped_raise.rb
1610
- - prebuilt/05c358d38d661a3774f813d49c66f5b68939af94432d66257a56f49ee2598994.marshal
1611
- - prebuilt/26c40c61f09b09b11651c64668eda3ceb180f83cf2a3802d3033d2fd99ac841b.marshal
1612
- - prebuilt/verdicts-230ac18c25abfd3d92ede032bee2661a4c2cd2773ab0cf0e2ae52546ba2f7af9.json
1625
+ - prebuilt/9ee54e2f0d4d6d3fe046827779b221e02de665fba911a048ae5dd8b8fbfb7d90.marshal
1626
+ - prebuilt/d74a00e996222c1d8b676d5959092effc46b4dc59e0555f3bf9554ac58e3094a.marshal
1627
+ - prebuilt/verdicts-df19f35868d3284a70f55e42dcef87ef4e65e6a678e5135f72e1f6cff4c1357b.json
1613
1628
  - qa/settings.yml
1614
1629
  - rust/.cargo/config.toml
1615
1630
  - rust/.cargo/mutants.toml
@@ -1706,6 +1721,29 @@ files:
1706
1721
  - rust/host/src/payments.rs
1707
1722
  - rust/host/src/payments/keystore.rs
1708
1723
  - rust/host/src/payments/tests.rs
1724
+ - rust/host/src/platform_signin/attack_tests.rs
1725
+ - rust/host/src/platform_signin/config.rs
1726
+ - rust/host/src/platform_signin/cookie.rs
1727
+ - rust/host/src/platform_signin/extension.rs
1728
+ - rust/host/src/platform_signin/fetcher.rs
1729
+ - rust/host/src/platform_signin/handler.rs
1730
+ - rust/host/src/platform_signin/handler_tests.rs
1731
+ - rust/host/src/platform_signin/jti.rs
1732
+ - rust/host/src/platform_signin/jwks.rs
1733
+ - rust/host/src/platform_signin/jws.rs
1734
+ - rust/host/src/platform_signin/known_answer_tests.rs
1735
+ - rust/host/src/platform_signin/mapping_tests.rs
1736
+ - rust/host/src/platform_signin/mod.rs
1737
+ - rust/host/src/platform_signin/mode_tests.rs
1738
+ - rust/host/src/platform_signin/pages.rs
1739
+ - rust/host/src/platform_signin/query.rs
1740
+ - rust/host/src/platform_signin/session.rs
1741
+ - rust/host/src/platform_signin/settings.rs
1742
+ - rust/host/src/platform_signin/strict_json.rs
1743
+ - rust/host/src/platform_signin/test_support.rs
1744
+ - rust/host/src/platform_signin/ttl_tests.rs
1745
+ - rust/host/src/platform_signin/verify.rs
1746
+ - rust/host/src/platform_signin/wiring_support.rs
1709
1747
  - rust/host/src/presentation.rs
1710
1748
  - rust/host/src/presentation_write.rs
1711
1749
  - rust/host/src/query_step.rs