server/lib/src/refeed.rs
//! **The estate keys the ONE box fed this box again, kept** (monetize MAIL-RELAY + NIGHT-PUBREPO,
//! 2026-10-28).
//!
//! A box is fed its estate keys at LAY time, by the ONE box. Keys the ONE box holds LATER — the
//! mains' mail relay (`secret.smtp_url` / `secret.smtp_from` / `secret.smtp_ca`), the site's
//! `public_repositories` — reach a laid box as one signed `refeed` order over the pull channel
//! (`holger_fleet::order::Refeed`; holger-fleet refuses a key Holger does not take, by name). This
//! module keeps them.
//!
//! * kept at [`KEPT_REL`] under the data root, 0600, written whole (temp, fsync, rename) — the
//! custody the box's own fleet key has; a later refeed goes over an earlier one (a relay as a
//! whole, the public list as a whole);
//! * read over the feed in force ([`over`]): the kept keys are the newer ones (a lay feeds
//! first, the operator feeds again after), so they win. The pull channel's postman reads it at
//! every mail: a relay is in use from the next mail, with no restart. The boot reads the public
//! repositories through it (server-cli `paired::fed_public_repositories_at`): they are in force
//! from the next start;
//! * checked BEFORE anything is kept: a relay that does not parse, a public repository this box
//! does not serve or one that gates itself, are refused by name and nothing is kept; a token is
//! never logged, never in an answer.
use std::collections::BTreeMap;
use std::path::Path;
use holger_fleet::order::{refeed_key, Refeed};
/// Where the fed keys are kept under the data root.
pub const KEPT_REL: &str = "fleet/refeed.json";
/// The feed keys of the relay (monetize-mail `FEED_SMTP_*`, the lay's own words).
pub const FEED_SMTP_URL: &str = refeed_key::SMTP_URL;
pub const FEED_SMTP_FROM: &str = refeed_key::SMTP_FROM;
/// One more root the relay's certificate is trusted under (PEM; the ONE box's `secret.smtp_ca`).
pub const FEED_SMTP_CA: &str = refeed_key::SMTP_CA;
const RELAY: [&str; 3] = [FEED_SMTP_URL, FEED_SMTP_FROM, FEED_SMTP_CA];
/// Pure: **are `names` repositories this box may make public** — each one it serves
/// (`served`) and none that gates itself (`gated`); refused by name (server-lib `posture`).
pub fn public_check(names: &[String], served: &[String], gated: &[String]) -> Result<(), String> {
for n in names {
if !served.iter().any(|s| s == n) {
let declared: Vec<&str> = served.iter().map(String::as_str).collect();
return Err(crate::posture::public_unknown(n, &declared));
}
if gated.iter().any(|g| g == n) {
return Err(format!(
"{} `{n}` gates itself: it is never public",
crate::posture::ACCESS_REFUSED
));
}
}
Ok(())
}
/// **Keep the keys of `r`**, checked first: the relay as the shared sender reads it
/// (monetize-mail: `smtp://` STARTTLS or `smtps://`, a sender, the root a PEM), the public
/// repositories by `public_ok` (what this box serves now). Then written whole at [`KEPT_REL`],
/// 0600, over what was kept before. The answer names the keys and the relay's host and port, never
/// a secret value and never the sender's address (RA5-26: the answer is logged; no addresses in logs). A key that does not pass is refused by name and nothing is kept.
pub fn keep(
root: &Path,
r: &Refeed,
public_ok: &dyn Fn(&[String]) -> Result<(), String>,
) -> Result<String, String> {
// A key the box keeps as a generic setting (holger-fleet `refeed_key::AS_SETTINGS`) is never
// kept here: `HolgerBox::keep_setting` keeps it.
let r = &r.kept_keys();
let mut said = Vec::new();
if let Some(url) = r.get(FEED_SMTP_URL) {
let from = r.get(FEED_SMTP_FROM).unwrap_or_default();
let ca = r.get(FEED_SMTP_CA);
let mailer =
monetize_mail::Mailer::with_root(url, from, ca.map(str::as_bytes)).map_err(|why| {
format!("the refeed's mail relay cannot mail ({why}): nothing was kept")
})?;
said.push(format!(
"the mail relay {} is in use from the next mail, with the sender it was fed",
mailer.relay().describe()
));
}
let public = r
.keys
.get(refeed_key::PUBLIC_REPOSITORIES)
.map(|v| crate::posture::public_list_of(v));
if let Some(list) = &public {
public_ok(list).map_err(|why| format!("{why}: nothing was kept"))?;
said.push(format!(
"public repositories {} from the next start",
if list.is_empty() {
"none".to_string()
} else {
list.join(", ")
}
));
}
let mut kept = kept(root);
if r.get(FEED_SMTP_URL).is_some() {
kept.retain(|k, _| !RELAY.contains(&k.as_str()));
}
for (k, v) in &r.keys {
kept.insert(k.clone(), v.trim().to_string());
}
let body = serde_json::to_vec(&Refeed { keys: kept })
.map_err(|e| format!("the refeed could not be written: {e}"))?;
write_secret(&root.join(KEPT_REL), &body)
.map_err(|e| format!("the refeed could not be kept at {KEPT_REL}: {e}"))?;
let names = r.keys.keys().cloned().collect::<Vec<_>>().join(", ");
log::warn!(
"refeed: the ONE box fed this box {names} again; kept at {KEPT_REL}: {}",
said.join("; ")
);
Ok(format!("kept {names}: {}", said.join("; ")))
}
/// **The kept keys** (feed keys → values). Empty when nothing is kept, or the kept file does not
/// read (said).
pub fn kept(root: &Path) -> BTreeMap<String, String> {
let path = root.join(KEPT_REL);
let Ok(bytes) = std::fs::read(&path) else {
return BTreeMap::new();
};
match serde_json::from_slice::<Refeed>(&bytes) {
Ok(r) => r
.keys
.into_iter()
.filter(|(k, v)| {
refeed_key::KNOWN.contains(&k.as_str())
&& !refeed_key::AS_SETTINGS.contains(&k.as_str())
&& (k == refeed_key::PUBLIC_REPOSITORIES || !v.trim().is_empty())
})
.collect(),
Err(e) => {
log::error!("refeed: the kept keys at {KEPT_REL} do not read ({e}): the feed the box was laid with is used");
BTreeMap::new()
}
}
}
/// **The feed in force with the kept keys over it**: they are newer than what the lay fed, so
/// they win (a kept relay as a whole: a kept relay with no root drops the lay's root). Nothing
/// kept: the feed as it is.
pub fn over(mut feed: BTreeMap<String, String>, root: &Path) -> BTreeMap<String, String> {
let k = kept(root);
if k.contains_key(FEED_SMTP_URL) {
feed.retain(|key, _| !RELAY.contains(&key.as_str()));
}
feed.extend(k);
feed
}
/// `bytes` at `path`, 0600, written whole (temp beside it, fsync, rename).
fn write_secret(path: &Path, bytes: &[u8]) -> std::io::Result<()> {
use std::io::Write as _;
use std::os::unix::fs::OpenOptionsExt as _;
let dir = path.parent().unwrap_or(Path::new("."));
std::fs::create_dir_all(dir)?;
let tmp = dir.join(format!(
".{}.tmp",
path.file_name().and_then(|n| n.to_str()).unwrap_or("kept")
));
let _ = std::fs::remove_file(&tmp);
let mut f = std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.mode(0o600)
.open(&tmp)?;
f.write_all(bytes)?;
f.sync_all()?;
drop(f);
std::fs::rename(&tmp, path)?;
if let Ok(d) = std::fs::File::open(dir) {
let _ = d.sync_all();
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
const TOKEN: &str = "t0kenVALUEnever-shown";
fn refeed(keys: &[(&str, &str)]) -> Refeed {
Refeed {
keys: keys
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect(),
}
}
fn served(n: &[&str]) -> Vec<String> {
n.iter().map(|s| s.to_string()).collect()
}
/// ★ **A relay that does not parse is refused by name and nothing is kept**; the kept relay
/// goes over the lay's as a whole, and the rest of the feed stays.
#[test]
fn a_relay_that_does_not_parse_is_refused_and_the_kept_one_goes_over_the_lays() {
let dir = tempfile::tempdir().unwrap();
let any = |_: &[String]| Ok(());
let bad = refeed(&[
(
FEED_SMTP_URL,
&format!("http://u:{TOKEN}@relay.mock.invalid"),
),
(FEED_SMTP_FROM, "h@mock.invalid"),
]);
let e = keep(dir.path(), &bad, &any).unwrap_err();
assert!(e.contains("refeed") && !e.contains(TOKEN), "{e}");
assert!(!dir.path().join(KEPT_REL).exists(), "nothing kept");
assert!(kept(dir.path()).is_empty());
let lay = BTreeMap::from([
(
FEED_SMTP_URL.to_string(),
"smtp://old:old@old.mock.invalid:587".to_string(),
),
(FEED_SMTP_FROM.to_string(), "old@mock.invalid".to_string()),
(FEED_SMTP_CA.to_string(), "OLD CA".to_string()),
("fleet".to_string(), "f".to_string()),
]);
assert_eq!(
over(lay.clone(), dir.path()),
lay,
"nothing kept: the lay's feed as it is"
);
let url = format!("smtps://u:{TOKEN}@relay.mock.invalid:465");
let said = keep(
dir.path(),
&refeed(&[(FEED_SMTP_URL, &url), (FEED_SMTP_FROM, "new@mock.invalid")]),
&any,
)
.unwrap();
assert!(
said.contains("relay.mock.invalid:465") && !said.contains(TOKEN),
"{said}"
);
let f = over(lay, dir.path());
assert_eq!(
(
f[FEED_SMTP_URL].as_str(),
f[FEED_SMTP_FROM].as_str(),
f["fleet"].as_str()
),
(url.as_str(), "new@mock.invalid", "f")
);
assert!(
!f.contains_key(FEED_SMTP_CA),
"the kept relay as a whole: the lay's root is dropped"
);
}
/// ★ **RA5-26: the refeed never says the relay's sender address** (owner rule RA4-2: no
/// addresses in logs). The answer goes to the warn log and back in the order's answer, so it
/// names the relay's host and port and that a sender is set, never the address itself.
#[test]
fn the_refeed_never_says_the_relays_sender_address() {
let dir = tempfile::tempdir().unwrap();
let any = |_: &[String]| Ok(());
let said = keep(
dir.path(),
&refeed(&[
(FEED_SMTP_URL, "smtps://u:p@relay.mock.invalid:465"),
(FEED_SMTP_FROM, "estate-sender@mock.invalid"),
]),
&any,
)
.unwrap();
assert!(said.contains("relay.mock.invalid:465"), "{said}");
assert!(
!said.contains("estate-sender") && !said.contains('@'),
"the sender address is in the answer: {said}"
);
assert_eq!(
over(BTreeMap::new(), dir.path())
.get(FEED_SMTP_FROM)
.map(String::as_str),
Some("estate-sender@mock.invalid"),
"the sender is still kept"
);
}
/// ★ **NIGHT-PUBREPO through the refeed**: a public list is checked against what the box
/// serves before it is kept — a name it does not serve, and a repository that gates itself,
/// are refused by name and nothing is kept; a kept list goes over the lay's and a later relay
/// keeps it.
#[test]
fn public_repositories_are_checked_before_they_are_kept_and_go_over_the_lays() {
let dir = tempfile::tempdir().unwrap();
let box_ = served(&["crates", "npm", "bundles", "monetize-backup"]);
let gated = served(&["monetize-backup"]);
let check = |names: &[String]| public_check(names, &box_, &gated);
let e = keep(
dir.path(),
&refeed(&[(refeed_key::PUBLIC_REPOSITORIES, "crates,pypi")]),
&check,
)
.unwrap_err();
assert!(
e.starts_with(crate::posture::PUBLIC_UNKNOWN) && e.contains("pypi"),
"{e}"
);
let e = keep(
dir.path(),
&refeed(&[(refeed_key::PUBLIC_REPOSITORIES, "monetize-backup")]),
&check,
)
.unwrap_err();
assert!(e.starts_with(crate::posture::ACCESS_REFUSED), "{e}");
assert!(!dir.path().join(KEPT_REL).exists(), "nothing kept");
let said = keep(
dir.path(),
&refeed(&[(refeed_key::PUBLIC_REPOSITORIES, "crates,npm")]),
&check,
)
.unwrap();
assert!(
said.contains("public repositories crates, npm from the next start"),
"{said}"
);
let lay = BTreeMap::from([("fleet".to_string(), "f".to_string())]);
assert_eq!(
over(lay.clone(), dir.path())
.get(refeed_key::PUBLIC_REPOSITORIES)
.map(String::as_str),
Some("crates,npm")
);
keep(
dir.path(),
&refeed(&[
(FEED_SMTP_URL, "smtps://u:p@relay.mock.invalid:465"),
(FEED_SMTP_FROM, "h@mock.invalid"),
]),
&check,
)
.unwrap();
let f = over(lay, dir.path());
assert_eq!(
(
f.get(refeed_key::PUBLIC_REPOSITORIES).map(String::as_str),
f.get(FEED_SMTP_FROM).map(String::as_str)
),
(Some("crates,npm"), Some("h@mock.invalid")),
"a later relay keeps the list"
);
let mode = std::os::unix::fs::PermissionsExt::mode(
&std::fs::metadata(dir.path().join(KEPT_REL))
.unwrap()
.permissions(),
) & 0o777;
assert_eq!(mode, 0o600, "kept as a secret");
}
}