Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/monetize

crates/monetize-server/src/estate_order/refeed.rs

//! **The ONE box feeds an already-laid estate its keys again** (MAIL-RELAY + NIGHT-PUBREPO,
//! 2026-10-28).
//!
//! A box is fed its estate keys at LAY time only. Two keys this box holds LATER never reached
//! the estates laid before them:
//!
//! * **the mains' mail relay** of the estate's OWN product (`<secrets>/secret.sold_smtp_url.<product>`,
//!   `…sold_smtp_from.<product>`, `…sold_smtp_ca.<product>`, [`super::relay_feed`]; owner 2026-10-28:
//!   Gunnar sites send as estate@gunnar.rs, Holger sites as estate@holger.rs): holger.rs, gunnar.rs
//!   and test2 were laid while this box held none, so their mains mail nothing (`site first-link`
//!   refused, admin invites unmailed);
//! * **a site's public repositories** (`[estates.<label>] public_repositories`, NIGHT-PUBREPO):
//!   holger.rs was laid before its list was declared;
//! * **a site's public face** (`[estates.<label>] public_site`, GRS-PUBLIC, live bug 2026-10-28):
//!   gunnar.rs was laid by its kit before TASKS-12 declared it, and a keep boot never feeds a
//!   front again, so its console was never told and https://gunnar.rs/ asked every reader to log
//!   in. Fed as `console_public_site = on | off`; a Gunnar main stores it as its console's switch.
//!
//! `monetize site refeed <site> [--only mail_relay|public_repositories|public_site]` (door `POST
//! /site/refeed`, `EstateOrder.Refeed`, the operator bearer alone) feeds them again over the door
//! the estate already pulls through (no new protocol): ONE signed `refeed` order
//! (`monetize_embed::fleet::Refeed`, key → value in the words of the lay's feed) queued for each
//! data role of the estate (the main and the twin), a newer one superseding an unacked older
//! one. The box takes each key it knows by name and refuses the order by name otherwise; its
//! primary's ack is the answer `site status` says ([`answer_of`]).
//!
//! * nothing to feed is refused by name, [`NONE`]; the public repositories asked by name
//!   (`--only public_repositories`) with none declared are fed as the empty list, no
//!   repository public (RA5-18: a repository taken out of the list is private again); the relay asked of a box that holds none for
//!   the estate's product (or half of one), [`NO_RELAY`], naming the product; a relay that does not parse, [`BAD_RELAY`]; a group this box does
//!   not feed, [`UNKNOWN`];
//! * an estate that is not laid and delivered, or whose boxes the fleet does not hold, is
//!   refused by name, [`NOT_LAID`];
//! * every feed is audited: the run's history and the server's log name the keys, the relay's
//!   host and port, its sender (masked) and the order's seq — never a secret value.

use std::collections::BTreeMap;

use super::{lifecycle, phase, StartRefusal, Transition};
use crate::state::AppState;

/// The refusal tag of a refeed with nothing to feed.
pub const NONE: &str = "[site-refeed-none]";
/// The refusal tag of the relay asked of a box that holds none for the mains it lays.
pub const NO_RELAY: &str = "[site-mail-relay-none]";
/// The refusal tag of a relay that does not parse.
pub const BAD_RELAY: &str = "[site-mail-relay-bad]";
/// The refusal tag of a group this box does not feed.
pub const UNKNOWN: &str = "[site-refeed-unknown]";
/// The refusal tag of an estate that is not laid and delivered (or not held by the fleet).
pub const NOT_LAID: &str = "[site-refeed-not-laid]";
/// The refusal tag of a group the estate's product's boxes do not take (its facts say so).
pub const NOT_TAKEN: &str = "[site-refeed-not-taken]";

/// **Whether `product`'s boxes take a site's public repositories**, as its facts say
/// (`Facts::takes_public_repositories`; a Gunnar box takes none: it refuses a refeed that carries
/// the key whole). A product with no lifecycle here is fed as declared, as before.
pub fn takes_public_repositories(product: &str) -> bool {
    lifecycle::find(product)
        .or_else(|| lifecycle::find(lifecycle::family_of(product)))
        .is_none_or(|l| l.facts().takes_public_repositories())
}

/// **Whether `product`'s boxes take a site's public-site switch in a refeed**, as its facts say
/// (`Facts::takes_public_site`: only a product that says so; a box that does not know the key
/// refuses the refeed whole).
pub fn takes_public_site(product: &str) -> bool {
    lifecycle::find(product)
        .or_else(|| lifecycle::find(lifecycle::family_of(product)))
        .is_some_and(|l| l.facts().takes_public_site())
}

/// The groups a refeed feeds (`--only`).
pub mod group {
    /// The mains' mail relay: `secret.smtp_url`, `secret.smtp_from` (and `secret.smtp_ca`).
    pub const MAIL_RELAY: &str = "mail_relay";
    /// The site's `public_repositories`.
    pub const PUBLIC_REPOSITORIES: &str = "public_repositories";
    /// The site's public face, `public_site` (GRS-PUBLIC): `console_public_site = on | off`.
    pub const PUBLIC_SITE: &str = "public_site";
    pub const ALL: [&str; 3] = [MAIL_RELAY, PUBLIC_REPOSITORIES, PUBLIC_SITE];
}

/// How long a queued refeed waits for the estate's boxes to pull it.
pub const REFEED_ORDER_TTL_MS: u64 = 24 * 60 * 60 * 1000;

/// **What `site refeed` asked**: the words (the keys' names, the relay's host and port, its
/// sender masked; never a secret) and the seq of the signed `refeed` order queued for the
/// estate's primary.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct RefeedAsked {
    pub words: String,
    pub asked: u64,
}

/// **`EstateOrder.Refeed`**: the keys this box holds for the estate `name` now (`only`: these
/// groups; empty: every one it holds), fed to its data roles as one signed order each.
pub fn feed(state: &AppState, name: &str, only: &[String]) -> Result<RefeedAsked, StartRefusal> {
    use monetize_embed::fleet::{kind, refeed as key, Refeed};
    let name = name.trim().to_ascii_lowercase();
    let only: Vec<String> = only
        .iter()
        .map(|g| g.trim().replace('-', "_"))
        .filter(|g| !g.is_empty())
        .collect();
    if let Some(g) = only.iter().find(|g| !group::ALL.contains(&g.as_str())) {
        return Err(StartRefusal::BadName(format!(
            "{UNKNOWN} {g:?} is no group this box feeds (it feeds {}): nothing was fed",
            group::ALL.join(", ")
        )));
    }
    let wants = |g: &str| only.is_empty() || only.iter().any(|o| o == g);
    let eo = &state.estates;
    let cfg = eo.cfg.as_ref().ok_or_else(|| {
        StartRefusal::NotSold("this monetize lays no estates ([estate_orders] is absent)".into())
    })?;
    let Some(run) = eo.get(&name).filter(|r| !phase::is_gone(&r.phase)) else {
        return Err(StartRefusal::NotFound(format!(
            "{name} is not an estate this box laid: nothing to feed"
        )));
    };
    if !phase::is_term(&run.phase) {
        return Err(StartRefusal::NotNow(format!("{NOT_LAID} {name} is not laid and delivered (it is {}): an estate laid from now on is fed its keys at lay; nothing was fed", run.phase)));
    }
    let not_held = |why: String| {
        StartRefusal::NotNow(format!(
            "{NOT_LAID} {name} cannot pull a refeed: {why}; nothing was fed"
        ))
    };
    let product = crate::fleet::run_product(state, &run).map_err(not_held)?;
    let (url_n, from_n, _) = super::sold_relay_names(&product);
    let family = lifecycle::family_of(&product).to_string();
    let mut keys = BTreeMap::new();
    let mut said = Vec::new();
    // The mains' mail relay of the estate's OWN product, as the lay feeds it.
    if wants(group::MAIL_RELAY) {
        match super::relay_feed(eo.relay_fed.as_deref(), &product) {
            Err(why) if !only.is_empty() => return Err(StartRefusal::NotNow(format!("{NO_RELAY} {why}: nothing was fed to {name}"))),
            Err(why) => eprintln!("monetize-server: site {name}: the refeed carries no mail relay: {why}"),
            Ok(relay) => match (relay.get(super::FEED_SMTP_URL), relay.get(super::FEED_SMTP_FROM)) {
                (Some(url), Some(from)) => {
                    let ca = relay.get(super::FEED_SMTP_CA).cloned().unwrap_or_default();
                    let at = monetize_mail::Mailer::with_root(url, from, (!ca.is_empty()).then_some(ca.as_bytes()))
                        .map(|m| m.relay().describe())
                        .map_err(|why| StartRefusal::BadName(format!("{BAD_RELAY} the relay this box holds for its mains does not parse ({why}): nothing was fed to {name}")))?;
                    said.push(format!("the mail relay {at} as {}", super::first_link::masked_in(from)));
                    keys.insert(key::SMTP_URL.to_string(), url.clone());
                    keys.insert(key::SMTP_FROM.to_string(), from.clone());
                    if !ca.is_empty() {
                        keys.insert(key::SMTP_CA.to_string(), ca);
                    }
                }
                _ if !only.is_empty() => {
                    return Err(StartRefusal::NotNow(format!(
                        "{NO_RELAY} this ONE box holds no relay for the {family} mains it lays (secret.{url_n} / secret.{from_n}): nothing was fed to {name}"
                    )))
                }
                _ => {}
            },
        }
    }
    // The site's public repositories, as its declaration says them now.
    if wants(group::PUBLIC_REPOSITORIES) && !takes_public_repositories(&product) {
        if !only.is_empty() {
            return Err(StartRefusal::NotNow(format!("{NOT_TAKEN} the {family} boxes take no public_repositories (the product's facts): nothing was fed to {name}")));
        }
        if !state.registry.public_repositories(&run.estate).is_empty() {
            eprintln!("monetize-server: site {name}: its public_repositories are not fed: the {family} boxes take none");
        }
    } else if wants(group::PUBLIC_REPOSITORIES) {
        let public = state.registry.public_repositories(&run.estate);
        // RA5-18: asked by name, an empty declaration is fed as it is — no repository public —
        // so a repository taken out of the list is made private again on the laid boxes.
        if public.is_empty() && !only.is_empty() {
            said.push(format!(
                "public_repositories none (no repository public: [estates.{}] declares none)",
                run.estate
            ));
            keys.insert(key::PUBLIC_REPOSITORIES.to_string(), String::new());
        } else if !public.is_empty() {
            said.push(format!("public_repositories {}", public.join(",")));
            keys.insert(key::PUBLIC_REPOSITORIES.to_string(), public.join(","));
        }
    }
    // The site's public face, as its declaration says it now (GRS-PUBLIC): asked by name it is
    // fed `on` or `off`; a plain refeed feeds it only where it is declared on.
    if wants(group::PUBLIC_SITE) && !takes_public_site(&product) {
        if !only.is_empty() {
            return Err(StartRefusal::NotNow(format!("{NOT_TAKEN} the {family} boxes take no public_site switch (the product's facts): nothing was fed to {name}")));
        }
    } else if wants(group::PUBLIC_SITE) {
        let on = state.registry.public_site(&run.estate);
        if on || !only.is_empty() {
            let word = if on { "on" } else { "off" };
            said.push(format!(
                "{} {word} ([estates.{}] public_site = {on})",
                key::PUBLIC_SITE,
                run.estate
            ));
            keys.insert(key::PUBLIC_SITE.to_string(), word.to_string());
        }
    }
    if keys.is_empty() {
        return Err(StartRefusal::NotNow(format!(
            "{NONE} this ONE box holds nothing to feed {name} again: no relay for the {family} mains it lays (secret.{url_n} / secret.{from_n}), no public_repositories and no public_site declared for it"
        )));
    }
    let fleet = run.fleet_name().to_string();
    let life = super::terms_of(state, cfg, &run)
        .and_then(|c| lifecycle::of(&c))
        .or_else(|_| lifecycle::of(cfg))
        .map_err(StartRefusal::NotSold)?;
    let primary = life.primary_role();
    let mut roles: Vec<String> = life.data_roles().into_iter().map(|d| d.role).collect();
    if !roles.contains(&primary) {
        roles.insert(0, primary.clone());
    }
    let not_held = |why: String| {
        StartRefusal::NotNow(format!(
            "{NOT_LAID} {name}'s {primary} cannot pull a refeed: {why}; nothing was fed"
        ))
    };
    let g = state
        .fleet
        .guardian()
        .ok_or_else(|| not_held("the fleet store is not open".into()))?;
    if g.store.product_of(&fleet).as_deref() != Some(product.as_str()) {
        return Err(not_held(format!(
            "the fleet does not hold {fleet} as a laid estate"
        )));
    }
    let body = serde_json::to_string(&Refeed { keys: keys.clone() })
        .map_err(|e| not_held(e.to_string()))?;
    let _pen = eo.pen_for_a_call(cfg, &run.company)?;
    let now = state.now();
    let mut asked = 0;
    let mut seqs = Vec::new();
    for role in &roles {
        let order = monetize_guardian::Guardian::order(
            &product,
            &fleet,
            role,
            kind::REFEED,
            body.clone(),
            now,
            REFEED_ORDER_TTL_MS,
        );
        let queued = g
            .queue_order(order, |o| o.kind == kind::REFEED && &o.role == role)
            .map_err(|e| not_held(e.to_string()))?;
        if *role == primary {
            asked = queued.seq;
        }
        seqs.push(format!("{role} {}", queued.seq));
    }
    let what = said.join(" and ");
    let to = roles.join(" and ");
    let words = format!("{what} fed again to {fleet}'s {to}: signed orders {} ({}); nothing is in use until the {primary} answers — `site status {name}` says its answer", seqs.join(", "), kind::REFEED);
    if let Some(mut r) = eo.get(&run.company) {
        let p = r.phase.clone();
        r.history.push(Transition { phase: p, at_ms: now, note: format!("the operator fed {fleet}'s {to} again ({}): {what}: signed orders {} ({}) queued to pull", keys.keys().cloned().collect::<Vec<_>>().join(", "), seqs.join(", "), kind::REFEED) });
        super::save(state, &r).map_err(StartRefusal::NotSold)?;
    }
    eprintln!("monetize-server: AUDIT site {name}: the operator fed {fleet}'s {to} again ({}): {what} (signed orders {}, {})", keys.keys().cloned().collect::<Vec<_>>().join(", "), seqs.join(", "), kind::REFEED);
    Ok(RefeedAsked { words, asked })
}

/// **The estate's primary's newest answer to a `refeed`**: `(seq, kept, words)` — the seq of
/// the newest order it answered (0: none), whether it kept the keys, and its answer as one
/// sentence, every address masked. `(0, false, "")` when nothing was ever fed.
pub fn answer_of(state: &AppState, run: &super::EstateRun) -> (u64, bool, String) {
    use monetize_embed::fleet::kind;
    let fleet = run.fleet_name();
    let role = super::run_terms(state, run)
        .and_then(|t| lifecycle::of(&t).ok())
        .map(|l| l.primary_role());
    let (Some(g), Some(role)) = (state.fleet.guardian(), role) else {
        return (0, false, String::new());
    };
    let queued = g
        .store
        .orders_for(fleet, &role, 0)
        .into_iter()
        .filter(|o| o.kind == kind::REFEED)
        .map(|o| o.seq)
        .max();
    let acked = g.store.box_record(fleet, &role).and_then(|b| {
        b.acks
            .into_iter()
            .filter(|a| a.kind == kind::REFEED)
            .max_by_key(|a| a.seq)
    });
    let name = if run.estate.is_empty() {
        &run.company
    } else {
        &run.estate
    };
    let (seq, kept, words) = match (queued, acked) {
        (Some(q), a) if a.as_ref().is_none_or(|a| a.seq < q) => {
            let (seq, kept) = a.map_or((0, false), |a| (a.seq, a.ok));
            (seq, kept, format!("{name}'s {role} was fed its keys again (order {q}); it has not pulled the order yet"))
        }
        (_, Some(a)) if a.ok => (
            a.seq,
            true,
            format!(
                "{name}'s {role} kept its keys (order {}): {}",
                a.seq, a.detail
            ),
        ),
        (_, Some(a)) => (
            a.seq,
            false,
            format!(
                "{name}'s {role} refused its keys (order {}): {}",
                a.seq, a.detail
            ),
        ),
        _ => return (0, false, String::new()),
    };
    (seq, kept, super::first_link::masked_in(&words))
}