Skip to main content

holger_fleet/
order.rs

1//! **The orders a Holger box pulls**, decoded and checked.
2//!
3//! The kinds are monetize's (`monetize_embed::fleet::kind` for the repair verbs,
4//! `monetize_product::pull::order` for the product's). The signature is monetize-embed's
5//! `monetize_embed::fleet::order_message` under the product's monetize key: an order that does not verify is
6//! FORGED — never run, never acked, and it never moves the sequence (a forged high seq
7//! must not silence the real ones). Everything else that is wrong with an order is a
8//! REFUSAL, acked by name so the guardian learns why.
9
10use monetize_embed::fleet::{kind, GrowGoAhead, SignedOrder};
11use monetize_embed::signing::Snapshot;
12use monetize_embed::VerifyingKey;
13use monetize_estate_side::pump::Checked;
14use monetize_product::pull::{order as pkind, AddPrincipal, GrowRoom, PurgeTenant};
15use monetize_product::EntitlementFact;
16use serde::Deserialize;
17
18/// Who this box is on the wire: the shared pump's own (P4a).
19pub use monetize_estate_side::pump::Addressee;
20
21/// A recharge, as ordered.
22#[derive(Debug, Clone, PartialEq, Eq, Default, Deserialize)]
23#[serde(default)]
24pub struct Recharge {
25    /// The main to fill (its control plane, `host:port`).
26    pub main: String,
27    /// The twin it is filled from.
28    pub twin: String,
29    pub accept_lost: bool,
30    pub epoch: u64,
31    /// The old primary THIS box fences first; the recharge runs only when that fence held
32    /// ([`crate::exec`]).
33    pub fence: Option<String>,
34    /// The guardian's proof the old primary is fenced AT THE CLOUD.
35    pub fenced: Option<String>,
36    pub case: Option<String>,
37}
38
39/// A decoded order body.
40#[derive(Debug, Clone, PartialEq)]
41pub enum Order {
42    /// One repository's signed entitlement fact (in holger a tenant IS a repository).
43    Entitlement(EntitlementFact),
44    /// Monetize's books put back on a recharged main.
45    RestateEntitlements(Snapshot),
46    /// Delete a repository for non-payment (or ask how its purge stands).
47    PurgeTenant(PurgeTenant),
48    /// Room was sold past the set: queued, the operator told.
49    GrowRoom(GrowRoom),
50    /// A sold estate's paid data step.
51    GrowGoAhead(GrowGoAhead),
52    RechargeFromTwin(Recharge),
53    FencePrimary {
54        primary: String,
55        epoch: u64,
56    },
57    AdoptTwin {
58        twin: String,
59        principal: String,
60    },
61    /// A freshly laid box is TOLD it is the twin of `primary` (holger installs with no role).
62    BecomeTwin {
63        primary: String,
64    },
65    ReviewTwin,
66    /// A paid signup's account: in holger, a REPOSITORY on a plan (`username` names it; the
67    /// plan arrives as its entitlement fact). Idempotent per `signup`.
68    AddPrincipal(AddPrincipal),
69    /// A sold estate's resend sign-in link (Gunnar's S10): mint and mail it here.
70    SignInAgain(SignInAgain),
71    /// **Estate keys fed again** (monetize MAIL-RELAY + NIGHT-PUBREPO, 2026-10-28): each one
72    /// Holger takes again ([`refeed_key::KNOWN`]), kept beside the feed the box was laid with.
73    Refeed(Refeed),
74}
75
76/// **The estate keys a `refeed` may carry that Holger takes again** (monetize-embed
77/// `fleet::refeed`, named here: a lone clone's crates.io monetize-embed predates it).
78pub mod refeed_key {
79    pub const SMTP_URL: &str = "secret.smtp_url";
80    pub const SMTP_FROM: &str = "secret.smtp_from";
81    pub const SMTP_CA: &str = "secret.smtp_ca";
82    pub const PUBLIC_REPOSITORIES: &str = "public_repositories";
83    /// **The site's public-site switch** (`on | off`; HOL-PUBSITE-FEED, Gunnar's GRS-PUBLIC):
84    /// the lay's own word (holger-estate-front `console::PUBLIC_SITE_FEED`, monetize-embed
85    /// `fleet::refeed::PUBLIC_SITE`). At lay it reaches the front; in a refeed it reaches main
86    /// and twin, which keep it as the console's generic setting ([`super::PUBLIC_SITE_SETTING`]).
87    pub const PUBLIC_SITE: &str = "console_public_site";
88    pub const KNOWN: [&str; 5] = [
89        SMTP_URL,
90        SMTP_FROM,
91        SMTP_CA,
92        PUBLIC_REPOSITORIES,
93        PUBLIC_SITE,
94    ];
95    /// The keys a box keeps as its own generic settings ([`super::Refeed::settings`]), never
96    /// beside the feed ([`super::Refeed::kept_keys`]).
97    pub const AS_SETTINGS: [&str; 1] = [PUBLIC_SITE];
98}
99
100/// **The generic holger setting the console keeps its public-site switch under**
101/// (holger-public-site `setting::NAME`; the stored position wins over the front's feed).
102/// holger-server gives the name no meaning: a box keeps a refeed's switch under it as an
103/// administrator's `/-/settings/console.public-site` write would, audited the same way.
104pub const PUBLIC_SITE_SETTING: &str = "console.public-site";
105
106/// Body of a `refeed` order (monetize-embed `fleet::Refeed`, named here): key → value, the lay's
107/// feed words. **A `secret.` value is a secret**: the `Debug` names the keys only.
108#[derive(Clone, PartialEq, Eq, Default, Deserialize, serde::Serialize)]
109#[serde(default)]
110pub struct Refeed {
111    pub keys: std::collections::BTreeMap<String, String>,
112}
113
114impl std::fmt::Debug for Refeed {
115    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
116        f.debug_struct("Refeed")
117            .field("keys", &self.keys.keys().collect::<Vec<_>>())
118            .finish()
119    }
120}
121
122impl Refeed {
123    /// A key's value, trimmed; blank is unset.
124    pub fn get(&self, key: &str) -> Option<&str> {
125        self.keys
126            .get(key)
127            .map(|v| v.trim())
128            .filter(|v| !v.is_empty())
129    }
130
131    /// The public-site switch this refeed carries (`Some(true)` on, `Some(false)` off); `None`
132    /// when it carries none, or a word that is neither (the decode refuses that one by name).
133    pub fn public_site(&self) -> Option<bool> {
134        match self
135            .keys
136            .get(refeed_key::PUBLIC_SITE)
137            .map(|v| v.trim().to_ascii_lowercase())
138            .as_deref()
139        {
140            Some("on") => Some(true),
141            Some("off") => Some(false),
142            _ => None,
143        }
144    }
145
146    /// **The box's generic settings this refeed sets**, (name, value): the public-site switch
147    /// as [`PUBLIC_SITE_SETTING`] = `on | off`, the bytes an administrator's flip stores.
148    pub fn settings(&self) -> Vec<(&'static str, Vec<u8>)> {
149        self.public_site()
150            .map(|on| {
151                vec![(
152                    PUBLIC_SITE_SETTING,
153                    if on { b"on".to_vec() } else { b"off".to_vec() },
154                )]
155            })
156            .into_iter()
157            .flatten()
158            .collect()
159    }
160
161    /// The keys a box keeps beside its feed: every key but the ones kept as settings
162    /// ([`refeed_key::AS_SETTINGS`]).
163    pub fn kept_keys(&self) -> Refeed {
164        Refeed {
165            keys: self
166                .keys
167                .iter()
168                .filter(|(k, _)| !refeed_key::AS_SETTINGS.contains(&k.as_str()))
169                .map(|(k, v)| (k.clone(), v.clone()))
170                .collect(),
171        }
172    }
173}
174
175/// Body of a `sign_in_again` order (monetize-embed `fleet::SignInAgain`, named here: a lone
176/// clone's crates.io monetize-embed predates it). Never a secret.
177#[derive(Debug, Clone, PartialEq, Eq, Default, Deserialize)]
178#[serde(default)]
179pub struct SignInAgain {
180    /// The account whose first passkey the invite enrols (the order's account).
181    pub account: String,
182    /// The order's address: where the estate mails the link.
183    pub to: String,
184    /// The console's relying-party host the link points at (`<company>.<apex>`).
185    pub host: String,
186    /// How long the invite lives (0: the default).
187    pub ttl_secs: u64,
188}
189
190/// Holger's own order kinds beside monetize-embed's repair verbs (monetize
191/// `products/holger/src/guardian.rs`).
192pub mod holger_kind {
193    pub const BECOME_TWIN: &str = "become_twin";
194    /// monetize-product `pull::order::REFEED` (a core kind since MAIL-RELAY 2026-10-28), named
195    /// here: a lone clone's crates.io monetize-product predates it.
196    pub const REFEED: &str = "refeed";
197}
198
199#[derive(Deserialize, Default)]
200#[serde(default)]
201struct PrimaryBody {
202    primary: String,
203}
204
205/// What [`accept`] made of one wire order.
206#[derive(Debug, Clone, PartialEq)]
207pub enum Verdict {
208    /// Not monetize's: never run, never acked, the sequence does not move.
209    Forged(String),
210    /// Monetize's, but not for this box to run now: acked not-ok with these words.
211    Refused(String),
212    Run(Order),
213}
214
215#[derive(Deserialize)]
216struct RestateBody {
217    snapshot: Snapshot,
218}
219
220#[derive(Deserialize, Default)]
221#[serde(default)]
222struct FenceBody {
223    primary: String,
224    epoch: u64,
225}
226
227#[derive(Deserialize, Default)]
228#[serde(default)]
229struct AdoptBody {
230    twin: String,
231    principal: String,
232}
233
234/// **The box's check**: the shared pump's ([`monetize_estate_side::pump::check`]: signed by
235/// monetize, for this box, newer than the last order taken, inside its window), then a kind
236/// Holger runs ([`decode`]).
237pub fn accept(
238    o: &SignedOrder,
239    me: &Addressee,
240    key: &VerifyingKey,
241    last_seq: u64,
242    now_unix_ms: u64,
243) -> Verdict {
244    match monetize_estate_side::pump::check(o, me, key, last_seq, now_unix_ms) {
245        Checked::Forged(why) => Verdict::Forged(why),
246        Checked::Refused(why) => Verdict::Refused(why),
247        Checked::Fresh => match decode(o.seq, &o.kind, &o.body) {
248            Ok(order) => Verdict::Run(order),
249            Err(why) => Verdict::Refused(why),
250        },
251    }
252}
253
254/// **The body of one kind**, or the refusal by name.
255pub fn decode(seq: u64, kind: &str, body: &str) -> Result<Order, String> {
256    let body = if body.trim().is_empty() { "{}" } else { body };
257    let bad =
258        |e: serde_json::Error| format!("order {seq} ({kind}) has a body that does not decode: {e}");
259    Ok(match kind {
260        pkind::ENTITLEMENT => Order::Entitlement(serde_json::from_str(body).map_err(bad)?),
261        kind::RESTATE_ENTITLEMENTS => Order::RestateEntitlements(serde_json::from_str::<RestateBody>(body).map_err(bad)?.snapshot),
262        pkind::PURGE_TENANT => {
263            let p: PurgeTenant = serde_json::from_str(body).map_err(bad)?;
264            if p.tenant.trim().is_empty() {
265                return Err(format!("order {seq} is a purge_tenant that names no repository"));
266            }
267            Order::PurgeTenant(p)
268        }
269        pkind::GROW_ROOM => {
270            let g: GrowRoom = serde_json::from_str(body).map_err(bad)?;
271            if g.need_bytes == 0 {
272                return Err(format!("order {seq} is a grow_room that wants nothing"));
273            }
274            Order::GrowRoom(g)
275        }
276        kind::GROW_GO_AHEAD => {
277            let g: GrowGoAhead = serde_json::from_str(body).map_err(bad)?;
278            if g.go_ahead.trim().is_empty() || g.data_gib == 0 {
279                return Err(format!("order {seq} is a grow_go_ahead without monetize's signed step or a size"));
280            }
281            Order::GrowGoAhead(g)
282        }
283        kind::RECHARGE_FROM_TWIN => {
284            let mut r: Recharge = serde_json::from_str(body).map_err(bad)?;
285            r.fence = r.fence.filter(|f| !f.trim().is_empty());
286            r.fenced = r.fenced.filter(|f| !f.trim().is_empty());
287            if r.main.trim().is_empty() || r.twin.trim().is_empty() {
288                return Err(format!("order {seq} is a recharge_from_twin that names no main or no twin"));
289            }
290            Order::RechargeFromTwin(r)
291        }
292        kind::FENCE_PRIMARY => {
293            let b: FenceBody = serde_json::from_str(body).map_err(bad)?;
294            Order::FencePrimary { primary: b.primary, epoch: b.epoch }
295        }
296        kind::ADOPT_TWIN => {
297            let b: AdoptBody = serde_json::from_str(body).map_err(bad)?;
298            if b.twin.trim().is_empty() {
299                return Err(format!("order {seq} is an adopt_twin that names no twin"));
300            }
301            Order::AdoptTwin { twin: b.twin, principal: b.principal }
302        }
303        holger_kind::BECOME_TWIN => {
304            let b: PrimaryBody = serde_json::from_str(body).map_err(bad)?;
305            if b.primary.trim().is_empty() {
306                return Err(format!("order {seq} is a {kind} that names no primary"));
307            }
308            Order::BecomeTwin { primary: b.primary }
309        }
310        kind::REVIEW_TWIN => Order::ReviewTwin,
311        pkind::ADD_PRINCIPAL => {
312            let a: AddPrincipal = serde_json::from_str(body).map_err(bad)?;
313            if a.username.trim().is_empty() || a.signup.trim().is_empty() {
314                return Err(format!("order {seq} is an add_principal that names no repository or no signup"));
315            }
316            Order::AddPrincipal(a)
317        }
318        pkind::ADD_USERS => return Err(format!("order {seq}: Holger has no seats to grant on an account (a Holger tenant is a repository), so add_users is refused")),
319        kind::SIGN_IN_AGAIN => {
320            let b: SignInAgain = serde_json::from_str(body).map_err(bad)?;
321            if let Err(why) = crate::sign_in::check(&b.account, &b.to, &b.host) {
322                return Err(format!("order {seq} is a sign_in_again this box cannot mail: {why}"));
323            }
324            Order::SignInAgain(b)
325        }
326        holger_kind::REFEED => {
327            // The body may carry the relay's token: its decode error is not echoed.
328            let r: Refeed = serde_json::from_str(body).map_err(|_| format!("order {seq} (refeed) has a body that does not decode as estate keys"))?;
329            if r.keys.is_empty() {
330                return Err(format!("order {seq} is a refeed that names no estate key"));
331            }
332            if let Some(k) = r.keys.keys().find(|k| !refeed_key::KNOWN.contains(&k.as_str())) {
333                return Err(format!("order {seq} is a refeed of the estate key {k:?}, which Holger does not take again (it takes {}): nothing was kept", refeed_key::KNOWN.join(", ")));
334            }
335            match (r.get(refeed_key::SMTP_URL), r.get(refeed_key::SMTP_FROM)) {
336                (Some(url), Some(_)) if !(url.starts_with("smtp://") || url.starts_with("smtps://")) => return Err(format!("order {seq} is a refeed whose {} is not smtp:// or smtps://", refeed_key::SMTP_URL)),
337                (Some(_), Some(_)) => {}
338                (None, None) if r.get(refeed_key::SMTP_CA).is_none() => {}
339                _ => return Err(format!("order {seq} is a refeed with half a mail relay ({} and {} go together)", refeed_key::SMTP_URL, refeed_key::SMTP_FROM)),
340            }
341            // The public-site switch is `on` or `off` (HOL-PUBSITE-FEED), never another word.
342            if let Some(v) = r.keys.get(refeed_key::PUBLIC_SITE) {
343                if r.public_site().is_none() {
344                    return Err(format!("order {seq} is a refeed of {} = {:?}, which is neither `on` nor `off`: nothing was kept", refeed_key::PUBLIC_SITE, v.trim()));
345                }
346            }
347            // An empty list is "none public" (RA5-18: the ONE box feeds a cleared list as "").
348            if let Some(list) = r.keys.get(refeed_key::PUBLIC_REPOSITORIES).filter(|l| !l.trim().is_empty()) {
349                if let Some(bad) = list.split(',').map(str::trim).find(|n| n.is_empty() || !n.bytes().all(|c| c.is_ascii_alphanumeric() || matches!(c, b'-' | b'_' | b'.'))) {
350                    return Err(format!("order {seq} is a refeed whose {} names {bad:?}: a repository is one plain word", refeed_key::PUBLIC_REPOSITORIES));
351                }
352            }
353            Order::Refeed(r)
354        }
355        other => return Err(format!("order {seq}: Holger does not know the order kind {other:?}")),
356    })
357}
358
359#[cfg(test)]
360pub(crate) mod tests {
361    use super::*;
362    use ed25519_dalek::Signer as _;
363    use monetize_embed::fleet::order_message;
364
365    pub const MONETIZE_SEED: [u8; 32] = [7u8; 32];
366
367    pub fn me(role: &str) -> Addressee {
368        Addressee {
369            product: "holger".into(),
370            instance: "acme".into(),
371            role: role.into(),
372        }
373    }
374
375    pub fn monetize_key() -> VerifyingKey {
376        ed25519_dalek::SigningKey::from_bytes(&MONETIZE_SEED).verifying_key()
377    }
378
379    pub fn signed(seq: u64, role: &str, kind: &str, body: &str, signer: [u8; 32]) -> SignedOrder {
380        let mut o = SignedOrder {
381            seq,
382            product: "holger".into(),
383            instance: "acme".into(),
384            role: role.into(),
385            kind: kind.into(),
386            body: body.into(),
387            issued_unix_ms: 9_000,
388            not_after_unix_ms: 0,
389            signature: Vec::new(),
390        };
391        o.signature = ed25519_dalek::SigningKey::from_bytes(&signer)
392            .sign(&order_message(&o))
393            .to_bytes()
394            .to_vec();
395        o
396    }
397
398    #[test]
399    fn a_forged_order_is_forged_whatever_it_claims() {
400        let o = signed(9, "main", "review_twin", "{}", [9u8; 32]);
401        assert!(matches!(
402            accept(&o, &me("main"), &monetize_key(), 0, 10_000),
403            Verdict::Forged(_)
404        ));
405    }
406
407    #[test]
408    fn an_order_for_the_twin_a_replay_and_an_expired_one_are_refused_by_name() {
409        let k = monetize_key();
410        let twin = signed(2, "twin", "review_twin", "{}", MONETIZE_SEED);
411        assert!(
412            matches!(accept(&twin, &me("main"), &k, 0, 10_000), Verdict::Refused(w) if w.contains("twin"))
413        );
414        let old = signed(2, "main", "review_twin", "{}", MONETIZE_SEED);
415        assert!(
416            matches!(accept(&old, &me("main"), &k, 2, 10_000), Verdict::Refused(w) if w.contains("replay"))
417        );
418        let mut late = signed(3, "main", "review_twin", "{}", MONETIZE_SEED);
419        late.not_after_unix_ms = 5_000;
420        late.signature = ed25519_dalek::SigningKey::from_bytes(&MONETIZE_SEED)
421            .sign(&order_message(&late))
422            .to_bytes()
423            .to_vec();
424        assert!(
425            matches!(accept(&late, &me("main"), &k, 0, 10_000), Verdict::Refused(w) if w.contains("expired"))
426        );
427    }
428
429    /// ★ **HOL-PUBSITE-FEED (Gunnar's GRS-PUBLIC): a `refeed` carries the site's public-site
430    /// switch** (`console_public_site = on | off`, the lay's own word), alone or beside the other
431    /// keys; a value that is neither `on` nor `off` is refused by name and nothing is kept.
432    #[test]
433    fn a_refeed_carries_the_public_site_switch_and_a_switch_that_is_neither_on_nor_off_is_refused()
434    {
435        for (body, want) in [
436            (r#"{"keys":{"console_public_site":"on"}}"#, true),
437            (r#"{"keys":{"console_public_site":" OFF "}}"#, false),
438            (
439                r#"{"keys":{"console_public_site":"on","public_repositories":"crates"}}"#,
440                true,
441            ),
442        ] {
443            let o = signed(7, "main", "refeed", body, MONETIZE_SEED);
444            match accept(&o, &me("main"), &monetize_key(), 0, 10_000) {
445                Verdict::Run(Order::Refeed(r)) => {
446                    assert_eq!(r.public_site(), Some(want), "{body}");
447                    assert_eq!(
448                        r.settings(),
449                        vec![(
450                            PUBLIC_SITE_SETTING,
451                            if want {
452                                b"on".to_vec()
453                            } else {
454                                b"off".to_vec()
455                            }
456                        )],
457                        "{body}: the switch is the console's generic setting"
458                    );
459                    assert!(
460                        !r.kept_keys().keys.contains_key(refeed_key::PUBLIC_SITE),
461                        "{body}: the switch is no key kept beside the feed"
462                    );
463                }
464                other => panic!("{body}: {other:?}"),
465            }
466        }
467        let o = signed(
468            8,
469            "main",
470            "refeed",
471            r#"{"keys":{"console_public_site":"maybe"}}"#,
472            MONETIZE_SEED,
473        );
474        match accept(&o, &me("main"), &monetize_key(), 0, 10_000) {
475            Verdict::Refused(w) => assert!(
476                w.contains("refeed") && w.contains("console_public_site") && w.contains("maybe"),
477                "{w}"
478            ),
479            other => panic!("{other:?}"),
480        }
481    }
482
483    /// ★ **MAIL-RELAY + NIGHT-PUBREPO: a `refeed` is run with the keys it carries**, each taken
484    /// by name: the relay and the public repositories. A key Holger does not take again, half a
485    /// relay, a relay that is not `smtp://`/`smtps://`, a repository that is not one plain word,
486    /// and no key at all are refused by name, never echoing the token; Debug names keys only.
487    #[test]
488    fn a_refeed_is_run_with_the_keys_it_carries_and_a_key_holger_does_not_take_is_refused_by_name()
489    {
490        let body = r#"{"keys":{"secret.smtp_url":"smtps://holger%40holger.rs:t0kenVALUE@relay.mock.invalid:465","secret.smtp_from":"Holger <holger@mock.invalid>","public_repositories":"crates,npm"}}"#;
491        let o = signed(4, "main", "refeed", body, MONETIZE_SEED);
492        match accept(&o, &me("main"), &monetize_key(), 0, 10_000) {
493            Verdict::Run(Order::Refeed(r)) => {
494                assert_eq!(
495                    (
496                        r.get(refeed_key::SMTP_FROM),
497                        r.get(refeed_key::PUBLIC_REPOSITORIES)
498                    ),
499                    (Some("Holger <holger@mock.invalid>"), Some("crates,npm"))
500                );
501                assert!(!format!("{r:?}").contains("t0kenVALUE"), "{r:?}");
502            }
503            other => panic!("{other:?}"),
504        }
505        let o = signed(
506            5,
507            "main",
508            "refeed",
509            r#"{"keys":{"public_repositories":"crates"}}"#,
510            MONETIZE_SEED,
511        );
512        assert!(
513            matches!(
514                accept(&o, &me("main"), &monetize_key(), 0, 10_000),
515                Verdict::Run(Order::Refeed(_))
516            ),
517            "the public list alone"
518        );
519        // RA5-18: a cleared list ("": none public, monetize 523a41ac) is a list, never refused.
520        let o = signed(
521            5,
522            "main",
523            "refeed",
524            r#"{"keys":{"public_repositories":""}}"#,
525            MONETIZE_SEED,
526        );
527        match accept(&o, &me("main"), &monetize_key(), 0, 10_000) {
528            Verdict::Run(Order::Refeed(r)) => assert_eq!(
529                r.keys
530                    .get(refeed_key::PUBLIC_REPOSITORIES)
531                    .map(String::as_str),
532                Some(""),
533                "none public"
534            ),
535            other => panic!("a cleared public list is refused: {other:?}"),
536        }
537        for (bad, word) in [
538            (r#"{"keys":{}}"#, "names no estate key"),
539            (
540                r#"{"keys":{"console_admin_token":"t0kenVALUE"}}"#,
541                "console_admin_token",
542            ),
543            (
544                r#"{"keys":{"secret.smtp_url":"smtps://u:t0kenVALUE@r.mock.invalid"}}"#,
545                "half a mail relay",
546            ),
547            (
548                r#"{"keys":{"secret.smtp_url":"http://u:t0kenVALUE@r.mock.invalid","secret.smtp_from":"h@mock.invalid"}}"#,
549                "not smtp://",
550            ),
551            (
552                r#"{"keys":{"public_repositories":"crates,../x"}}"#,
553                "one plain word",
554            ),
555            (r#"{"keys":7}"#, "does not decode"),
556        ] {
557            let o = signed(6, "main", "refeed", bad, MONETIZE_SEED);
558            match accept(&o, &me("main"), &monetize_key(), 0, 10_000) {
559                Verdict::Refused(w) => assert!(
560                    w.contains("refeed") && w.contains(word) && !w.contains("t0kenVALUE"),
561                    "{bad}: {w}"
562                ),
563                other => panic!("{bad}: {other:?}"),
564            }
565        }
566    }
567
568    #[test]
569    fn holgers_missing_concepts_are_refused_by_name() {
570        let o = signed(
571            1,
572            "main",
573            "add_principal",
574            r#"{"username":"crates","kind":"org","signup":"s-1"}"#,
575            MONETIZE_SEED,
576        );
577        assert!(
578            matches!(accept(&o, &me("main"), &monetize_key(), 0, 10_000), Verdict::Run(Order::AddPrincipal(a)) if a.username == "crates" && a.signup == "s-1"),
579            "a signup is a repository on a plan"
580        );
581        let o = signed(
582            2,
583            "main",
584            "add_principal",
585            r#"{"username":"","signup":"s-1"}"#,
586            MONETIZE_SEED,
587        );
588        assert!(
589            matches!(accept(&o, &me("main"), &monetize_key(), 0, 10_000), Verdict::Refused(w) if w.contains("names no repository"))
590        );
591        for (kind, word) in [("add_users", "add_users"), ("make_coffee", "make_coffee")] {
592            let o = signed(
593                1,
594                "main",
595                kind,
596                r#"{"username":"a","signup":"s"}"#,
597                MONETIZE_SEED,
598            );
599            match accept(&o, &me("main"), &monetize_key(), 0, 10_000) {
600                Verdict::Refused(w) => assert!(w.contains(word), "{w}"),
601                other => panic!("{kind}: {other:?}"),
602            }
603        }
604    }
605
606    /// ★ **A sold estate's resend sign-in link is run here** (A16, Gunnar's S10): monetize's
607    /// signed `sign_in_again` names whose passkey, which address and which console host — never
608    /// a secret; a body a link or a mail header cannot carry is refused by name.
609    #[test]
610    fn a_sign_in_again_is_run_and_a_body_a_link_cannot_carry_is_refused() {
611        let body = r#"{"account":"acme","to":"buyer@mock.invalid","host":"acme.holger.mock.invalid","ttl_secs":86400}"#;
612        let o = signed(5, "main", "sign_in_again", body, MONETIZE_SEED);
613        match accept(&o, &me("main"), &monetize_key(), 0, 10_000) {
614            Verdict::Run(Order::SignInAgain(s)) => assert_eq!(
615                (
616                    s.account.as_str(),
617                    s.to.as_str(),
618                    s.host.as_str(),
619                    s.ttl_secs
620                ),
621                (
622                    "acme",
623                    "buyer@mock.invalid",
624                    "acme.holger.mock.invalid",
625                    86_400
626                )
627            ),
628            other => panic!("{other:?}"),
629        }
630        for bad in [
631            r#"{"account":"a&user=x","to":"b@mock.invalid","host":"h.mock.invalid"}"#,
632            r#"{"account":"a","to":"b@mock.invalid\r\nBcc: x@mock.invalid","host":"h.mock.invalid"}"#,
633            r#"{"account":"a","to":"b@mock.invalid","host":""}"#,
634        ] {
635            let o = signed(6, "main", "sign_in_again", bad, MONETIZE_SEED);
636            assert!(
637                matches!(accept(&o, &me("main"), &monetize_key(), 0, 10_000), Verdict::Refused(w) if w.contains("sign_in_again")),
638                "{bad}"
639            );
640        }
641    }
642}