hecks 3.7.0 → 3.9.1
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.
- checksums.yaml +4 -4
- data/lib/hecks/bluebook/meta_validator/modes.rb +14 -0
- data/lib/hecks/bluebook/meta_validator.rb +4 -2
- data/lib/hecks/corpus/routes.rb +1 -1
- data/lib/hecks/gate/stages.yml +11 -1
- data/lib/hecks/hecks/adapters/site_toolchain/prober.rb +64 -0
- data/lib/hecks/hecks/adapters/site_toolchain.rb +41 -5
- data/lib/hecks/language/bluebook/vocabulary.bluebook +1 -1
- data/lib/hecks/projections/deploy/shared_database_instance/templates/rds.yaml.tmpl +1 -1
- data/lib/hecks/projections/deploy/templates/lambda/template.yaml.tmpl +1 -1
- data/lib/hecks/projections/site/probe.rb +131 -0
- data/lib/hecks/projections/site/site_host/dockerfile.rb +1 -1
- data/lib/hecks/runtime/value/validation.rb +34 -3
- data/lib/hecks/site/bluebook/site.bluebook +31 -0
- data/lib/hecks/site/bluebook/site.ports.hecksagon +6 -0
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks/vocabulary.rb +1 -1
- data/prebuilt/{verdicts-be73f0df9eee0abe2f4aadc968321c4b919d19d424a3ccfe1777a71dabf7d9e7.json → verdicts-91aaf93ce8981bd9a754948f075d95650dc2ec6435015376f5c60992b36a24b1.json} +1 -1
- data/rust/codegen/src/fielded.rs +3 -3
- data/rust/codegen/src/json_codec.rs +2 -2
- data/rust/codegen/src/naming.rs +20 -0
- data/rust/host/Cargo.toml +4 -0
- data/rust/host/HECKS_RELEASE +1 -1
- data/rust/host/src/boot.rs +443 -0
- data/rust/host/src/boundary_fuzz/ir.rs +1 -0
- data/rust/host/src/commerce_ir.rs +459 -0
- data/rust/host/src/extension.rs +100 -0
- data/rust/host/src/ir.rs +0 -448
- data/rust/host/src/journal.rs +1 -1
- data/rust/host/src/lib.rs +40 -0
- data/rust/host/src/main.rs +4 -477
- data/rust/host/src/mint.rs +1 -1
- data/rust/host/src/payments/tests.rs +8 -8
- data/rust/host/src/payments.rs +10 -10
- data/rust/host/src/rate_limit.rs +27 -41
- data/rust/host/src/secrets.rs +15 -0
- data/rust/host/src/web/commerce.rs +121 -0
- data/rust/host/src/web/newsletter.rs +2 -1
- data/rust/host/src/web/newsletter_send.rs +1 -1
- data/rust/host/src/web/registration_receipt.rs +2 -2
- data/rust/host/src/web/registrations.rs +49 -49
- data/rust/host/src/web/resend_webhook.rs +300 -0
- data/rust/host/src/web.rs +43 -47
- metadata +13 -5
- /data/prebuilt/{a0818e2de67364dc1ab22868b6cbf869c62da6f8d9b924c778fa5cce2e76ee94.marshal → 0ee7f83cba59ce7d1c45c65ba166b071779f70143449f584f33ff7989f64eccb.marshal} +0 -0
- /data/prebuilt/{945481eefbdb584649d45f256307053558966a868b9c38dee7ee88f8169610f8.marshal → 6e4a6bcf0aa880e2e6b056e1846b70e5a768090b9dc998bbadeaf5d6bcfd3db8.marshal} +0 -0
data/rust/host/src/rate_limit.rs
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
use axum::http::header::{HeaderValue, CONTENT_TYPE, RETRY_AFTER};
|
|
5
5
|
use axum::http::{HeaderMap, HeaderName, StatusCode};
|
|
6
6
|
use axum::response::{IntoResponse, Response};
|
|
7
|
+
use crate::extension::RateRule;
|
|
7
8
|
use serde_json::json;
|
|
8
9
|
use std::collections::{BTreeMap, HashMap, VecDeque};
|
|
9
10
|
use std::net::{IpAddr, Ipv6Addr};
|
|
@@ -14,8 +15,6 @@ use std::time::{Duration, Instant};
|
|
|
14
15
|
const UNKNOWN_CLIENT: &str = "unknown";
|
|
15
16
|
|
|
16
17
|
const DEFAULT_WINDOW_SECONDS: u64 = 3600;
|
|
17
|
-
const DEFAULT_SUBSCRIBE: usize = 10;
|
|
18
|
-
const DEFAULT_REGISTER: usize = 15;
|
|
19
18
|
const DEFAULT_MAX_KEYS: usize = 10_000;
|
|
20
19
|
|
|
21
20
|
/// The outcome of counting one request against a window.
|
|
@@ -215,28 +214,12 @@ fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
|
|
|
215
214
|
a.len() == b.len() && a.iter().zip(b).fold(0u8, |diff, (x, y)| diff | (x ^ y)) == 0
|
|
216
215
|
}
|
|
217
216
|
|
|
218
|
-
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
219
|
-
pub enum Form {
|
|
220
|
-
Subscribe,
|
|
221
|
-
Register,
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
impl Form {
|
|
225
|
-
pub fn for_request(method: &str, path: &str) -> Option<Self> {
|
|
226
|
-
match (method, path) {
|
|
227
|
-
("POST", "/newsletter/subscribers") => Some(Self::Subscribe),
|
|
228
|
-
("POST", "/registrations") => Some(Self::Register),
|
|
229
|
-
_ => None,
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
217
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
235
218
|
pub struct Config {
|
|
236
219
|
enabled: bool,
|
|
237
220
|
window: Duration,
|
|
238
|
-
|
|
239
|
-
|
|
221
|
+
// Each extension rule's budget, by rule name.
|
|
222
|
+
limits: HashMap<&'static str, usize>,
|
|
240
223
|
max_keys: usize,
|
|
241
224
|
trust: ProxyTrust,
|
|
242
225
|
}
|
|
@@ -256,8 +239,10 @@ impl Config {
|
|
|
256
239
|
}
|
|
257
240
|
};
|
|
258
241
|
let window = number("HECKS_RATE_LIMIT_WINDOW_SECONDS", DEFAULT_WINDOW_SECONDS);
|
|
259
|
-
let
|
|
260
|
-
|
|
242
|
+
let limits = crate::extension::rate_rules()
|
|
243
|
+
.iter()
|
|
244
|
+
.map(|rule| (rule.name, usize::try_from(number(rule.limit_env, rule.default_limit as u64)).unwrap_or(usize::MAX)))
|
|
245
|
+
.collect();
|
|
261
246
|
let max_keys = number("HECKS_RATE_LIMIT_MAX_KEYS", DEFAULT_MAX_KEYS as u64);
|
|
262
247
|
let hops = get("HECKS_TRUSTED_PROXY_HOPS").map(|v| v.trim().to_string()).filter(|v| !v.is_empty()).map_or(0, |raw| {
|
|
263
248
|
raw.parse::<usize>().unwrap_or_else(|_| {
|
|
@@ -272,19 +257,20 @@ impl Config {
|
|
|
272
257
|
let config = Self {
|
|
273
258
|
enabled,
|
|
274
259
|
window: Duration::from_secs(window),
|
|
275
|
-
|
|
276
|
-
register: usize::try_from(register).unwrap_or(usize::MAX),
|
|
260
|
+
limits,
|
|
277
261
|
max_keys: usize::try_from(max_keys).unwrap_or(usize::MAX),
|
|
278
262
|
trust: ProxyTrust { networks, hops, auth },
|
|
279
263
|
};
|
|
280
264
|
(config, warnings)
|
|
281
265
|
}
|
|
282
266
|
|
|
283
|
-
fn limit_for(&self,
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
267
|
+
fn limit_for(&self, rule: &RateRule) -> usize {
|
|
268
|
+
self.limits.get(rule.name).copied().unwrap_or(rule.default_limit)
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
#[cfg(test)]
|
|
272
|
+
fn limit(&self, name: &str) -> usize {
|
|
273
|
+
self.limits[name]
|
|
288
274
|
}
|
|
289
275
|
}
|
|
290
276
|
|
|
@@ -336,7 +322,7 @@ pub enum Verdict {
|
|
|
336
322
|
|
|
337
323
|
pub struct RateLimits {
|
|
338
324
|
config: Config,
|
|
339
|
-
windows: Mutex<HashMap
|
|
325
|
+
windows: Mutex<HashMap<&'static str, SlidingWindow>>,
|
|
340
326
|
warned_untrusted_proxy: AtomicBool,
|
|
341
327
|
}
|
|
342
328
|
|
|
@@ -355,8 +341,7 @@ impl RateLimits {
|
|
|
355
341
|
json!({
|
|
356
342
|
"enabled": config.enabled,
|
|
357
343
|
"window_seconds": config.window.as_secs(),
|
|
358
|
-
"
|
|
359
|
-
"register": config.register,
|
|
344
|
+
"limits": config.limits,
|
|
360
345
|
"trusted_networks": config.trust.networks.len(),
|
|
361
346
|
"trusted_proxy_hops": config.trust.hops,
|
|
362
347
|
"proxy_auth": config.trust.auth.is_some(),
|
|
@@ -372,7 +357,7 @@ impl RateLimits {
|
|
|
372
357
|
|
|
373
358
|
// `check` with the clock supplied, so tests need not sleep.
|
|
374
359
|
pub fn check_at(&self, method: &str, path: &str, headers: &HeaderMap, peer: Option<IpAddr>, now: Instant) -> Verdict {
|
|
375
|
-
let Some(
|
|
360
|
+
let Some(rule) = crate::extension::rate_rule_for(method, path).filter(|_| self.config.enabled) else {
|
|
376
361
|
return Verdict::Allowed;
|
|
377
362
|
};
|
|
378
363
|
let key = self.client_key(headers, peer);
|
|
@@ -380,8 +365,8 @@ impl RateLimits {
|
|
|
380
365
|
let mut windows = self.windows.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
|
|
381
366
|
let config = &self.config;
|
|
382
367
|
windows
|
|
383
|
-
.entry(
|
|
384
|
-
.or_insert_with(|| SlidingWindow::new(config.limit_for(
|
|
368
|
+
.entry(rule.name)
|
|
369
|
+
.or_insert_with(|| SlidingWindow::new(config.limit_for(&rule), config.window, config.max_keys))
|
|
385
370
|
.check(&key, now)
|
|
386
371
|
};
|
|
387
372
|
if decision.allowed {
|
|
@@ -725,7 +710,7 @@ mod tests {
|
|
|
725
710
|
assert!(warnings.is_empty());
|
|
726
711
|
assert!(config.enabled);
|
|
727
712
|
assert_eq!(config.window, HOUR);
|
|
728
|
-
assert_eq!((config.subscribe, config.register, config.max_keys), (10, 15, 10_000));
|
|
713
|
+
assert_eq!((config.limit("subscribe"), config.limit("register"), config.max_keys), (10, 15, 10_000));
|
|
729
714
|
assert_eq!(config.trust, ProxyTrust::default());
|
|
730
715
|
}
|
|
731
716
|
|
|
@@ -743,7 +728,7 @@ mod tests {
|
|
|
743
728
|
]);
|
|
744
729
|
assert!(warnings.is_empty(), "{warnings:?}");
|
|
745
730
|
assert_eq!(config.window, Duration::from_secs(60));
|
|
746
|
-
assert_eq!((config.subscribe, config.register, config.max_keys), (3, 4, 50));
|
|
731
|
+
assert_eq!((config.limit("subscribe"), config.limit("register"), config.max_keys), (3, 4, 50));
|
|
747
732
|
assert_eq!(config.trust, trust(&["10.0.0.0/8", "127.0.0.1"], 1, Some(("x-origin-secret", "s3cret"))));
|
|
748
733
|
}
|
|
749
734
|
|
|
@@ -766,7 +751,7 @@ mod tests {
|
|
|
766
751
|
("HECKS_TRUSTED_PROXY_HOPS", "two"),
|
|
767
752
|
("HECKS_TRUSTED_PROXIES", "10.0.0.0/8, banana"),
|
|
768
753
|
]);
|
|
769
|
-
assert_eq!((config.subscribe, config.register, config.window), (10, 15, HOUR));
|
|
754
|
+
assert_eq!((config.limit("subscribe"), config.limit("register"), config.window), (10, 15, HOUR));
|
|
770
755
|
assert_eq!(config.trust.hops, 0);
|
|
771
756
|
assert_eq!(config.trust.networks.len(), 1);
|
|
772
757
|
assert_eq!(warnings.len(), 5, "{warnings:?}");
|
|
@@ -786,8 +771,9 @@ mod tests {
|
|
|
786
771
|
|
|
787
772
|
#[test]
|
|
788
773
|
fn only_the_public_write_routes_are_limited() {
|
|
789
|
-
|
|
790
|
-
assert_eq!(
|
|
774
|
+
let name = |method, path| crate::extension::rate_rule_for(method, path).map(|rule| rule.name);
|
|
775
|
+
assert_eq!(name("POST", "/newsletter/subscribers"), Some("subscribe"));
|
|
776
|
+
assert_eq!(name("POST", "/registrations"), Some("register"));
|
|
791
777
|
for (method, path) in [
|
|
792
778
|
("GET", "/newsletter/subscribers"),
|
|
793
779
|
("GET", "/newsletter/subscribers/confirm"),
|
|
@@ -800,7 +786,7 @@ mod tests {
|
|
|
800
786
|
("POST", "/members"),
|
|
801
787
|
("POST", "/newsletter/subscribers/"),
|
|
802
788
|
] {
|
|
803
|
-
assert_eq!(
|
|
789
|
+
assert_eq!(crate::extension::rate_rule_for(method, path).map(|rule| rule.name), None, "{method} {path}");
|
|
804
790
|
}
|
|
805
791
|
}
|
|
806
792
|
|
data/rust/host/src/secrets.rs
CHANGED
|
@@ -75,6 +75,12 @@ pub fn extract_field(secret_json: &str, field: &str) -> Result<String, String> {
|
|
|
75
75
|
.ok_or_else(|| format!("secret's SecretString has no string {field:?} field"))
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
/// One named string field out of a secret's JSON `SecretString`, or `None` when the secret does not
|
|
79
|
+
/// carry it (or is not JSON, or the field is blank). For a field a secret may have and may not.
|
|
80
|
+
pub fn optional_field(secret_json: &str, field: &str) -> Option<String> {
|
|
81
|
+
extract_field(secret_json, field).ok().filter(|value| !value.trim().is_empty())
|
|
82
|
+
}
|
|
83
|
+
|
|
78
84
|
/// The `postgres://` URL for a database secret.
|
|
79
85
|
///
|
|
80
86
|
/// The user is the secret's `username`, which is `postgres` for RDS's managed master secret and a
|
|
@@ -96,6 +102,15 @@ pub fn database_url(secret_json: &str, host: &str, database: &str) -> Result<Str
|
|
|
96
102
|
mod tests {
|
|
97
103
|
use super::*;
|
|
98
104
|
|
|
105
|
+
#[test]
|
|
106
|
+
fn an_optional_field_is_read_when_present_and_none_when_absent_blank_or_not_json() {
|
|
107
|
+
let json = r#"{"api_key":"re_1","webhook_secret":"whsec_abc","blank":" "}"#;
|
|
108
|
+
assert_eq!(optional_field(json, "webhook_secret").as_deref(), Some("whsec_abc"));
|
|
109
|
+
assert_eq!(optional_field(json, "missing"), None);
|
|
110
|
+
assert_eq!(optional_field(json, "blank"), None);
|
|
111
|
+
assert_eq!(optional_field("not json", "webhook_secret"), None);
|
|
112
|
+
}
|
|
113
|
+
|
|
99
114
|
#[test]
|
|
100
115
|
fn a_master_secret_logs_in_as_postgres_on_the_default_port() {
|
|
101
116
|
let json = r#"{"username":"postgres","password":"abc123"}"#;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// Commerce as a host extension: the newsletter, payments, registrations and mail routes, their boot
|
|
2
|
+
// check and their boot-time secret, answered through `extension::HostExtension` instead of being
|
|
3
|
+
// named in `render` and `route`.
|
|
4
|
+
|
|
5
|
+
use super::{
|
|
6
|
+
checkout_enabled, newsletter, newsletter_send, payments_routes, registrations_list_route, resend_webhook, respond, session_secret,
|
|
7
|
+
};
|
|
8
|
+
use crate::extension::{Ctx, HostExtension, RateRule};
|
|
9
|
+
use crate::payments;
|
|
10
|
+
use crate::secrets::{self, AwsSecretFetcher};
|
|
11
|
+
use async_trait::async_trait;
|
|
12
|
+
use serde_json::Value;
|
|
13
|
+
|
|
14
|
+
pub struct Commerce;
|
|
15
|
+
|
|
16
|
+
fn checkout_domain() -> Option<String> {
|
|
17
|
+
std::env::var("HECKS_CHECKOUT_DOMAIN").ok()
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
#[async_trait]
|
|
21
|
+
impl HostExtension for Commerce {
|
|
22
|
+
// Registration/checkout/newsletter routes are opt-in: they only exist when HECKS_CHECKOUT_DOMAIN
|
|
23
|
+
// names this domain, since Event/Registration and the Payments::Payment chapter are declared
|
|
24
|
+
// against it specifically. The capability shapes are pinned by
|
|
25
|
+
// spec/fixtures/rust_host/checkout_fixture, which this module's tests run against.
|
|
26
|
+
async fn guest_route(&self, ctx: &Ctx<'_>) -> Option<Value> {
|
|
27
|
+
if !checkout_enabled(checkout_domain().as_deref(), &ctx.config.domain) {
|
|
28
|
+
return None;
|
|
29
|
+
}
|
|
30
|
+
let ir = crate::extension::loaded_ir();
|
|
31
|
+
// Newsletter subscribe shares this gate (same hecksagon as the registration aggregates) and
|
|
32
|
+
// is checked first since it needs none of the Payments::Payment/Event context checkout
|
|
33
|
+
// requires. The subscriber list is PII (email, names, status): only an Admin or Owner
|
|
34
|
+
// holding the account cookie may read it, like sending does.
|
|
35
|
+
if ctx.method == "GET" && ctx.path == "/newsletter/subscribers" && ir.and_then(crate::commerce_ir::newsletter_provider).is_some() {
|
|
36
|
+
let Some(domain_ir) = ir else {
|
|
37
|
+
return Some(respond(500, "text/plain", "HECKS_IR_PATH not set or unreadable — this domain has no web layer configured"));
|
|
38
|
+
};
|
|
39
|
+
if let Err(response) = newsletter_send::require_admin(domain_ir, ctx.cookies, &session_secret(), ctx.client).await {
|
|
40
|
+
return Some(response);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
let headers = ctx.event.and_then(|e| e.get("headers"));
|
|
44
|
+
if let Some(response) =
|
|
45
|
+
resend_webhook::resend_webhook_route(ctx.method, ctx.path, ctx.raw_body, headers, ctx.client, ctx.wasm_path, ctx.config, ctx.invoker).await
|
|
46
|
+
{
|
|
47
|
+
return Some(response);
|
|
48
|
+
}
|
|
49
|
+
if let Some(response) =
|
|
50
|
+
newsletter::newsletter_route(ctx.method, ctx.path, ctx.query, ctx.raw_body, ctx.client, ctx.wasm_path, ctx.config, ctx.invoker).await
|
|
51
|
+
{
|
|
52
|
+
return Some(response);
|
|
53
|
+
}
|
|
54
|
+
let stripe_signature = ctx
|
|
55
|
+
.event
|
|
56
|
+
.and_then(|e| e.get("headers"))
|
|
57
|
+
.and_then(|h| h.get("stripe-signature"))
|
|
58
|
+
.and_then(|v| v.as_str())
|
|
59
|
+
.unwrap_or("");
|
|
60
|
+
payments_routes(ir, ctx.method, ctx.path, ctx.raw_body, stripe_signature, ctx.client, ctx.wasm_path, ctx.config, ctx.invoker).await
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async fn account_route(&self, ctx: &Ctx<'_>) -> Option<Value> {
|
|
64
|
+
let domain_ir = ctx.domain_ir?;
|
|
65
|
+
let secret = session_secret();
|
|
66
|
+
// Sending a newsletter issue (web/newsletter_send.rs's own header): an Admin's or Owner's
|
|
67
|
+
// account cookie, unlike the guest newsletter routes served ahead of the account gate.
|
|
68
|
+
if newsletter_send::issue_action(ctx.method, ctx.path).is_some() {
|
|
69
|
+
return newsletter_send::issue_route(
|
|
70
|
+
ctx.method, ctx.path, domain_ir, ctx.raw_body, ctx.cookies, &secret, ctx.client, ctx.wasm_path, ctx.config, ctx.invoker,
|
|
71
|
+
)
|
|
72
|
+
.await;
|
|
73
|
+
}
|
|
74
|
+
if ctx.method == "GET" && ctx.path == "/registrations" {
|
|
75
|
+
return Some(registrations_list_route(domain_ir, ctx.cookies, &secret, ctx.client, ctx.wasm_path, ctx.config).await);
|
|
76
|
+
}
|
|
77
|
+
// The tenant's payment connection: same gate as the checkout routes, since only the
|
|
78
|
+
// HECKS_CHECKOUT_DOMAIN domain carries one — any other domain served by this binary falls
|
|
79
|
+
// through as unknown.
|
|
80
|
+
if payments::owns(ctx.method, ctx.path) && checkout_enabled(checkout_domain().as_deref(), &ctx.config.domain) {
|
|
81
|
+
return payments::route(
|
|
82
|
+
ctx.method, ctx.path, ctx.raw_body, ctx.cookies, &secret, domain_ir, &payments::PlatformConfig::load().await,
|
|
83
|
+
ctx.client, ctx.wasm_path, ctx.config, ctx.invoker,
|
|
84
|
+
)
|
|
85
|
+
.await;
|
|
86
|
+
}
|
|
87
|
+
None
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
fn rate_rules(&self) -> Vec<RateRule> {
|
|
91
|
+
vec![
|
|
92
|
+
RateRule { name: "subscribe", method: "POST", path: "/newsletter/subscribers", limit_env: "HECKS_RATE_LIMIT_SUBSCRIBE", default_limit: 10 },
|
|
93
|
+
RateRule { name: "register", method: "POST", path: "/registrations", limit_env: "HECKS_RATE_LIMIT_REGISTER", default_limit: 15 },
|
|
94
|
+
]
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Checkout on AWS keeps the business's payment keys in a named Secrets Manager secret; refuse
|
|
98
|
+
// before touching the database when none is named.
|
|
99
|
+
fn boot_check(&self, domain: &str) -> Result<(), String> {
|
|
100
|
+
payments::check_boot(checkout_enabled(checkout_domain().as_deref(), domain))
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// RESEND_SECRET_ID: a failed fetch only logs -- mail is optional, and a missing secret must not
|
|
104
|
+
// stop the host from serving everything else (resend.rs answers 503 without a key). The same
|
|
105
|
+
// secret may also carry `webhook_secret`, the signing secret of the Resend webhook that
|
|
106
|
+
// POST /webhooks/resend verifies; without it that route answers 503.
|
|
107
|
+
async fn boot_secrets(&self, fetcher: &AwsSecretFetcher) {
|
|
108
|
+
let Ok(resend_secret_id) = std::env::var("RESEND_SECRET_ID") else { return };
|
|
109
|
+
let fetched = fetcher.fetch_secret_string(&resend_secret_id).await.map_err(|e| format!("{e:#}"));
|
|
110
|
+
let read = fetched.and_then(|json| secrets::extract_field(&json, "api_key").map(|api_key| (api_key, secrets::optional_field(&json, "webhook_secret"))));
|
|
111
|
+
match read {
|
|
112
|
+
Ok((api_key, webhook_secret)) => unsafe {
|
|
113
|
+
std::env::set_var("RESEND_API_KEY", api_key);
|
|
114
|
+
if let Some(webhook_secret) = webhook_secret {
|
|
115
|
+
std::env::set_var("RESEND_WEBHOOK_SECRET", webhook_secret);
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
Err(e) => eprintln!("RESEND_SECRET_ID could not be read, so email sending stays off: {e}"),
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -2,7 +2,8 @@ use super::newsletter_send::{site_url, unsubscribe_token, unsubscribe_token_matc
|
|
|
2
2
|
use super::{instances_for, last_refusal, respond};
|
|
3
3
|
use crate::auth;
|
|
4
4
|
use crate::dispatch;
|
|
5
|
-
use crate::ir::
|
|
5
|
+
use crate::ir::ir;
|
|
6
|
+
use crate::commerce_ir::{newsletter_provider, NewsletterProvider};
|
|
6
7
|
use crate::journal::LineageConfig;
|
|
7
8
|
use crate::lambda_client::LambdaInvoker;
|
|
8
9
|
use crate::resend::{Email, Mailer};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
use super::{active_session_email, instances_for, last_refusal, percent_decode, respond};
|
|
2
2
|
use crate::auth;
|
|
3
3
|
use crate::dispatch;
|
|
4
|
-
use crate::
|
|
4
|
+
use crate::commerce_ir::{newsletter_issues_provider, newsletter_provider, NewsletterIssuesProvider, NewsletterProvider};
|
|
5
5
|
use crate::journal::LineageConfig;
|
|
6
6
|
use crate::lambda_client::LambdaInvoker;
|
|
7
7
|
use crate::resend::{Email, Mailer};
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
use super::instances_for;
|
|
6
6
|
use crate::dispatch;
|
|
7
|
-
use crate::
|
|
7
|
+
use crate::commerce_ir::PaymentsProvider;
|
|
8
8
|
use crate::journal::LineageConfig;
|
|
9
9
|
use crate::resend::{Email, Mailer};
|
|
10
10
|
use serde_json::{json, Value};
|
|
@@ -82,7 +82,7 @@ pub(super) async fn send_receipt(reference: &str, client: &Mutex<Client>, wasm_p
|
|
|
82
82
|
let Ok(read) = dispatch::read(client, wasm_path).await else {
|
|
83
83
|
return;
|
|
84
84
|
};
|
|
85
|
-
let binding = crate::
|
|
85
|
+
let binding = crate::commerce_ir::registrations_binding(&config.domain);
|
|
86
86
|
let registrations = instances_for(&read, &binding.registration_prefix());
|
|
87
87
|
let Some((_, registration)) = registrations.iter().find(|(id, _)| id == reference) else {
|
|
88
88
|
return;
|