hecks 3.11.0 → 3.13.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 (118) hide show
  1. checksums.yaml +4 -4
  2. data/lib/hecks/adapters/driven/script_runner.adapter +3 -0
  3. data/lib/hecks/adapters/driven/script_runner.rb +93 -0
  4. data/lib/hecks/adapters/driven.rb +1 -0
  5. data/lib/hecks/adapters/driving/cli_runner/resolution.rb +6 -3
  6. data/lib/hecks/adapters/driving/cli_runner.rb +1 -0
  7. data/lib/hecks/adapters/driving/newcomer.rb +50 -0
  8. data/lib/hecks/bluebook/assembly/contracts/read_models.rb +1 -1
  9. data/lib/hecks/bluebook/assembly/marks.rb +3 -15
  10. data/lib/hecks/bluebook/assembly/scalars.rb +39 -0
  11. data/lib/hecks/bluebook/meta_validator/readings.rb +6 -0
  12. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +6 -3
  13. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +16 -12
  14. data/lib/hecks/bluebook/meta_validator.rb +28 -21
  15. data/lib/hecks/bluebook.rb +1 -0
  16. data/lib/hecks/deploy/bluebook/deploy.bluebook +17 -3
  17. data/lib/hecks/framework/bluebook/change.behaviors +30 -0
  18. data/lib/hecks/framework/bluebook/change.bluebook +162 -0
  19. data/lib/hecks/framework/bluebook/change.hecksagon +21 -0
  20. data/lib/hecks/framework/change.oidc.json +34 -0
  21. data/lib/hecks/framework/console_settings.oidc.json +103 -0
  22. data/lib/hecks/framework/governance.oidc.json +28 -0
  23. data/lib/hecks/framework/identity.oidc.json +18 -0
  24. data/lib/hecks/framework/privacy.oidc.json +30 -0
  25. data/lib/hecks/gate/stages.yml +35 -0
  26. data/lib/hecks/grammar/translation.oidc.json +48 -0
  27. data/lib/hecks/hecks/adapters/codebase/test_runner.rb +12 -1
  28. data/lib/hecks/hecks/adapters/deploy_toolchain.rb +2 -1
  29. data/lib/hecks/hecks/adapters/rehearsal/postgres_store.rb +5 -1
  30. data/lib/hecks/hecks/hecks.world +3 -0
  31. data/lib/hecks/language/bluebook/bluebook.behaviors +23 -0
  32. data/lib/hecks/language/bluebook/policy.behaviors +19 -0
  33. data/lib/hecks/language/bluebook/vocabulary.bluebook +52 -0
  34. data/lib/hecks/projection_files/expression_tables.rb +7 -2
  35. data/lib/hecks/projections/deploy/box/compose.rb +6 -4
  36. data/lib/hecks/projections/deploy/box/edge.rb +62 -0
  37. data/lib/hecks/projections/deploy/box/logging.rb +99 -0
  38. data/lib/hecks/projections/deploy/box/roll_scripts.rb +16 -8
  39. data/lib/hecks/projections/deploy/box/settings/cdn.rb +40 -0
  40. data/lib/hecks/projections/deploy/box/settings/logs.rb +40 -0
  41. data/lib/hecks/projections/deploy/box/settings/patterns.rb +11 -0
  42. data/lib/hecks/projections/deploy/box/settings/types.rb +16 -1
  43. data/lib/hecks/projections/deploy/box/settings.rb +6 -2
  44. data/lib/hecks/projections/deploy/box/stack_yaml.rb +5 -2
  45. data/lib/hecks/projections/deploy/box/templates/Makefile.tmpl +16 -0
  46. data/lib/hecks/projections/deploy/box/templates/box.yaml.tmpl +8 -4
  47. data/lib/hecks/projections/deploy/box/templates/deploy-box.sh.tmpl +3 -0
  48. data/lib/hecks/projections/deploy/box/templates/edge.mk.tmpl +17 -0
  49. data/lib/hecks/projections/deploy/box/templates/edge.yaml.tmpl +65 -0
  50. data/lib/hecks/projections/deploy/box/templates/log-group.yaml.tmpl +12 -0
  51. data/lib/hecks/projections/deploy/box/templates/log-policy.yaml.tmpl +7 -0
  52. data/lib/hecks/projections/deploy/box/templates/render-compose-taskdef.sh.tmpl +5 -4
  53. data/lib/hecks/projections/deploy/box/templates/render-compose.sh.tmpl +5 -4
  54. data/lib/hecks/projections/deploy/box.rb +15 -3
  55. data/lib/hecks/projections/deploy/fargate/cdn/lines.rb +12 -1
  56. data/lib/hecks/projections/site/admin_cms/templates/membership.rb +10 -7
  57. data/lib/hecks/projections/site/edge/behaviours.rb +10 -6
  58. data/lib/hecks/projections/site/edge/checks.rb +2 -0
  59. data/lib/hecks/projections/site/edge/function_checks.rb +104 -0
  60. data/lib/hecks/projections/site/edge/function_code.rb +86 -0
  61. data/lib/hecks/projections/site/edge/redirect_host.js.tmpl +65 -0
  62. data/lib/hecks/projections/site/edge.rb +35 -9
  63. data/lib/hecks/projections/site/site_cdn.rb +21 -7
  64. data/lib/hecks/projections/site/site_root/workflow.rb +5 -0
  65. data/lib/hecks/projections/site/table/row_builder.rb +12 -2
  66. data/lib/hecks/projections/site/table.rb +4 -2
  67. data/lib/hecks/projector/cli_audience.rb +20 -0
  68. data/lib/hecks/projector/cli_projector/usage.rb +15 -4
  69. data/lib/hecks/rust_build/host.rb +11 -4
  70. data/lib/hecks/rust_build/prebuilt_binary.rb +101 -0
  71. data/lib/hecks/site/bluebook/site.bluebook +16 -1
  72. data/lib/hecks/tools/ci_gate_decision.rb +35 -0
  73. data/lib/hecks/tools/ci_gates.rb +20 -1
  74. data/lib/hecks/tools/lanes/files.rb +8 -0
  75. data/lib/hecks/tools/lanes/prebuilt_workflow.rb +81 -0
  76. data/lib/hecks/tools/lanes.rb +1 -0
  77. data/lib/hecks/tools/oidc_manifests.rb +44 -6
  78. data/lib/hecks/tools/release_only.rb +111 -0
  79. data/lib/hecks/tools/site_routes/templates.rb +13 -9
  80. data/lib/hecks/version.rb +1 -1
  81. data/lib/hecks/vocabulary.rb +15 -1
  82. data/prebuilt/2b59737fbdbd5f44edda0fddce2611f45d76087b3a7c782d1f1a17ea58c77948.marshal +0 -0
  83. data/prebuilt/58208399bada10b0db99928c48e38789288fc58f7b71a72b21bfa386680e32ed.marshal +0 -0
  84. data/prebuilt/{verdicts-230ac18c25abfd3d92ede032bee2661a4c2cd2773ab0cf0e2ae52546ba2f7af9.json → verdicts-48318ca172edd21443b1ad02801c697fff100b746e828f09db987647641a5df2.json} +1 -1
  85. data/rust/host/HECKS_RELEASE +1 -1
  86. data/rust/host/src/auth.rs +58 -0
  87. data/rust/host/src/extension.rs +13 -1
  88. data/rust/host/src/lib.rs +1 -0
  89. data/rust/host/src/platform_signin/attack_tests.rs +1100 -0
  90. data/rust/host/src/platform_signin/config.rs +512 -0
  91. data/rust/host/src/platform_signin/cookie.rs +237 -0
  92. data/rust/host/src/platform_signin/extension.rs +190 -0
  93. data/rust/host/src/platform_signin/fetcher.rs +322 -0
  94. data/rust/host/src/platform_signin/handler.rs +251 -0
  95. data/rust/host/src/platform_signin/handler_tests.rs +554 -0
  96. data/rust/host/src/platform_signin/jti.rs +196 -0
  97. data/rust/host/src/platform_signin/jwks.rs +542 -0
  98. data/rust/host/src/platform_signin/jws.rs +249 -0
  99. data/rust/host/src/platform_signin/known_answer_tests.rs +237 -0
  100. data/rust/host/src/platform_signin/mapping_tests.rs +254 -0
  101. data/rust/host/src/platform_signin/mod.rs +75 -0
  102. data/rust/host/src/platform_signin/mode_tests.rs +382 -0
  103. data/rust/host/src/platform_signin/pages.rs +204 -0
  104. data/rust/host/src/platform_signin/query.rs +233 -0
  105. data/rust/host/src/platform_signin/session.rs +151 -0
  106. data/rust/host/src/platform_signin/settings.rs +385 -0
  107. data/rust/host/src/platform_signin/strict_json.rs +351 -0
  108. data/rust/host/src/platform_signin/test_support.rs +235 -0
  109. data/rust/host/src/platform_signin/ttl_tests.rs +94 -0
  110. data/rust/host/src/platform_signin/verify.rs +604 -0
  111. data/rust/host/src/platform_signin/wiring_support.rs +319 -0
  112. data/rust/host/src/web.rs +50 -6
  113. data/rust/parser/src/parse/value_object.rs +22 -10
  114. metadata +59 -7
  115. data/prebuilt/05c358d38d661a3774f813d49c66f5b68939af94432d66257a56f49ee2598994.marshal +0 -0
  116. data/prebuilt/26c40c61f09b09b11651c64668eda3ceb180f83cf2a3802d3033d2fd99ac841b.marshal +0 -0
  117. /data/lib/hecks/framework/{oidc.json → compliance.oidc.json} +0 -0
  118. /data/lib/hecks/grammar/{oidc.json → expression.oidc.json} +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,14 +1,14 @@
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.13.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chris Young
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-10-16 00:00:00.000000000 Z
11
+ date: 2026-10-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: prism
@@ -97,6 +97,8 @@ files:
97
97
  - lib/hecks/adapters/driven/postgres_era.adapter
98
98
  - lib/hecks/adapters/driven/prism.adapter
99
99
  - lib/hecks/adapters/driven/prism.rb
100
+ - lib/hecks/adapters/driven/script_runner.adapter
101
+ - lib/hecks/adapters/driven/script_runner.rb
100
102
  - lib/hecks/adapters/driven/secure_random_identity.adapter
101
103
  - lib/hecks/adapters/driven/secure_random_identity.rb
102
104
  - lib/hecks/adapters/driven/sql_query_builder.rb
@@ -145,6 +147,7 @@ files:
145
147
  - lib/hecks/adapters/driving/mcp_scope/argument_policy.rb
146
148
  - lib/hecks/adapters/driving/mcp_scope/env_problems.rb
147
149
  - lib/hecks/adapters/driving/mcp_scope/presenter.rb
150
+ - lib/hecks/adapters/driving/newcomer.rb
148
151
  - lib/hecks/adapters/driving/ruby.rb
149
152
  - lib/hecks/adapters/driving/ruby/aggregate_module.rb
150
153
  - lib/hecks/adapters/driving/ruby/chapter.rb
@@ -183,6 +186,7 @@ files:
183
186
  - lib/hecks/bluebook/assembly/contracts/sagas.rb
184
187
  - lib/hecks/bluebook/assembly/contracts/structure.rb
185
188
  - lib/hecks/bluebook/assembly/marks.rb
189
+ - lib/hecks/bluebook/assembly/scalars.rb
186
190
  - lib/hecks/bluebook/assembly/specializer.rb
187
191
  - lib/hecks/bluebook/attribute.rb
188
192
  - lib/hecks/bluebook/behaviour/aggregate.rb
@@ -452,13 +456,21 @@ files:
452
456
  - lib/hecks/forms/value_object_shape.rb
453
457
  - lib/hecks/fqn.rb
454
458
  - lib/hecks/framework.rb
459
+ - lib/hecks/framework/bluebook/change.behaviors
460
+ - lib/hecks/framework/bluebook/change.bluebook
461
+ - lib/hecks/framework/bluebook/change.hecksagon
455
462
  - lib/hecks/framework/bluebook/compliance.bluebook
456
463
  - lib/hecks/framework/bluebook/console_settings.bluebook
457
464
  - lib/hecks/framework/bluebook/framework.hecksagon
458
465
  - lib/hecks/framework/bluebook/governance.bluebook
459
466
  - lib/hecks/framework/bluebook/identity.bluebook
460
467
  - lib/hecks/framework/bluebook/privacy.bluebook
461
- - lib/hecks/framework/oidc.json
468
+ - lib/hecks/framework/change.oidc.json
469
+ - lib/hecks/framework/compliance.oidc.json
470
+ - lib/hecks/framework/console_settings.oidc.json
471
+ - lib/hecks/framework/governance.oidc.json
472
+ - lib/hecks/framework/identity.oidc.json
473
+ - lib/hecks/framework/privacy.oidc.json
462
474
  - lib/hecks/freezer.rb
463
475
  - lib/hecks/fuzzing.rb
464
476
  - lib/hecks/fuzzing/bounded_exhaustive_expressions.rb
@@ -572,11 +584,12 @@ files:
572
584
  - lib/hecks/grammar/evolve/keywords.rb
573
585
  - lib/hecks/grammar/evolve/seeds.rb
574
586
  - lib/hecks/grammar/expression.bluebook
587
+ - lib/hecks/grammar/expression.oidc.json
575
588
  - lib/hecks/grammar/expression_operators.json
576
589
  - lib/hecks/grammar/grammar.hecksagon
577
- - lib/hecks/grammar/oidc.json
578
590
  - lib/hecks/grammar/operators.rb
579
591
  - lib/hecks/grammar/translation.bluebook
592
+ - lib/hecks/grammar/translation.oidc.json
580
593
  - lib/hecks/hecks/adapters/bluebook_report.rb
581
594
  - lib/hecks/hecks/adapters/codebase/codemods.rb
582
595
  - lib/hecks/hecks/adapters/codebase/conformance.rb
@@ -699,10 +712,12 @@ files:
699
712
  - lib/hecks/language/adapter.bluebook
700
713
  - lib/hecks/language/bluebook/aggregate.bluebook
701
714
  - lib/hecks/language/bluebook/attaches/paging.bluebook
715
+ - lib/hecks/language/bluebook/bluebook.behaviors
702
716
  - lib/hecks/language/bluebook/bluebook.bluebook
703
717
  - lib/hecks/language/bluebook/bluebook.hecksagon
704
718
  - lib/hecks/language/bluebook/command.bluebook
705
719
  - lib/hecks/language/bluebook/entity.bluebook
720
+ - lib/hecks/language/bluebook/policy.behaviors
706
721
  - lib/hecks/language/bluebook/policy.bluebook
707
722
  - lib/hecks/language/bluebook/process_manager.bluebook
708
723
  - lib/hecks/language/bluebook/projection.bluebook
@@ -837,14 +852,18 @@ files:
837
852
  - lib/hecks/projections/deploy/box/access.rb
838
853
  - lib/hecks/projections/deploy/box/caddy.rb
839
854
  - lib/hecks/projections/deploy/box/compose.rb
855
+ - lib/hecks/projections/deploy/box/edge.rb
840
856
  - lib/hecks/projections/deploy/box/hosting.rb
841
857
  - lib/hecks/projections/deploy/box/hosting_parts.rb
842
858
  - lib/hecks/projections/deploy/box/hosting_settings.rb
859
+ - lib/hecks/projections/deploy/box/logging.rb
843
860
  - lib/hecks/projections/deploy/box/roll_recipe.rb
844
861
  - lib/hecks/projections/deploy/box/roll_scripts.rb
845
862
  - lib/hecks/projections/deploy/box/settings.rb
863
+ - lib/hecks/projections/deploy/box/settings/cdn.rb
846
864
  - lib/hecks/projections/deploy/box/settings/checks.rb
847
865
  - lib/hecks/projections/deploy/box/settings/containers.rb
866
+ - lib/hecks/projections/deploy/box/settings/logs.rb
848
867
  - lib/hecks/projections/deploy/box/settings/origin.rb
849
868
  - lib/hecks/projections/deploy/box/settings/patterns.rb
850
869
  - lib/hecks/projections/deploy/box/settings/types.rb
@@ -861,8 +880,12 @@ files:
861
880
  - lib/hecks/projections/deploy/box/templates/deploy-usage-services.tmpl
862
881
  - lib/hecks/projections/deploy/box/templates/deploy-usage-taskdef.tmpl
863
882
  - lib/hecks/projections/deploy/box/templates/ecr-repository.yaml.tmpl
883
+ - lib/hecks/projections/deploy/box/templates/edge.mk.tmpl
884
+ - lib/hecks/projections/deploy/box/templates/edge.yaml.tmpl
864
885
  - lib/hecks/projections/deploy/box/templates/fetch-secrets.sh
865
886
  - lib/hecks/projections/deploy/box/templates/hosting.mk.tmpl
887
+ - lib/hecks/projections/deploy/box/templates/log-group.yaml.tmpl
888
+ - lib/hecks/projections/deploy/box/templates/log-policy.yaml.tmpl
866
889
  - lib/hecks/projections/deploy/box/templates/mode-summary-services.tmpl
867
890
  - lib/hecks/projections/deploy/box/templates/mode-summary-taskdef.tmpl
868
891
  - lib/hecks/projections/deploy/box/templates/provision-database.sh.tmpl
@@ -1165,7 +1188,10 @@ files:
1165
1188
  - lib/hecks/projections/site/edge/behaviours.rb
1166
1189
  - lib/hecks/projections/site/edge/checks.rb
1167
1190
  - lib/hecks/projections/site/edge/findings.rb
1191
+ - lib/hecks/projections/site/edge/function_checks.rb
1192
+ - lib/hecks/projections/site/edge/function_code.rb
1168
1193
  - lib/hecks/projections/site/edge/pattern.rb
1194
+ - lib/hecks/projections/site/edge/redirect_host.js.tmpl
1169
1195
  - lib/hecks/projections/site/edge/route_checks.rb
1170
1196
  - lib/hecks/projections/site/edge/row_reader.rb
1171
1197
  - lib/hecks/projections/site/edge/rules.rb
@@ -1481,6 +1507,7 @@ files:
1481
1507
  - lib/hecks/rust_build/host.rb
1482
1508
  - lib/hecks/rust_build/kernel_input.rb
1483
1509
  - lib/hecks/rust_build/native_build.rb
1510
+ - lib/hecks/rust_build/prebuilt_binary.rb
1484
1511
  - lib/hecks/rust_build/project_rust.rb
1485
1512
  - lib/hecks/rust_build/project_rust/codegen_run.rb
1486
1513
  - lib/hecks/rust_build/project_rust/domain_loader.rb
@@ -1559,6 +1586,7 @@ files:
1559
1586
  - lib/hecks/tools/lanes.rb
1560
1587
  - lib/hecks/tools/lanes/files.rb
1561
1588
  - lib/hecks/tools/lanes/live.rb
1589
+ - lib/hecks/tools/lanes/prebuilt_workflow.rb
1562
1590
  - lib/hecks/tools/lanes/rows.rb
1563
1591
  - lib/hecks/tools/lanes/ruleset.rb
1564
1592
  - lib/hecks/tools/lanes/watch_workflow.rb
@@ -1567,6 +1595,7 @@ files:
1567
1595
  - lib/hecks/tools/oidc_manifests.rb
1568
1596
  - lib/hecks/tools/query_ir_run.rb
1569
1597
  - lib/hecks/tools/regeneration_run.rb
1598
+ - lib/hecks/tools/release_only.rb
1570
1599
  - lib/hecks/tools/rust_comment_style.rb
1571
1600
  - lib/hecks/tools/rust_comment_style/caps_words.rb
1572
1601
  - lib/hecks/tools/rust_comment_style/fixing.rb
@@ -1607,9 +1636,9 @@ files:
1607
1636
  - lib/rubocop/cop/hecks/symbol_string_key_mix.rb
1608
1637
  - lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb
1609
1638
  - lib/rubocop/cop/hecks/untyped_raise.rb
1610
- - prebuilt/05c358d38d661a3774f813d49c66f5b68939af94432d66257a56f49ee2598994.marshal
1611
- - prebuilt/26c40c61f09b09b11651c64668eda3ceb180f83cf2a3802d3033d2fd99ac841b.marshal
1612
- - prebuilt/verdicts-230ac18c25abfd3d92ede032bee2661a4c2cd2773ab0cf0e2ae52546ba2f7af9.json
1639
+ - prebuilt/2b59737fbdbd5f44edda0fddce2611f45d76087b3a7c782d1f1a17ea58c77948.marshal
1640
+ - prebuilt/58208399bada10b0db99928c48e38789288fc58f7b71a72b21bfa386680e32ed.marshal
1641
+ - prebuilt/verdicts-48318ca172edd21443b1ad02801c697fff100b746e828f09db987647641a5df2.json
1613
1642
  - qa/settings.yml
1614
1643
  - rust/.cargo/config.toml
1615
1644
  - rust/.cargo/mutants.toml
@@ -1706,6 +1735,29 @@ files:
1706
1735
  - rust/host/src/payments.rs
1707
1736
  - rust/host/src/payments/keystore.rs
1708
1737
  - rust/host/src/payments/tests.rs
1738
+ - rust/host/src/platform_signin/attack_tests.rs
1739
+ - rust/host/src/platform_signin/config.rs
1740
+ - rust/host/src/platform_signin/cookie.rs
1741
+ - rust/host/src/platform_signin/extension.rs
1742
+ - rust/host/src/platform_signin/fetcher.rs
1743
+ - rust/host/src/platform_signin/handler.rs
1744
+ - rust/host/src/platform_signin/handler_tests.rs
1745
+ - rust/host/src/platform_signin/jti.rs
1746
+ - rust/host/src/platform_signin/jwks.rs
1747
+ - rust/host/src/platform_signin/jws.rs
1748
+ - rust/host/src/platform_signin/known_answer_tests.rs
1749
+ - rust/host/src/platform_signin/mapping_tests.rs
1750
+ - rust/host/src/platform_signin/mod.rs
1751
+ - rust/host/src/platform_signin/mode_tests.rs
1752
+ - rust/host/src/platform_signin/pages.rs
1753
+ - rust/host/src/platform_signin/query.rs
1754
+ - rust/host/src/platform_signin/session.rs
1755
+ - rust/host/src/platform_signin/settings.rs
1756
+ - rust/host/src/platform_signin/strict_json.rs
1757
+ - rust/host/src/platform_signin/test_support.rs
1758
+ - rust/host/src/platform_signin/ttl_tests.rs
1759
+ - rust/host/src/platform_signin/verify.rs
1760
+ - rust/host/src/platform_signin/wiring_support.rs
1709
1761
  - rust/host/src/presentation.rs
1710
1762
  - rust/host/src/presentation_write.rs
1711
1763
  - rust/host/src/query_step.rs