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

nordisk/gunnar

crates/gunnar-front/src/recharge.rs

//! **The front re-admits a lost box itself** (F4, the owner 2026-10-09): a SOLD estate keeps its
//! installer material in its own primary's sealed slot ([`crate::operator_door::INSTALLER_MATERIAL_SLOT`],
//! which the front TAKES from the shop after the sale — [`take_kept`], `SoldEstate.TakeMaterial`;
//! the shop never calls into the estate, ONE monetize G1), so when its `main` or
//! its `twin` is LOST the front is the estate's installer:
//!
//! 1. the shop does only the cloud half — `SoldEstate.Recharge` lays that ONE box again from the
//!    release medium, with the seed the front hands it (the kept seed, its bootstrap certificate
//!    minted fresh from the kept CA) as its `user_data`;
//! 2. the front re-admits it with nordisk-pairing's `readmit` over the installer's link —
//!    to the box's UTILITY address (the front and its boxes share that network; a guest of the
//!    UpCloud mock cannot reach a public address at all), its own door direct — and a recharged
//!    `main` is filled from its twin (`recharge_from`);
//! 3. the new ledger and plan go back into the slot, and the shop is told `done` (its release
//!    clone given back, its metadata off).
//!
//! The material is in this process only while it is used (it is never written to the front's
//! disk). [`watch`] decides WHEN: a box gone for [`GONE_FOR`] while the other gunnar box
//! answers; the shop refuses a box that still stands, so a link that merely broke lays nothing.

use std::collections::BTreeMap;
use std::net::Ipv4Addr;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};

use crate::shop_door::MATERIAL_VALID;

/// How the front reaches one box as its installer.
pub type BoxPath = Box<dyn nordisk_pairing::installer_link::Path>;
/// One box and whether its certificate was renewed.
pub type Renewed = (String, Result<bool, String>);

/// How long a box must be gone before the front asks the shop to lay it again.
pub const GONE_FOR: Duration = Duration::from_secs(180);
/// How often the front looks.
pub const LOOK_EVERY: Duration = Duration::from_secs(20);
/// After a refusal or a failed attempt, the next is no sooner than this.
pub const AGAIN_AFTER: Duration = Duration::from_secs(600);

/// **The kept bundle, its plan move, its paths and its re-admission are nordisk-pairing's**
/// (`kept`, A32-30: the same Holger's front uses; Gunnar's copy is gone). The installer's sink
/// says each line once (gunnar.rs 2026-10-25: 1.9 M nested `readmit:` lines).
pub use nordisk_pairing::kept::{moved, paths_for, readmit, Kept, MemSink, READMIT_BUDGET_S};

/// The seed name a Gunnar box is laid with (`seed.gunnar.toml` in the kept bundle).
pub const GUNNAR_SEED: &str = "gunnar";

/// Pure: **is it time to recharge `role`?** Gone for [`GONE_FOR`] while the OTHER gunnar box
/// answers (a whole estate unreachable is the front's own link, never two lost boxes), and no
/// attempt within [`AGAIN_AFTER`].
pub fn due(gone_for: Option<Duration>, other_answers: bool, last_try: Option<Duration>) -> bool {
    gone_for.is_some_and(|g| g >= GONE_FOR)
        && other_answers
        && last_try.is_none_or(|t| t >= AGAIN_AFTER)
}

/// The kept bundle, held in this process while the primary lives (read from it, or as the
/// shop handed it over): a lost primary's slot is read from here — or from the twin.
static HELD: Mutex<Option<Vec<u8>>> = Mutex::new(None);

/// Hold the bundle (the door's slot verb calls this as it stores it).
pub fn hold(bytes: &[u8]) {
    *HELD.lock().unwrap_or_else(|p| p.into_inner()) = Some(bytes.to_vec());
}

pub fn held() -> Option<Vec<u8>> {
    HELD.lock().unwrap_or_else(|p| p.into_inner()).clone()
}

/// **The fed shop door** (F4 under ONE monetize): the door, company, shop key and client-key
/// secret the front calls the ONE monetize IN through — fed on its own, with no monetize on the
/// estate (monetize-shop-pairing `contract::FEED_SHOP_DOOR`).
pub const FEED_SHOP_DOOR: &str = "shop_door.toml";

/// The product this front's calls to the ONE box are signed as on `Fleet`.
pub const PRODUCT: &str = "gunnar";

/// The shop's door from the fed [`FEED_SHOP_DOOR`], its client key from the fed
/// `secret.<client_key_secret>` beside it (a fed secret is a FEED line on this
/// front — nothing of it on disk, no monetize here to land it): `None` on an
/// estate that was not sold. The LEGACY fed `monetize.toml` fallback is gone
/// with the embedded monetize (K3, ONE monetize: the shop's pairing REFUSES a
/// sale feed that names `monetize.toml`).
pub fn shop_door(
    feed: &BTreeMap<String, String>,
) -> Option<Result<crate::shop_door::ShopDoor, String>> {
    let fed = feed.get(FEED_SHOP_DOOR)?;
    Some((|| {
        let t: toml::Table = toml::from_str(fed)
            .map_err(|e| format!("the fed {FEED_SHOP_DOOR} is not TOML: {e}"))?;
        let secret = t
            .get("client_key_secret")
            .and_then(|v| v.as_str())
            .unwrap_or(crate::shop_door::CLIENT_KEY_SECRET);
        let key = feed
            .get(&format!("secret.{secret}"))
            .ok_or_else(|| format!("the client key is not fed (`secret.{secret}`)"))?;
        // P3-FLEET: the calls ride `Fleet.Ask` as product `gunnar` (a feed from before the fold
        // names no product), and ask the old SoldEstate door once on a ONE box from before it.
        Ok(crate::shop_door::ShopDoor::from_settings(&t, key)?
            .with_seam(
                feed.get(gunnar_server::feed::FEED_FLEET_AT)
                    .map(String::as_str),
                feed.get(gunnar_server::feed::FEED_FLEET_CA)
                    .map(String::as_str),
            )?
            .or_over_fleet(PRODUCT))
    })())
}

/// **The front takes its installer material from the shop** (F4, the pull of G1: the shop never
/// PUTs into an estate's slot): `take(false)` answers the bundle the shop's pairing kept for it;
/// it must read as a kept bundle, it is stored in the primary's slot (`store`) and held, and
/// only then is the shop told `take(true)` — after which its custody forgets it. Nothing stored:
/// the shop is not told, and the front asks again (the shop waits a day before it forgets).
pub fn take_kept(
    take: &dyn Fn(bool) -> Result<(Vec<u8>, String), String>,
    store: &dyn Fn(Vec<u8>) -> Result<(), String>,
) -> Result<String, String> {
    let (bundle, words) = take(false)?;
    let kept = Kept::parse(&bundle).map_err(|e| format!("the shop's installer material: {e}"))?;
    store(bundle.clone()).map_err(|e| {
        format!("the installer material could not be stored in the primary's slot: {e}")
    })?;
    hold(&bundle);
    let (_, done) = take(true)?;
    Ok(format!(
        "{words}; the installer material of {} is in the primary's slot; {done}",
        kept.plan.estate
    ))
}

/// Can `addr` be reached at all (a TCP connect)?
fn answers(addr: &str) -> bool {
    use std::net::ToSocketAddrs as _;
    addr.to_socket_addrs()
        .ok()
        .and_then(|mut a| a.next())
        .is_some_and(|a| std::net::TcpStream::connect_timeout(&a, Duration::from_secs(5)).is_ok())
}

/// **One recharge of `role`**: the kept bundle (held, else the other box's slot), the shop
/// lays the box again with a fresh seed, the front re-admits it, the new bundle is stored, the
/// shop told done.
pub fn recharge(
    root: &std::path::Path,
    door: &crate::shop_door::ShopDoor,
    role: &str,
    other_control: &str,
    primary: &dyn Fn() -> Option<String>,
    rt: &tokio::runtime::Handle,
) -> Result<String, String> {
    let bytes = match held() {
        Some(b) => b,
        None => rt.block_on(crate::operator_door::read_slot(
            root,
            other_control,
            crate::operator_door::INSTALLER_MATERIAL_SLOT,
        ))?,
    };
    let kept = Kept::parse(&bytes)?;
    let seed = kept.fresh_seed(GUNNAR_SEED)?;
    let laid = door
        .recharge_box(role, &seed, false)
        .map_err(|e| format!("the shop: {e}"))?;
    tracing::warn!(role, words = %laid.words, "recharge: the shop laid the box again; the front re-admits it");
    let public: Ipv4Addr = laid
        .public_ipv4
        .parse()
        .map_err(|e| format!("the shop's public address {:?}: {e}", laid.public_ipv4))?;
    let utility: Ipv4Addr = laid
        .utility_ipv4
        .parse()
        .map_err(|e| format!("the shop's utility address {:?}: {e}", laid.utility_ipv4))?;
    let (after, words) = readmit(&kept, role, public, utility, GUNNAR_SEED, &seed)?;
    let rendered = after.render()?;
    hold(&rendered);
    // The primary (as the re-feed names it now) keeps the new bundle.
    let control = primary().ok_or("no primary is known after the re-admission")?;
    if let Err(e) = rt.block_on(crate::operator_door::store_slot(
        root,
        &control,
        crate::operator_door::INSTALLER_MATERIAL_SLOT,
        rendered,
    )) {
        tracing::error!(error = %e, "recharge: the new ledger could not be stored in the primary's slot yet (held here; stored on the next look)");
    }
    let done = door
        .recharge_box(role, "", true)
        .map_err(|e| format!("the shop (done): {e}"))?;
    Ok(format!("{words}; {}", done.words))
}

/// **The boxes `roles` re-admitted after an in-place upgrade, in ONE run**: the shared
/// nordisk-pairing `kept::readmit_together` (T16 / P4-REST: Gunnar's 2026-10-25 re-admission,
/// every product's front now), over this bundle as the shared reader takes it. Answers the
/// bundle with the ledger as it moved.
pub fn readmit_upgraded(kept: &Kept, roles: &[&str]) -> Result<(Kept, String), String> {
    let (after, words) = nordisk_pairing::kept::readmit_together(kept, roles)?;
    let mut back = kept.clone();
    back.ledger = after.ledger;
    Ok((back, words))
}

/// **ONE box upgraded from the release's keep medium** (the update's keep path, owner
/// 2026-10-14): the shop's `Upgrade` for `role`, the box re-admitted from the kept bundle, the
/// new ledger held and stored in the primary's slot, then `done`. Answers the words.
pub fn upgrade_one(
    root: &std::path::Path,
    door: &crate::shop_door::ShopDoor,
    role: &str,
    primary: &dyn Fn() -> Option<String>,
    rt: &tokio::runtime::Handle,
) -> Result<String, String> {
    let (upgraded, words) = door
        .upgrade(role, false)
        .map(|a| (a.upgraded, a.words))
        .map_err(|e| format!("the shop: {e}"))?;
    if !upgraded {
        return Ok(words);
    }
    let back = readmit_boxes(root, &[role], primary, rt)?;
    let done = door
        .upgrade(role, true)
        .map_err(|e| format!("the shop (done): {e}"))?;
    Ok(format!("{words}; {back}; {}", done.words))
}

/// **Boxes re-admitted where they stand, from the kept bundle** ([`readmit_upgraded`]): the new
/// ledger held and stored in the primary's slot. The second half of [`upgrade_one`], and the
/// update's way back for boxes whose keep boot ran and was never re-admitted (2026-10-25). A
/// front that holds no bundle yet (just booted) reads it from either box's slot first.
pub fn readmit_boxes(
    root: &std::path::Path,
    roles: &[&str],
    primary: &dyn Fn() -> Option<String>,
    rt: &tokio::runtime::Handle,
) -> Result<String, String> {
    let bytes = match held() {
        Some(b) => b,
        None => {
            let at = |ip: Ipv4Addr| format!("{ip}:{}", crate::console::CONTROL_PORT);
            let mut why = Vec::new();
            let mut got = None;
            for (name, control) in [
                ("main", at(nordisk_pairing::OVERLAY_BOX_A)),
                ("twin", at(nordisk_pairing::OVERLAY_BOX_B)),
            ] {
                match rt.block_on(crate::operator_door::read_slot(
                    root,
                    &control,
                    crate::operator_door::INSTALLER_MATERIAL_SLOT,
                )) {
                    Ok(b) => {
                        got = Some(b);
                        break;
                    }
                    Err(e) => why.push(format!("{name}: {e}")),
                }
            }
            let b = got.ok_or_else(|| {
                format!(
                    "no kept bundle is held yet, and no box's slot gave it ({})",
                    why.join("; ")
                )
            })?;
            hold(&b);
            b
        }
    };
    let kept = Kept::parse(&bytes)?;
    let (after, back) = readmit_upgraded(&kept, roles)?;
    let rendered = after.render()?;
    hold(&rendered);
    if let Some(control) = primary() {
        if let Err(e) = rt.block_on(crate::operator_door::store_slot(
            root,
            &control,
            crate::operator_door::INSTALLER_MATERIAL_SLOT,
            rendered,
        )) {
            tracing::error!(error = %e, "update: the new ledger could not be stored in the primary's slot yet (held here)");
        }
    }
    Ok(back)
}

/// How often the front looks whether a box's own certificate is due ([`renew_boxes`]).
pub const RENEW_EVERY: Duration = Duration::from_secs(6 * 3600);

/// **Renew the boxes' own certificates that are due** (F4.2, nordisk-pairing 0.5.12): the front,
/// as the estate's installer from the kept material, reads each box's certificate over `paths`
/// and re-signs ITS key at two thirds of its life (`now_unix` injected), for the lifetime the
/// install gave them. Per box: renewed, not due, or what refused it.
pub fn renew_boxes(
    kept: &Kept,
    paths: Vec<(String, BoxPath)>,
    now_unix: i64,
) -> Result<Vec<Renewed>, String> {
    let m = kept.material()?;
    let valid = kept
        .plan
        .box_cert_valid_s
        .map(Duration::from_secs)
        .unwrap_or(MATERIAL_VALID);
    Ok(nordisk_pairing::installer::renew(
        &m, paths, valid, now_unix,
    ))
}

/// **The installer CA renewed from the kept material before its end** (F4, the 180-day gap):
/// at two thirds of the CA certificate's life the front re-signs it with its own key — the same
/// key and name, a new lifetime — and answers the bundle to re-seal; `None` while it is not due.
pub fn renew_ca_due(kept: &Kept, now_unix: i64) -> Result<Option<Kept>, String> {
    let ca = kept.ca()?;
    if !nordisk_pairing::mtls::renewal_due(&ca.cert_pem, now_unix)
        .map_err(|e| format!("the kept CA certificate: {e}"))?
    {
        return Ok(None);
    }
    let renewed = ca
        .renew_ca(MATERIAL_VALID)
        .map_err(|e| format!("the installer CA could not be renewed: {e}"))?;
    let mut after = kept.clone();
    after.files.insert(
        nordisk_pairing::installer::CA_CERT.to_string(),
        renewed.cert_pem,
    );
    Ok(Some(after))
}

/// **One renewal look over the kept bundle** (F4, the 180-day gaps): the installer CA renewed
/// first when due ([`renew_ca_due`]: the answer's bundle, to be held and stored back into the
/// primary's slot), then every box's own certificate from it ([`renew_all`]), each renewal
/// handing its box a fresh bootstrap for its next join (nordisk-pairing 0.5.13).
#[allow(clippy::type_complexity)]
pub fn renew_once(
    kept: &Kept,
    now_unix: i64,
) -> Result<(Option<Kept>, Vec<(String, Result<bool, String>)>), String> {
    let ca_renewed = renew_ca_due(kept, now_unix)?;
    let boxes = renew_all(ca_renewed.as_ref().unwrap_or(kept), now_unix)?;
    Ok((ca_renewed, boxes))
}

/// [`renew_boxes`] for every box of the kept plan, on the installer's own paths.
pub fn renew_all(kept: &Kept, now_unix: i64) -> Result<Vec<Renewed>, String> {
    let m = kept.material()?;
    let paths = paths_for(
        &kept.plan,
        m.link
            .as_ref()
            .ok_or("the kept material holds no link keys")?,
    )?;
    renew_boxes(
        kept,
        paths.into_iter().map(|(vm, p)| (vm.name, p)).collect(),
        now_unix,
    )
}

/// **Watch the estate's two gunnar boxes, and recharge one that is lost.** As a jera job;
/// returns at once. Does nothing on an estate that was not sold (no fed [`FEED_SHOP_DOOR`]).
pub fn watch(vm: Arc<crate::vm::FrontVm>, rt: tokio::runtime::Handle) {
    // A jera job (A28 M18, A32-14): listed, a look that panics is said and a FAILED job and the next one runs, stopped at the
    // clean stop. Its first round only waits, as the thread's first sleep did.
    let say: gunnar_common::jobs::Say = Arc::new(|l: &str| tracing::error!("{l}"));
    let spawned = gunnar_common::jobs::start("front-recharge", LOOK_EVERY, say, {
        let root = vm.root().to_path_buf();
        let mut gone_since: BTreeMap<&'static str, Instant> = BTreeMap::new();
        let mut last_try: BTreeMap<&'static str, Instant> = BTreeMap::new();
        let mut refreshed: Option<Instant> = None;
        let mut renew_look: Option<Instant> = None;
        let mut take_look: Option<Instant> = None;
        let mut first = true;
        move || {
            if std::mem::take(&mut first) {
                return gunnar_common::jobs::Round::Again;
            }
            let Some(reply) = vm.latest() else {
                return gunnar_common::jobs::Round::Again;
            };
            let Some(Ok(door)) = shop_door(&reply.feed) else {
                return gunnar_common::jobs::Round::Again;
            };
            // The boxes' own certificates, renewed from the kept material at 2/3 of their life.
            if let Some(b) =
                held().filter(|_| renew_look.is_none_or(|r| r.elapsed() >= RENEW_EVERY))
            {
                renew_look = Some(Instant::now());
                match Kept::parse(&b)
                    .and_then(|k| renew_once(&k, nordisk_pairing::mtls::now_unix()))
                {
                    Ok((ca_renewed, each)) => {
                        if let Some(after) = ca_renewed {
                            tracing::warn!("renew: the installer CA certificate was renewed (same key and name, a new lifetime); stored back into the slot");
                            match after.render() {
                                Ok(rendered) => {
                                    hold(&rendered);
                                    let control = crate::console::target(&reply)
                                        .map(|t| t.control)
                                        .unwrap_or_else(|| {
                                            format!(
                                                "{}:{}",
                                                nordisk_pairing::OVERLAY_BOX_A,
                                                crate::console::CONTROL_PORT
                                            )
                                        });
                                    if let Err(e) = rt.block_on(crate::operator_door::store_slot(
                                        &root,
                                        &control,
                                        crate::operator_door::INSTALLER_MATERIAL_SLOT,
                                        rendered,
                                    )) {
                                        tracing::error!(error = %e, "renew: the renewed CA could not be stored in the primary's slot yet (held here; renewed again on the next look)");
                                    }
                                }
                                Err(e) => {
                                    tracing::error!(error = %e, "renew: the renewed bundle would not render")
                                }
                            }
                        }
                        each.iter().for_each(|(name, r)| match r {
                        Ok(true) => tracing::warn!(box_ = %name, "renew: the box's own certificate was renewed (same key, a new lifetime)"),
                        Ok(false) => tracing::debug!(box_ = %name, "renew: not due"),
                        Err(e) => tracing::error!(box_ = %name, error = %e, "renew: not renewed; looked at again later"),
                        })
                    }
                    Err(e) => tracing::error!(error = %e, "renew: the kept material cannot renew"),
                }
            }
            let at = |ip: Ipv4Addr| format!("{ip}:{}", crate::console::CONTROL_PORT);
            let (main_at, twin_at) = (
                at(nordisk_pairing::OVERLAY_BOX_A),
                at(nordisk_pairing::OVERLAY_BOX_B),
            );
            let primary_now = crate::console::target(&reply)
                .map(|t| t.control)
                .unwrap_or_else(|| main_at.clone());
            let (main_up, twin_up) = (answers(&main_at), answers(&twin_at));
            // While the primary lives, the bundle is held fresh from its slot.
            if answers(&primary_now)
                && refreshed.is_none_or(|r| r.elapsed() >= Duration::from_secs(600))
            {
                if let Ok(b) = rt.block_on(crate::operator_door::read_slot(
                    &root,
                    &primary_now,
                    crate::operator_door::INSTALLER_MATERIAL_SLOT,
                )) {
                    hold(&b);
                    refreshed = Some(Instant::now());
                }
            }
            // No material in the slot yet: the front takes it from the shop (it never pushes).
            if held().is_none()
                && answers(&primary_now)
                && take_look.is_none_or(|t| t.elapsed() >= AGAIN_AFTER)
            {
                take_look = Some(Instant::now());
                let take = |done: bool| {
                    door.take_material(done)
                        .map(|a| (a.bundle, a.words))
                        .map_err(|e| format!("the shop: {e}"))
                };
                let store = |b: Vec<u8>| {
                    rt.block_on(crate::operator_door::store_slot(
                        &root,
                        &primary_now,
                        crate::operator_door::INSTALLER_MATERIAL_SLOT,
                        b,
                    ))
                    .map(|_| ())
                };
                match take_kept(&take, &store) {
                    Ok(words) => tracing::warn!("material: {words}"),
                    Err(e) => {
                        tracing::error!(error = %e, "material: not taken yet; asked again later")
                    }
                }
            }
            for (role, up, other_up, other_at) in [
                ("main", main_up, twin_up, twin_at.clone()),
                ("twin", twin_up, main_up, main_at.clone()),
            ] {
                // A box the ONE box's update is working on (its keep boot, its re-exec) is down, not lost.
                if up || crate::update::busy(Some(role)) {
                    gone_since.remove(role);
                    continue;
                }
                let since = *gone_since.entry(role).or_insert_with(Instant::now);
                if !due(
                    Some(since.elapsed()),
                    other_up,
                    last_try.get(role).map(|t| t.elapsed()),
                ) {
                    continue;
                }
                last_try.insert(role, Instant::now());
                tracing::warn!(
                    role,
                    gone_s = since.elapsed().as_secs(),
                    "recharge: the box is gone; the front asks the shop to lay it again"
                );
                let vm_now = Arc::clone(&vm);
                let primary = move || {
                    vm_now
                        .latest()
                        .as_ref()
                        .and_then(crate::console::target)
                        .map(|t| t.control)
                };
                match recharge(&root, &door, role, &other_at, &primary, &rt) {
                    Ok(words) => {
                        tracing::warn!(role, "recharge: {words}");
                        gone_since.remove(role);
                    }
                    Err(e) => {
                        tracing::error!(role, error = %e, "recharge: not done; asked again later")
                    }
                }
            }
            gunnar_common::jobs::Round::Again
        }
    });
    if let Err(e) = spawned {
        tracing::error!(error = %e, "recharge: no thread to watch the boxes");
    }
}

#[cfg(test)]
pub(crate) mod tests {
    use super::*;
    use nordisk_pairing::door::Door;
    use nordisk_pairing::installer::Plan;
    use nordisk_pairing::seed::{Kind, Seed};

    pub(crate) fn order() -> monetize_server::estate_order::LaidOrder {
        serde_json::from_value(serde_json::json!({
            "company": "acme", "apex": "test2.gunnar.rs", "domain": "",
            "names": ["acme.test2.gunnar.rs", "www.acme.test2.gunnar.rs", "git.acme.test2.gunnar.rs"],
            "estate": {"reference": "estate/acme", "company": "acme", "zone": "se-sto1", "machines": [
                {"role": "front", "hostname": "f", "title": "f", "plan": "STARTER-4xCPU-8GB", "server": "s1", "public_ipv4": "198.51.100.1", "utility_ipv4": "10.6.0.1", "disks": [{"title": "sys", "id": "d1", "gib": 40, "tier": "standard", "address": "virtio:0"}]},
                {"role": "main", "hostname": "m", "title": "m", "plan": "STARTER-4xCPU-16GB", "server": "s2", "public_ipv4": "198.51.100.2", "utility_ipv4": "10.6.0.2", "disks": [{"title": "sys", "id": "d2", "gib": 50, "tier": "standard", "address": "virtio:0"}]},
                {"role": "twin", "hostname": "t", "title": "t", "plan": "STARTER-1xCPU-2GB", "server": "s3", "public_ipv4": "198.51.100.3", "utility_ipv4": "10.6.0.3", "disks": [{"title": "sys", "id": "d3", "gib": 20, "tier": "standard", "address": "virtio:0"}]}
            ]},
            "first_login_ttl_secs": 604800
        }))
        .unwrap()
    }

    pub(crate) fn kept() -> Kept {
        // The shop's own pairing mints the fixture (dev-dependencies: nothing of it ships).
        let gunnar =
            monetize_shop_pairing::ProductContract::of(&gunnar_monetize_guest::lifecycle::facts())
                .unwrap();
        let files: BTreeMap<String, String> =
            monetize_shop_pairing::mint_material_for(&gunnar, "acme.test2.gunnar.rs")
                .unwrap()
                .into_iter()
                .collect();
        let plan = monetize_shop_pairing::plan_of_for(
            &gunnar,
            &order(),
            BTreeMap::new(),
            BTreeMap::new(),
            &BTreeMap::new(),
            600,
        )
        .unwrap();
        // The kept link keys are the ones the seeds name (mint_material wrote both).
        Kept {
            files,
            ledger: toml::to_string(&Door::default()).unwrap(),
            plan,
        }
    }

    /// ★ **The kept bundle reads back, and the front can be the estate's installer from it**:
    /// its CA, a FRESH installer certificate, the link keys; a fresh seed whose bootstrap is new
    /// and whose link key is the kept one.
    #[test]
    fn the_kept_bundle_makes_the_front_the_estates_installer() {
        let k = kept();
        let back = Kept::parse(&k.render().unwrap()).unwrap();
        assert_eq!(back.plan, k.plan);
        let m = back.material().unwrap();
        assert_eq!(m.ca.cert_pem, k.files["ca.cert.pem"]);
        assert_ne!(
            m.me.cert_pem, k.files["installer.cert.pem"],
            "the installer certificate is minted fresh"
        );
        assert!(m.link.is_some());
        let fresh = Seed::parse(&back.fresh_seed(GUNNAR_SEED).unwrap()).unwrap();
        let old = Seed::parse(&k.files["seed.gunnar.toml"]).unwrap();
        assert_ne!(
            fresh.bootstrap_cert_pem, old.bootstrap_cert_pem,
            "a new bootstrap certificate"
        );
        assert_eq!(
            fresh.installer_link.as_ref().map(|l| l.vm_private.clone()),
            old.installer_link.as_ref().map(|l| l.vm_private.clone()),
            "the kept link key"
        );
        assert_eq!(
            (fresh.estate.as_str(), fresh.kind),
            (old.estate.as_str(), Kind::Gunnar)
        );
        assert!(Kept::parse(b"{\"v\":\"other\"}").is_err());
    }

    /// ★ The plan moves ONE box, and a re-admission gets the time a restore needs.
    #[test]
    fn the_plan_moves_one_box_and_the_others_stand() {
        let k = kept();
        let p = moved(
            &k.plan,
            "main",
            "203.0.113.7".parse().unwrap(),
            "10.6.0.9".parse().unwrap(),
        )
        .unwrap();
        let get = |p: &Plan, n: &str| p.vms.iter().find(|v| v.name == n).unwrap().clone();
        assert_eq!(
            (get(&p, "main").public, get(&p, "main").utility),
            ("203.0.113.7".parse().unwrap(), "10.6.0.9".parse().unwrap())
        );
        assert_eq!(get(&p, "twin"), get(&k.plan, "twin"), "twin stands");
        // The front stands where it stood; the main laid again gets a NEW pre-shared key and
        // the front the same one (nordisk-pairing `kept::moved`, lane WG-PSK).
        let psk = get(&p, "main")
            .feed
            .get(nordisk_pairing::installer::FEED_WG_PSK)
            .cloned()
            .expect("the main laid again is fed a PSK");
        let mut front = get(&p, "front");
        assert_eq!(
            front
                .feed
                .remove(&nordisk_pairing::installer::feed_wg_psk_for("main")),
            Some(psk),
            "the front holds the same one"
        );
        assert_eq!(front, get(&k.plan, "front"), "front stands");
        assert!(p.budget_s >= READMIT_BUDGET_S);
        assert!(moved(&k.plan, "nobody", Ipv4Addr::LOCALHOST, Ipv4Addr::LOCALHOST).is_err());
    }

    /// ★ **When the front recharges**: a box gone long enough while the other answers, once per
    /// [`AGAIN_AFTER`] — never when both are silent (that is the front's own link).
    #[test]
    fn a_box_is_recharged_only_when_gone_long_enough_and_the_other_answers() {
        let g = |s: u64| Some(Duration::from_secs(s));
        assert!(due(g(GONE_FOR.as_secs()), true, None));
        assert!(!due(g(GONE_FOR.as_secs() - 1), true, None), "not yet");
        assert!(
            !due(g(3600), false, None),
            "both silent: the front's own link, not two lost boxes"
        );
        assert!(!due(None, true, None), "it answers");
        assert!(!due(g(3600), true, g(60)), "tried a minute ago");
        assert!(due(g(3600), true, Some(AGAIN_AFTER)));
    }

    /// The shop's door is the fed `shop_door.toml`, its client key the fed secret beside it;
    /// an estate that was not sold has none, and nothing of it touches the front's disk.
    #[test]
    fn the_shop_door_is_the_fed_shop_door_toml_and_its_key_the_fed_secret() {
        let shop_key: String = monetize::sign::Signer::from_bytes(&[7; 32])
            .public_key()
            .iter()
            .map(|b| format!("{b:02x}"))
            .collect();
        let own = format!("door = \"https://monetize.ignalina.dk\"\ncompany = \"acme\"\nshop_pubkey = \"{shop_key}\"\nclient_key_secret = \"shop_client_key\"\n");
        let feed = BTreeMap::from([
            (FEED_SHOP_DOOR.to_string(), own.clone()),
            ("secret.shop_client_key".to_string(), "11".repeat(32)),
        ]);
        let door = shop_door(&feed).expect("a sold estate").expect("the door");
        assert_eq!(
            (door.door.as_str(), door.company.as_str()),
            ("https://monetize.ignalina.dk", "acme")
        );
        assert_eq!(door.shop_host(), "ignalina.dk");
        assert!(shop_door(&BTreeMap::new()).is_none(), "not sold: no shop");
        // ★ K3 under ONE monetize: a LEGACY fed monetize.toml raises no door — the front runs
        // no monetize to read it (the shop's pairing refuses such a feed at the sale).
        let legacy = BTreeMap::from([("monetize.toml".to_string(), format!("[clouds.shop]\ndoor = \"http://10.0.2.100:39963\"\ncompany = \"acme\"\nshop_pubkey = \"{shop_key}\"\n"))]);
        assert!(
            shop_door(&legacy).is_none(),
            "a legacy monetize.toml is not a shop door"
        );
        let keyless = BTreeMap::from([(FEED_SHOP_DOOR.to_string(), own)]);
        let e = shop_door(&keyless).unwrap().unwrap_err();
        assert!(
            e.contains("secret.shop_client_key"),
            "the missing fed key is said: {e}"
        );
        let bad = BTreeMap::from([(FEED_SHOP_DOOR.to_string(), "door = ".to_string())]);
        assert!(
            shop_door(&bad).unwrap().is_err(),
            "a broken door is said, not taken for unsold"
        );
    }

    /// ★ **F4 (E1 as a pull): the front TAKES its installer material from the shop** — the
    /// bundle is stored in the primary's slot and held, and only THEN is the shop told done; a
    /// slot that cannot store it, or a bundle that is not one, tells the shop nothing.
    #[test]
    fn the_front_takes_its_installer_material_and_tells_the_shop_only_once_it_is_stored() {
        let bundle = kept().render().unwrap();
        let calls = Mutex::new(Vec::<bool>::new());
        let take = |done: bool| -> Result<(Vec<u8>, String), String> {
            calls.lock().unwrap().push(done);
            Ok((
                if done { Vec::new() } else { bundle.clone() },
                if done {
                    "the shop forgets it".into()
                } else {
                    "handed".into()
                },
            ))
        };
        let stored = Mutex::new(None::<Vec<u8>>);
        let words = take_kept(&take, &|b| {
            *stored.lock().unwrap() = Some(b);
            Ok(())
        })
        .unwrap();
        assert_eq!(
            *calls.lock().unwrap(),
            vec![false, true],
            "taken, then done"
        );
        assert_eq!(
            stored.lock().unwrap().as_deref(),
            Some(bundle.as_slice()),
            "stored as handed"
        );
        assert_eq!(held().as_deref(), Some(bundle.as_slice()), "and held");
        assert!(
            words.contains("acme.test2.gunnar.rs") && words.contains("the shop forgets it"),
            "{words}"
        );
        calls.lock().unwrap().clear();
        let e = take_kept(&take, &|_| Err("the primary is away".into())).unwrap_err();
        assert!(e.contains("the primary is away"), "{e}");
        assert_eq!(
            *calls.lock().unwrap(),
            vec![false],
            "not stored: the shop is not told done"
        );
        calls.lock().unwrap().clear();
        let junk = |done: bool| -> Result<(Vec<u8>, String), String> {
            calls.lock().unwrap().push(done);
            Ok((b"{}".to_vec(), String::new()))
        };
        assert!(take_kept(&junk, &|_| panic!("never stored"))
            .unwrap_err()
            .contains("the shop's installer material"));
        assert_eq!(*calls.lock().unwrap(), vec![false]);
    }

    /// ★ **F4.2: the front renews a box's own certificate from the kept material at two thirds
    /// of its life** — a closed box serving its log door, a clock advanced past 2/3: renewed, the
    /// same key and name, kept on the box; not before.
    #[test]
    fn a_box_certificate_is_renewed_by_the_front_from_the_kept_material_at_two_thirds() {
        use nordisk_pairing::bootstrap::Vm as _;
        let k = kept();
        let ca = k.ca().unwrap();
        let boot = ca.issue_bootstrap(Duration::from_secs(3600)).unwrap();
        let dir = tempfile::tempdir().unwrap();
        let seed = Seed {
            estate: k.plan.estate.clone(),
            tunnel: nordisk_pairing::seed::TunnelKind::Wireguard,
            front: nordisk_pairing::seed::Front::default(),
            kind: Kind::Front,
            installer_ca_pem: ca.cert_pem.clone(),
            bootstrap_cert_pem: boot.cert_pem,
            bootstrap_key_pem: boot.key_pem,
            peers_expected: 3,
            mullvad: None,
            tailscale: None,
            installer_link: Some(nordisk_pairing::seed::LinkSeed {
                installer_pubkey: format!("{}=", "A".repeat(43)),
                vm_private: format!("{}=", "B".repeat(43)),
                port: nordisk_pairing::installer_link::LINK_PORT,
                psk: None,
            }),
        };
        let vm = Arc::new(crate::vm::FrontVm::open(dir.path(), seed).unwrap());
        let csr = vm.hello().unwrap().csr_pem;
        let first = ca.sign_box(&csr, "front", MATERIAL_VALID).unwrap();
        gunnar_server::pairing::write_secret(
            dir.path(),
            gunnar_server::pairing::OWN_CERT_REL,
            &first,
        )
        .unwrap();
        let l = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
        let at = match l.local_addr().unwrap() {
            std::net::SocketAddr::V4(a) => a,
            _ => unreachable!(),
        };
        let (v, ca_pem) = (Arc::clone(&vm), ca.cert_pem.clone());
        std::thread::spawn(move || {
            nordisk_pairing::bootstrap::serve_log_door(
                &*v,
                &nordisk_pairing::bootlog::BootLog::default(),
                &ca_pem,
                &l,
                &|_| {},
            )
        });
        let path = || {
            vec![(
                "front".to_string(),
                Box::new(nordisk_pairing::installer_link::DirectPath { at })
                    as Box<dyn nordisk_pairing::installer_link::Path>,
            )]
        };
        let now = nordisk_pairing::mtls::now_unix();
        let r = renew_boxes(&k, path(), now).unwrap();
        assert!(matches!(r[0].1, Ok(false)), "fresh, not due: {:?}", r[0].1);
        assert_eq!(vm.identity().cert_pem, first);
        let r = renew_boxes(&k, path(), now + 121 * 24 * 3600).unwrap();
        assert!(
            matches!(r[0].1, Ok(true)),
            "day 121 of 180: renewed: {:?}",
            r[0].1
        );
        let kept_now = vm.identity().cert_pem;
        assert_ne!(kept_now, first, "the box keeps the renewed certificate");
        assert_eq!(nordisk_pairing::mtls::cert_cn(&kept_now).unwrap(), "front");
        assert!(
            nordisk_pairing::mtls::renewal_refusal(&first, &kept_now).is_none(),
            "the same key and name"
        );
        // ★ The renewal handed the front a fresh bootstrap for its next join, kept in its seed.
        let on_disk = Seed::parse(
            &std::fs::read_to_string(dir.path().join(gunnar_server::pairing::SEED_REL)).unwrap(),
        )
        .unwrap();
        assert_ne!(on_disk.bootstrap_cert_pem, vm.identity().cert_pem);
        assert_eq!(
            nordisk_pairing::mtls::cert_cn(&on_disk.bootstrap_cert_pem).unwrap(),
            nordisk_pairing::mtls::BOOTSTRAP_CN,
            "a bootstrap pair"
        );
        assert!(
            nordisk_pairing::mtls::cert_left(&on_disk.bootstrap_cert_pem, now).unwrap()
                > Duration::from_secs(3600),
            "a fresh lifetime, not the medium's hour"
        );
    }

    /// ★ **The front renews the installer CA from the kept material before its end** (the
    /// 180-day gap): not due when fresh; at day 121 of 180 a bundle whose CA certificate is
    /// renewed — the same key, the same name, a later end — and nothing else changed.
    #[test]
    fn the_installer_ca_is_renewed_from_the_kept_material_before_its_end() {
        let k = kept();
        let now = nordisk_pairing::mtls::now_unix();
        assert!(renew_ca_due(&k, now).unwrap().is_none(), "fresh: not due");
        let after = renew_ca_due(&k, now + 121 * 24 * 3600)
            .unwrap()
            .expect("due at day 121 of 180");
        let (old, new) = (k.ca().unwrap(), after.ca().unwrap());
        assert_eq!(new.key_pem, old.key_pem, "the same key");
        assert_ne!(new.cert_pem, old.cert_pem);
        assert_eq!(
            nordisk_pairing::mtls::cert_cn(&new.cert_pem).unwrap(),
            nordisk_pairing::mtls::cert_cn(&old.cert_pem).unwrap()
        );
        assert_eq!(
            (after.ledger.as_str(), &after.plan),
            (k.ledger.as_str(), &k.plan),
            "nothing else changed"
        );
        assert_eq!(after.files.len(), k.files.len());
        // At day 121 for real: a CA certificate minted 121 days ago is due NOW and renews to a later end.
        let mut aged = k.clone();
        let key = rcgen::KeyPair::from_pem(&old.key_pem).unwrap();
        // The CA's own shape (nordisk-pairing's `mint`), moved 121 days back.
        let mut p = rcgen::CertificateParams::default();
        p.distinguished_name.push(
            rcgen::DnType::CommonName,
            nordisk_pairing::mtls::cert_cn(&old.cert_pem).unwrap(),
        );
        p.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Constrained(0));
        p.key_usages = vec![
            rcgen::KeyUsagePurpose::KeyCertSign,
            rcgen::KeyUsagePurpose::DigitalSignature,
        ];
        let back = Duration::from_secs(121 * 24 * 3600);
        let start = time::OffsetDateTime::now_utc();
        let left = nordisk_pairing::mtls::cert_left(&old.cert_pem, start.unix_timestamp()).unwrap();
        (p.not_before, p.not_after) = (start - back, start + left - back);
        aged.files.insert(
            nordisk_pairing::installer::CA_CERT.to_string(),
            p.self_signed(&key).unwrap().pem(),
        );
        let renewed = renew_ca_due(&aged, now).unwrap().expect("due now");
        let (was, is) = (aged.ca().unwrap(), renewed.ca().unwrap());
        assert!(
            nordisk_pairing::mtls::cert_left(&is.cert_pem, now).unwrap()
                > nordisk_pairing::mtls::cert_left(&was.cert_pem, now).unwrap()
                    + Duration::from_secs(100 * 24 * 3600),
            "a later end"
        );
        assert_eq!(is.key_pem, was.key_pem);
    }
}