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))
}