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,237 @@
1
+ //! The site's own sign-in cookie, `__Host-signin`, which binds a callback to the browser that
2
+ //! started the flow.
3
+ //!
4
+ //! The value is `<nonce>.<state>`, both base64url, written exactly as the issuer's reference
5
+ //! implementation writes it so the two sides agree. The `__Host-` prefix makes a browser insist on
6
+ //! `Secure`, `Path=/` and no `Domain`, so a sibling subdomain cannot plant a lookalike. `Secure` is
7
+ //! written unconditionally rather than inferred from the request, because the host sits behind
8
+ //! proxies that speak plain http to it. `SameSite=Lax`, not `Strict`: the return from the issuer is
9
+ //! a cross-site top-level navigation, and a strict cookie would not be sent on it.
10
+ //!
11
+ //! Reading is strict in the other direction: a cookie that appears twice is refused, because which
12
+ //! of two values counts is unknowable.
13
+
14
+ use serde_json::Value;
15
+
16
+ use super::verify::SigninCookie;
17
+
18
+ /// The attributes every write of the cookie carries, the same ones that set it and delete it.
19
+ const ATTRIBUTES: &str = "Path=/; Secure; HttpOnly; SameSite=Lax";
20
+ /// The cookie's name.
21
+ pub const NAME: &str = "__Host-signin";
22
+ /// How long the browser keeps the cookie, in seconds.
23
+ pub const MAX_AGE_SECONDS: u32 = 600;
24
+ /// The shortest nonce or state accepted.
25
+ pub const MIN_VALUE_LENGTH: usize = 16;
26
+ /// The longest nonce or state accepted.
27
+ pub const MAX_VALUE_LENGTH: usize = 128;
28
+
29
+ /// Why the cookie could not be used.
30
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
31
+ pub enum CookieFault {
32
+ /// The browser sent no such cookie.
33
+ Absent,
34
+ /// The browser sent it more than once.
35
+ Repeated,
36
+ /// The value is not `<nonce>.<state>` of acceptable shape.
37
+ Malformed,
38
+ }
39
+
40
+ /// Returns the `Set-Cookie` value that stores the nonce and state in the browser.
41
+ pub fn set_header(cookie: &SigninCookie) -> String {
42
+ format!(
43
+ "{NAME}={}.{}; Max-Age={MAX_AGE_SECONDS}; {ATTRIBUTES}",
44
+ cookie.nonce, cookie.state
45
+ )
46
+ }
47
+
48
+ /// Returns the `Set-Cookie` value that deletes the cookie, with the attributes that set it.
49
+ pub fn clear_header() -> String {
50
+ format!("{NAME}=; Max-Age=0; {ATTRIBUTES}")
51
+ }
52
+
53
+ /// Collects every `name=value` pair the request carried, from the event's `cookies` array and its
54
+ /// `cookie` header.
55
+ pub fn request_pairs(event: &Value) -> Vec<String> {
56
+ let listed = event
57
+ .get("cookies")
58
+ .and_then(Value::as_array)
59
+ .into_iter()
60
+ .flatten()
61
+ .filter_map(Value::as_str);
62
+ let header = event
63
+ .get("headers")
64
+ .and_then(|h| h.get("cookie"))
65
+ .and_then(Value::as_str)
66
+ .into_iter()
67
+ .flat_map(|h| h.split(';'));
68
+ listed
69
+ .chain(header)
70
+ .map(|pair| pair.trim().to_string())
71
+ .filter(|pair| !pair.is_empty())
72
+ .collect()
73
+ }
74
+
75
+ /// Finds the sign-in cookie among the request's pairs.
76
+ ///
77
+ /// # Errors
78
+ ///
79
+ /// Returns the [`CookieFault`] that says why there is no usable cookie.
80
+ pub fn read(pairs: &[String]) -> Result<SigninCookie, CookieFault> {
81
+ let mut values = pairs
82
+ .iter()
83
+ .filter_map(|pair| pair.split_once('='))
84
+ .filter(|(name, _)| name.trim() == NAME)
85
+ .map(|(_, value)| value.trim());
86
+ let value = values.next().ok_or(CookieFault::Absent)?;
87
+ if values.next().is_some() {
88
+ return Err(CookieFault::Repeated);
89
+ }
90
+ parse_value(value).ok_or(CookieFault::Malformed)
91
+ }
92
+
93
+ fn parse_value(value: &str) -> Option<SigninCookie> {
94
+ let (nonce, state) = value.split_once('.')?;
95
+ let acceptable = |part: &str| {
96
+ (MIN_VALUE_LENGTH..=MAX_VALUE_LENGTH).contains(&part.len()) && part.bytes().all(is_url_safe)
97
+ };
98
+ (acceptable(nonce) && acceptable(state)).then(|| SigninCookie {
99
+ nonce: nonce.into(),
100
+ state: state.into(),
101
+ })
102
+ }
103
+
104
+ fn is_url_safe(byte: u8) -> bool {
105
+ byte.is_ascii_alphanumeric() || byte == b'-' || byte == b'_'
106
+ }
107
+
108
+ #[cfg(test)]
109
+ mod tests {
110
+ use super::*;
111
+ use serde_json::json;
112
+
113
+ const NONCE: &str = "Nn0nceNn0nceNn0nceAA";
114
+ const STATE: &str = "StateStateState0123456789";
115
+
116
+ fn cookie() -> SigninCookie {
117
+ SigninCookie {
118
+ nonce: NONCE.into(),
119
+ state: STATE.into(),
120
+ }
121
+ }
122
+
123
+ fn pairs(items: &[&str]) -> Vec<String> {
124
+ items.iter().map(|s| s.to_string()).collect()
125
+ }
126
+
127
+ #[test]
128
+ fn the_set_header_matches_the_issuers_reference_exactly() {
129
+ assert_eq!(
130
+ set_header(&cookie()),
131
+ format!("__Host-signin={NONCE}.{STATE}; Max-Age=600; Path=/; Secure; HttpOnly; SameSite=Lax")
132
+ );
133
+ }
134
+
135
+ #[test]
136
+ fn the_clear_header_keeps_every_attribute_and_zeroes_the_age() {
137
+ assert_eq!(
138
+ clear_header(),
139
+ "__Host-signin=; Max-Age=0; Path=/; Secure; HttpOnly; SameSite=Lax"
140
+ );
141
+ }
142
+
143
+ #[test]
144
+ fn neither_header_carries_a_domain_attribute() {
145
+ assert!(!set_header(&cookie())
146
+ .to_ascii_lowercase()
147
+ .contains("domain"));
148
+ assert!(!clear_header().to_ascii_lowercase().contains("domain"));
149
+ }
150
+
151
+ #[test]
152
+ fn a_pair_reads_back_into_its_nonce_and_state() {
153
+ let read = read(&pairs(&[&format!("__Host-signin={NONCE}.{STATE}")])).unwrap();
154
+ assert_eq!(read, cookie());
155
+ }
156
+
157
+ #[test]
158
+ fn other_cookies_are_ignored_and_a_lookalike_name_is_not_the_cookie() {
159
+ let found = read(&pairs(&[
160
+ "a=b",
161
+ &format!("__Host-signin={NONCE}.{STATE}"),
162
+ "c=d",
163
+ ]))
164
+ .unwrap();
165
+ assert_eq!(found, cookie());
166
+ assert_eq!(
167
+ read(&pairs(&[
168
+ &format!("signin={NONCE}.{STATE}"),
169
+ &format!("x__Host-signin={NONCE}.{STATE}")
170
+ ])),
171
+ Err(CookieFault::Absent)
172
+ );
173
+ }
174
+
175
+ #[test]
176
+ fn no_cookie_is_absent() {
177
+ assert_eq!(read(&[]), Err(CookieFault::Absent));
178
+ }
179
+
180
+ #[test]
181
+ fn a_repeated_cookie_is_refused_even_when_the_values_agree() {
182
+ let one = format!("__Host-signin={NONCE}.{STATE}");
183
+ assert_eq!(read(&pairs(&[&one, &one])), Err(CookieFault::Repeated));
184
+ }
185
+
186
+ #[test]
187
+ fn a_value_of_the_wrong_shape_is_malformed() {
188
+ for bad in [
189
+ "",
190
+ NONCE,
191
+ &format!("{NONCE}."),
192
+ &format!(".{STATE}"),
193
+ &format!("{NONCE}.{STATE}.extra"),
194
+ &format!("short.{STATE}"),
195
+ &format!("{NONCE}.short"),
196
+ &format!("{NONCE}.{}", "s".repeat(129)),
197
+ &format!("{NONCE}.{STATE}+"),
198
+ &format!("{NONCE}.{STATE}="),
199
+ &format!("{NONCE}.sta te0123456789012"),
200
+ ] {
201
+ assert_eq!(
202
+ read(&pairs(&[&format!("__Host-signin={bad}")])),
203
+ Err(CookieFault::Malformed),
204
+ "{bad:?}"
205
+ );
206
+ }
207
+ }
208
+
209
+ #[test]
210
+ fn the_longest_acceptable_values_pass() {
211
+ let long = "a".repeat(128);
212
+ let found = read(&pairs(&[&format!("__Host-signin={long}.{long}")])).unwrap();
213
+ assert_eq!(found.nonce.len(), 128);
214
+ }
215
+
216
+ #[test]
217
+ fn pairs_come_from_the_cookie_header_and_from_the_cookies_array() {
218
+ let event = json!({"headers": {"cookie": "a=b; __Host-signin=x.y ; c=d"}, "cookies": ["e=f", "g=h"]});
219
+ let all = request_pairs(&event);
220
+ for expected in ["a=b", "__Host-signin=x.y", "c=d", "e=f", "g=h"] {
221
+ assert!(all.iter().any(|p| p == expected), "{expected} in {all:?}");
222
+ }
223
+ }
224
+
225
+ #[test]
226
+ fn a_cookie_named_in_both_places_counts_twice() {
227
+ let one = format!("__Host-signin={NONCE}.{STATE}");
228
+ let event = json!({"headers": {"cookie": one}, "cookies": [one]});
229
+ assert_eq!(read(&request_pairs(&event)), Err(CookieFault::Repeated));
230
+ }
231
+
232
+ #[test]
233
+ fn an_event_without_cookies_yields_no_pairs() {
234
+ assert!(request_pairs(&json!({})).is_empty());
235
+ assert!(request_pairs(&json!({"headers": {"cookie": 5}, "cookies": "x"})).is_empty());
236
+ }
237
+ }
@@ -0,0 +1,190 @@
1
+ //! The host extension that puts the sign-in flow on the site's routes.
2
+ //!
3
+ //! It is deliberately small and generically named so it can move to the platform later: it knows
4
+ //! the routes, the mode and nothing about any one site. With `PLATFORM_SIGNIN` unset it is never
5
+ //! installed, so the host answers exactly as before.
6
+ //!
7
+ //! ## One installed instance per process
8
+ //!
9
+ //! The single-use record of token identifiers lives in the verifier this extension owns, and the
10
+ //! host installs one extension for the life of the process, so the record is process-wide. It does
11
+ //! not survive a restart (the replay window that opens is at most the token lifetime plus the
12
+ //! skew, 90 seconds) and it is not shared between instances, which is why the settings refuse any
13
+ //! process that is not the single long-lived server.
14
+
15
+ use std::collections::HashMap;
16
+ use std::sync::Arc;
17
+
18
+ use async_trait::async_trait;
19
+ use serde_json::Value;
20
+
21
+ use super::fetcher::HttpJwksFetcher;
22
+ use super::handler::{CallbackRequest, SigninHandler, StdoutLog};
23
+ use super::jti::JtiStore;
24
+ use super::jwks::JwksCache;
25
+ use super::pages::{self, Failure};
26
+ use super::session::{HostMembers, MemberDirectory};
27
+ use super::settings::{Mode, SettingsError, SigninSettings};
28
+ use super::verify::Verifier;
29
+ use super::{cookie, query};
30
+ use crate::extension::{Ctx, HostExtension, RateRule};
31
+
32
+ /// Starts the flow in either mode.
33
+ pub const START_PATH: &str = "/auth/platform";
34
+ /// Finishes the flow in either mode.
35
+ pub const CALLBACK_PATH: &str = "/auth/platform/callback";
36
+ /// The site's Google sign-in path, which `default` mode takes over.
37
+ pub const GOOGLE_PATH: &str = "/auth/google";
38
+
39
+ /// Which half of the flow a request is for.
40
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
41
+ pub enum Route {
42
+ Start,
43
+ Callback,
44
+ }
45
+
46
+ /// The platform sign-in extension.
47
+ pub struct PlatformSignin {
48
+ handler: SigninHandler,
49
+ }
50
+
51
+ impl PlatformSignin {
52
+ /// Wraps a handler that is already built.
53
+ pub fn new(handler: SigninHandler) -> Self {
54
+ Self { handler }
55
+ }
56
+
57
+ /// Builds the extension from an explicit map, or `None` when `PLATFORM_SIGNIN` is unset.
58
+ ///
59
+ /// # Errors
60
+ ///
61
+ /// Returns the setting that is missing or malformed once the switch is on.
62
+ pub fn from_env_map(vars: &HashMap<String, String>) -> Result<Option<Self>, SettingsError> {
63
+ let Some(settings) = SigninSettings::from_env_map(vars)? else {
64
+ return Ok(None);
65
+ };
66
+ let keys = JwksCache::new(HttpJwksFetcher::new(&settings.config.jwks_url));
67
+ let verifier = Verifier::new(settings.config.clone(), keys, JtiStore::new());
68
+ Ok(Some(Self::new(SigninHandler::new(
69
+ settings,
70
+ Arc::new(verifier),
71
+ Arc::new(StdoutLog),
72
+ ))))
73
+ }
74
+
75
+ /// Decides which half of the flow, if either, a request is for.
76
+ pub fn classify(&self, event: &Value) -> Option<Route> {
77
+ let method = event
78
+ .pointer("/requestContext/http/method")
79
+ .and_then(Value::as_str)?;
80
+ let path = event.get("rawPath").and_then(Value::as_str)?;
81
+ if method != "GET" {
82
+ return None;
83
+ }
84
+ match path {
85
+ START_PATH => Some(Route::Start),
86
+ CALLBACK_PATH => Some(Route::Callback),
87
+ GOOGLE_PATH if self.takes_over_google(raw_query(event)) => Some(Route::Start),
88
+ _ => None,
89
+ }
90
+ }
91
+
92
+ fn takes_over_google(&self, raw_query: &str) -> bool {
93
+ self.handler.settings().mode == Mode::Default
94
+ && !query::has_value(raw_query, "via", "google")
95
+ }
96
+
97
+ /// Finishes a sign-in for a callback request against a member store.
98
+ pub async fn finish(&self, request: &CallbackRequest, members: &dyn MemberDirectory) -> Value {
99
+ self.handler.callback(request, members).await
100
+ }
101
+
102
+ /// Answers a request that belongs to the flow, or `None` for everything else.
103
+ pub async fn route_event(&self, event: &Value, members: &dyn MemberDirectory) -> Option<Value> {
104
+ match self.classify(event)? {
105
+ Route::Start => Some(self.handler.start()),
106
+ Route::Callback => Some(self.finish(&callback_request(event), members).await),
107
+ }
108
+ }
109
+ }
110
+
111
+ fn raw_query(event: &Value) -> &str {
112
+ event
113
+ .get("rawQueryString")
114
+ .and_then(Value::as_str)
115
+ .unwrap_or("")
116
+ }
117
+
118
+ fn callback_request(event: &Value) -> CallbackRequest {
119
+ CallbackRequest {
120
+ raw_query: raw_query(event).to_string(),
121
+ cookie_pairs: cookie::request_pairs(event),
122
+ }
123
+ }
124
+
125
+ #[async_trait]
126
+ impl HostExtension for PlatformSignin {
127
+ async fn guest_route(&self, ctx: &Ctx<'_>) -> Option<Value> {
128
+ let event = ctx.event?;
129
+ match self.classify(event)? {
130
+ Route::Start => Some(self.handler.start()),
131
+ Route::Callback => Some(self.finish_for_host(ctx, event).await),
132
+ }
133
+ }
134
+
135
+ fn rate_rules(&self) -> Vec<RateRule> {
136
+ vec![
137
+ RateRule {
138
+ name: "platform_signin_start",
139
+ method: "GET",
140
+ path: START_PATH,
141
+ limit_env: "HECKS_RATE_LIMIT_PLATFORM_SIGNIN_START",
142
+ default_limit: 30,
143
+ },
144
+ RateRule {
145
+ name: "platform_signin_callback",
146
+ method: "GET",
147
+ path: CALLBACK_PATH,
148
+ limit_env: "HECKS_RATE_LIMIT_PLATFORM_SIGNIN_CALLBACK",
149
+ default_limit: 30,
150
+ },
151
+ ]
152
+ }
153
+ }
154
+
155
+ impl PlatformSignin {
156
+ /// Finishes a callback against the host's own member store.
157
+ async fn finish_for_host(&self, ctx: &Ctx<'_>, event: &Value) -> Value {
158
+ let Some(domain_ir) = ctx.domain_ir else {
159
+ return pages::refusal(Failure::Unavailable);
160
+ };
161
+ let members = HostMembers {
162
+ client: ctx.client,
163
+ domain_ir,
164
+ wasm_path: ctx.wasm_path,
165
+ config: ctx.config,
166
+ invoker: ctx.invoker,
167
+ };
168
+ self.finish(&callback_request(event), &members).await
169
+ }
170
+ }
171
+
172
+ /// Refuses the boot because the sign-in settings were switched on and are wrong.
173
+ pub struct RefusedSettings(pub SettingsError);
174
+
175
+ #[async_trait]
176
+ impl HostExtension for RefusedSettings {
177
+ fn boot_check(&self, _domain: &str) -> Result<(), String> {
178
+ Err(format!("platform sign-in: {}", self.0))
179
+ }
180
+ }
181
+
182
+ /// The extensions that platform sign-in contributes for these settings: none when it is off, the
183
+ /// flow when it is on, and a boot refusal when the settings are wrong.
184
+ pub fn extensions_for(vars: &HashMap<String, String>) -> Vec<Arc<dyn HostExtension>> {
185
+ match PlatformSignin::from_env_map(vars) {
186
+ Ok(None) => Vec::new(),
187
+ Ok(Some(extension)) => vec![Arc::new(extension)],
188
+ Err(error) => vec![Arc::new(RefusedSettings(error))],
189
+ }
190
+ }