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

nordisk/monetize

crates/monetize-cli/src/lib.rs

//! **`monetize`** — the operator's command line for the ONE monetize box (owner 2026-10-13:
//! monetize had a server and a console and no CLI).
//!
//! ```text
//! monetize [--door <host:8475>] [--material <dir>] site list
//! monetize … site order <site> [--size <step>] --admin <address> [--seed-key <file>]
//! monetize … site test-order <site> [--size <step>] --admin <address> [--seed-key <file>]
//! monetize … site status <site>
//! monetize … site narrate <site> [<vm>] [--tail <n>]
//! monetize … site installer-log <site> <vm> [--since <n>]
//! monetize … site recheck <site>
//! monetize … site retry <site>
//! monetize … site abandon <site>
//! monetize … site open-pin <site>
//! monetize … site cancel <site>
//! monetize … site terminate <site> --confirm <name typed back>
//! monetize … site first-link <site> [--to <address>]
//! monetize … site refeed <site> [--only mail_relay|public_repositories|public_site]…
//! monetize … site console <site> <METHOD> <path> [--body-file <f>] [--content-type <t>] [--repo]
//! monetize … site show [<site>]
//! monetize … site add <site> --domain <apex> --first-admin <owner> [site fields]
//! monetize … site edit <site> [site fields]
//! monetize … estate update <estate> [--release <id>] [--front-first]
//! monetize … estate update-status <estate>
//! monetize … estate live-update <estate> on|off
//! monetize … estate update-cancel <estate> [--apply]
//! monetize … estate adopt <site> --front <server> --main <server> --twin <server> --feed-out <file>
//!            [--machines <file.json>] [--zone <zone>] [--release-of <role>=<id>]… [--again]
//! monetize … box running
//! monetize … box update
//! monetize … box certs [--try <host>]
//! monetize … box plugins [--apply]
//! monetize front certs --at https://<site>/<its certificate door> (--token-file <f> | --cookie-file <f>)
//!            [--ca <pem>] [--try <host>]
//! ```
//!
//! **`front certs`** is the one command that does not ask the ONE box: it asks a product's
//! FRONT its certificates, at the status door that front's console page reads (Holger's
//! `/front/certs`, Gunnar's `/api/front/certs`), with an admin's credential read from a file
//! (Holger's bearer token; Gunnar's break-glass token, or the console's session cookie) and
//! never printed. The front's lines are printed as it says them. The door is the path of the
//! address the operator names (RA1-3): the core holds no product's door.
//!
//! **The site book** (2026-10-14: config in the application, not in a kit): `add` and `edit`
//! put a site in the box's own store through the door (`SiteBook.PutSite`), judged by the
//! box's loader and applied live; `show` reads it back — the two Stripe secrets as "set" and
//! a fingerprint, never a value. A secret is read from a FILE (`--stripe-key-file`,
//! `--webhook-secret-file`), never from the command line, and never printed. `edit` is the
//! one command that asks twice: it reads the site, then puts it back with the flags given.
//!
//! **No logic here.** Each command is ONE request at the box's door (`:8475`, mutual TLS:
//! the operator's certificate under the box's installer CA — the one credential an operator
//! holds off the box; `--door` / `MONETIZE_DOOR`, `--material` / `MONETIZE_MATERIAL`), and
//! the door forwards it to monetize-server's `EstateOrder` verb (`ListSites`, `OrderSite`,
//! `Status`, `Cancel`, `Terminate`) with the box's own operator bearer — the very verbs the
//! console's Sites form calls. What a site is, whether it can be ordered, what an order
//! costs, whether a site is protected: all monetize-server's answers, printed as they come.
//!
//! [`request_of`] and [`said`] are pure and tested; [`door`] is the one piece of I/O.

use std::path::{Path, PathBuf};

/// The environment's defaults for `--door` and `--material`.
pub const DOOR_ENV: &str = "MONETIZE_DOOR";
pub const MATERIAL_ENV: &str = "MONETIZE_MATERIAL";

/// One request at the door.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Ask {
    pub method: &'static str,
    pub path: String,
    /// JSON, or empty for a read.
    pub body: String,
    /// The command, for [`said`].
    pub what: What,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum What {
    List,
    Order,
    Status,
    Cancel,
    Terminate,
    Show,
    /// `add`: one put.
    Put,
    /// `edit`: a show, then a put built from it ([`edit_body`]).
    Edit,
    /// `estate update`, `estate update-status`, `estate live-update`: the update's state.
    Update,
    /// `box running`: what the box runs and what the release bucket offers next.
    BoxRunning,
    /// `estate update-cancel <estate>` without `--apply` (A32-7): where the update stands, and
    /// that nothing was cancelled.
    UpdateCancelPlan,
    /// `box update --apply`: the box updates itself from the next signed release.
    BoxUpdate,
    /// `box update` without `--apply` (A32-6, the kit rule): the PLAN — what the box runs and
    /// what an update would do; nothing is staged, nothing restarts.
    BoxPlan,
    /// `box certs`: the box's certificates, as edda estate-front reports them.
    BoxCerts,
    /// `estate adopt`: a kit-installed site made the box's to update; the front's feed lines
    /// are WRITTEN to `--feed-out` (0600), never printed ([`adopt_feed`]).
    Adopt,
    /// `box plugins [--apply]`: the release's signed plugins, a queued job followed to its end.
    Plugins,
    /// `estates`: the Overview as the box classifies it, READ ONLY (OVW-CLEAN): the estates that
    /// exist, the ended or never installed, and the New-estate slot's choices.
    Estates,
    /// `box inventory`: the cloud account as this box lists it, READ ONLY (MAP-SEG 3): every
    /// labelled row with its estate, its order and its label, the orphans and the missing.
    Inventory,
    /// `site narrate <site>`: a narrated site's boot logs, each box's summary.
    Narrate,
    /// `site narrate <site> <vm>`: one box's boot log, printed as the box sent it.
    NarrateText,
    /// `site installer-log <site> <vm>`: the box's install log as the ONE box's pairing read it.
    InstallerLog,
    /// `site recheck|retry|abandon <site>`: the Sites page's "Check again" / "Set up again" /
    /// "Abandon this install"; `site
    /// open-pin <site>`: the guardian's pin windows opened again.
    Settle,
    /// `site first-link <site> [--to <address>]`: the site's first sign-in link mailed again.
    FirstLink,
    /// `site refeed <site> [--only <group>]…`: the keys this box holds for a laid estate now (the
    /// mains' mail relay, its public repositories) fed to its boxes again.
    Refeed,
    /// `site console <site> <METHOD> <path> [--body-file <f>] [--content-type <t>] [--repo]`: one
    /// call on a laid site's console through its public front, with the custody's bearer.
    Console,
    /// `site test-order <site>`: the order paid server-side with the vendor's TEST card.
    TestOrder,
    /// `site change-plan <site> …`: the operator's plan change of his own site (a grow).
    ChangePlan,
    /// `site do <site> <kind> [<param>=<value>]… [--file <param>=<path>]`: a product's own order,
    /// queued for its box to pull; the box's answer is waited for and said.
    Do,
    /// `site do-answer <site> <seq>`: the box's answer to one product order.
    DoAnswer,
    /// `site kinds <site>`: the order kinds the site's product declares.
    Kinds,
}

/// Where the box is and who asks.
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct Target {
    pub door: String,
    pub material: PathBuf,
}

pub fn help() -> &'static str {
    "monetize — the operator's command line for the ONE monetize box\n\n\
     monetize [--door <host:8475>] [--material <dir>] site list\n\
     monetize … site order <site> [--size <step>] --admin <address> [--seed-key <file>] [--return <url>]\n\
     monetize … site test-order <site> [--size <step>] --admin <address> [--seed-key <file>]\n\
     \x20          the order paid on the box with the site's vendor's TEST card (Stripe's pm_card_visa),\n\
     \x20          nobody at a page; refused by name on a live key, before the vendor is asked anything\n\
     monetize … site status <site>\n\
     monetize … site narrate <site> [<vm>] [--tail <n>]  a narrated site's boot logs (DEV-NARRATE): each box's\n\
     \x20          summary, or one box's log, newest last (--tail: its last n lines)\n\
     monetize … site installer-log <site> <vm> [--since <n>]  the box's install log as the ONE box read it over\n\
     \x20          the installer's link, from its first boot line (--since: from line n)\n\
     monetize … site recheck <site>                   Check again: every box's pair status; all PAIRED adopts the site\n\
     monetize … site retry <site>                     Set up again: a failed or owed site is laid and installed again\n\
     monetize … site abandon <site>                   Abandon this install: an install still running is failed now\n\
     monetize … site open-pin <site>                  the guardian's pin window opened again, until each pins, for the\n\
     \x20          site's boxes whose key is not pinned yet (a pinned key is never replaced)\n\
     monetize … site cancel <site>\n\
     monetize … site change-plan <site> [--main-plan <plan>] [--front-plan <plan>] [--data-gib <n>] [--set <field>=<n>]… [--test]\n\
     \x20          the operator's plan change of his own site: new plans, main's data grown to n GB a disk\n\
     \x20          (never smaller), the product's own order fields (--set); a dearer change is held at the\n\
     \x20          vendor's page (--test: with the vendor's TEST card, nobody at a page); a data step reaches\n\
     \x20          the boxes as a signed go-ahead they pull\n\
     monetize … site terminate <site> --confirm <the site's name, typed back>\n\
     \x20          (a customer's sold estate too: no backup is taken; nothing is refunded)\n\
     monetize … site first-link <site> [--to <address>]  the site's first sign-in link mailed again: to a new\n\
     \x20          address, or with no --to to the first admin's recorded one (a handed-off site's\n\
     \x20          main is asked to mint it, and its answer is waited for and said: mailed, or\n\
     \x20          refused with its reason; refused once its first admin has signed in)\n\
     monetize … site refeed <site> [--only mail_relay|public_repositories|public_site]…  what this box holds for a\n\
     \x20          laid site NOW fed to its main and twin again as one signed order: the mail relay of\n\
     \x20          the site's product (secret.sold_smtp_url.<product> / sold_smtp_from.<product>), its\n\
     \x20          public_repositories; its main's answer is waited for and said: kept, or refused with\n\
     \x20          its reason\n\
     monetize … site console <site> <METHOD> <path> [--body-file <f>] [--content-type <t>] [--repo]\n\
     \x20          one call (GET, POST or PUT) on a laid site's console at its public front (--repo: its\n\
     \x20          repository host repo.<host>), with the bearer the ONE box holds for it; only a call the\n\
     \x20          product or the box declares, audited; says HTTP <status>, then the front's answer\n\
     monetize … site kinds <site>                       the order kinds the site's product declares (its facts): each\n\
     \x20          kind, the role that runs it, its params (* required) and its file\n\
     monetize … site do <site> <kind> [<param>=<value>]… [--file <param>=<path>] [--no-wait]\n\
     \x20          one of the product's own orders, signed and queued for the box that runs it, which\n\
     \x20          PULLS it (and its file) on its next look; its answer is waited for and said\n\
     monetize … site do-answer <site> <seq>             the box's answer to one product order\n\
     monetize … site show [<site>]                      (no site: a blank one and the box's choices)\n\
     monetize … site add <site> --domain <apex> --first-admin <owner> [site fields]\n\
     monetize … site edit <site> [site fields]          (what is not given stays as the box has it)\n\n\
     site fields:\n\
     \x20 --domain <apex>  --product <id>  --first-admin <owner>  --operator-site yes|no  --terminable yes|no\n\
     \x20 --vendor stripe/<account>   (add: default a new account of its own, stripe/<site>)\n\
     \x20 --stripe-key-file <file>  --webhook-secret-file <file>   (read from files, never printed)\n\
     \x20 --mode test|live  --term-days <n>  --term-months <n>  --grace-hours <n>  --margin-bp <n>  --media-from <site>\n\n\
     monetize … estate update <estate> [--release <id>] [--front-first]   update twin, main, front to the release sold now\n\
     \x20 --front-first: the shop keep-boots the front first (a front with no update taker yet)\n\
     monetize … estate update-status <estate>\n\
     monetize … estate live-update <estate> on|off         off: keep media only (no live app slot)\n\
     monetize … estate update-cancel <estate> [--apply]     the plan; --apply: cancel the open update (the boxes done stay done)\n\
     monetize … estate adopt <site> --front <server> --main <server> --twin <server> --feed-out <file>\n\
     \x20          [--machines <file.json>] [--zone <zone>] [--release-of <role>=<id>]… [--again]\n\
     \x20          a kit-installed site becomes this box's to update; the front's feed lines are\n\
     \x20          written to --feed-out (0600, a new file), never printed\n\
     monetize … box running                                what this box runs, what the bucket offers next\n\
     monetize … box update [--apply]                       the plan; --apply: this box takes the next signed release (A/B, rolls back)\n\
     monetize … box update --back [--apply]                the way back: the other slot's release, confirmed on this box\n\
     monetize … box certs [--try <host>]                   this box's certificates: valid until, next attempt, last\n\
     \x20          reason; --try: try now (refused while a failed validation's wait runs)\n\
     monetize … box plugins [--apply]                      the release's signed plugins: what would change (--apply: take them, live)\n\
     monetize … estates                                    the Overview as the box classifies it, read only: the estates\n\
     \x20          that exist, the ended or never installed, the New-estate slot's choices\n\
     monetize … box inventory                              the cloud account as this box lists it, read only: each\n\
     \x20          labelled row with its estate and order, the orphans and the missing (no secret in it)\n\
     monetize front certs --at https://<site>/<door> (--token-file <f> | --cookie-file <f>) [--ca <pem>] [--try <host>]\n\
     \x20          a product front's certificates, asked at the status door named in --at (the one its\n\
     \x20          console page reads) with an admin's credential read from the file (never printed);\n\
     \x20          --try: try now (<door>/try); --ca: a front under its own CA\n\n\
     --door      the box's door (default: $MONETIZE_DOOR)\n\
     --material  the directory holding the operator's certificate for it (default: $MONETIZE_MATERIAL)\n\
     --admin     where the site's first admin's sign-in link is mailed\n\
     --seed-key  a file holding the seed's PUBLIC key (one OpenSSH line): the site's declared\n\
     \x20           repositories are seeded with its private half, which never leaves your machine\n\
     --return    where the payment page sends you back (default: https://<the door's host>/sites,\n\
     \x20           the box's own Sites page — never the site being laid, which has no front yet)\n\n\
     Every answer is the box's own; this tool decides nothing."
}

fn refuse(id: &str, msg: impl std::fmt::Display) -> String {
    format!("REFUSED [{id}] {msg}")
}

/// Pure: the leading `--door` / `--material` taken out (else the environment's), and the rest.
pub fn target_of(args: &[String], env: &dyn Fn(&str) -> Option<String>) -> Result<(Target, Vec<String>), String> {
    let mut t = Target { door: env(DOOR_ENV).unwrap_or_default(), material: env(MATERIAL_ENV).map(PathBuf::from).unwrap_or_default() };
    let mut rest = Vec::new();
    let mut it = args.iter();
    while let Some(a) = it.next() {
        match a.as_str() {
            "--door" => t.door = it.next().cloned().ok_or_else(|| refuse("flag", "--door needs <host:port>"))?,
            "--material" => t.material = PathBuf::from(it.next().ok_or_else(|| refuse("flag", "--material needs a directory"))?),
            _ => rest.push(a.clone()),
        }
    }
    Ok((t, rest))
}

/// Pure: **the one request a command is** — or the refusal naming what the command lacks.
/// `read_file` reads `--seed-key`'s file (its public line).
pub fn request_of(words: &[String], read_file: &dyn Fn(&Path) -> Result<String, String>) -> Result<Ask, String> {
    let w: Vec<&str> = words.iter().map(String::as_str).collect();
    let flag = |name: &str| w.iter().position(|a| *a == name).and_then(|i| w.get(i + 1)).map(|v| v.to_string());
    let site = |verb: &str| w.get(2).filter(|s| !s.starts_with("--")).map(|s| s.to_string()).ok_or_else(|| refuse("site", format!("`site {verb}` names the site: its label, as the box declares it (`monetize site list`)")));
    match (w.first().copied(), w.get(1).copied()) {
        (Some("site"), Some("list")) => Ok(Ask { method: "GET", path: "/site/list".into(), body: String::new(), what: What::List }),
        (Some("site"), Some("status")) => Ok(Ask { method: "GET", path: format!("/site/status/{}", site("status")?), body: String::new(), what: What::Status }),
        (Some("site"), Some("narrate")) => narrate_request(&w),
        (Some("site"), Some("installer-log")) => installer_log_request(&w),
        (Some("site"), Some("open-pin")) => Ok(Ask { method: "POST", path: format!("/site/open-pin/{}", site("open-pin")?), body: String::new(), what: What::Settle }),
        (Some("site"), Some(action @ ("recheck" | "retry" | "abandon"))) => Ok(Ask { method: "POST", path: "/site/settle".into(), body: serde_json::json!({ "estate": site(action)?, "action": action }).to_string(), what: What::Settle }),
        (Some("site"), Some("order")) => {
            let estate = site("order")?;
            let email = flag("--admin").ok_or_else(|| refuse("site-admin", "--admin <address>: where the first admin's sign-in link is mailed"))?;
            let body = serde_json::json!({ "estate": estate, "size": flag("--size").unwrap_or_default(), "email": email, "seed_key": seed_key_of(flag("--seed-key"), read_file)?, "return_url": flag("--return").unwrap_or_default() });
            Ok(Ask { method: "POST", path: "/site/order".into(), body: body.to_string(), what: What::Order })
        }
        (Some("site"), Some("test-order")) => {
            let estate = site("test-order")?;
            let email = flag("--admin").ok_or_else(|| refuse("site-admin", "--admin <address>: where the first admin's sign-in link is mailed"))?;
            if w.contains(&"--return") {
                return Err(refuse("site-test-order-return", "a test order is paid with the vendor's TEST card, at no page: it names no return page"));
            }
            let body = serde_json::json!({ "estate": estate, "size": flag("--size").unwrap_or_default(), "email": email, "seed_key": seed_key_of(flag("--seed-key"), read_file)? });
            Ok(Ask { method: "POST", path: "/site/test-order".into(), body: body.to_string(), what: What::TestOrder })
        }
        (Some("site"), Some("change-plan")) => {
            let estate = site("change-plan")?;
            let gib = match flag("--data-gib") {
                None => 0,
                Some(n) => n.parse::<u64>().map_err(|_| refuse("site-change-plan", format!("--data-gib {n:?} is not a number of GB")))?,
            };
            let mut order = serde_json::Map::new();
            for (i, _) in w.iter().enumerate().filter(|(_, a)| **a == "--set") {
                let kv = w.get(i + 1).copied().unwrap_or_default();
                let (k, v) = kv.split_once('=').filter(|(k, _)| !k.trim().is_empty()).ok_or_else(|| refuse("site-change-plan", format!("--set {kv:?} is <field>=<value>")))?;
                let v = v.trim();
                order.insert(k.trim().to_string(), v.parse::<u64>().map(serde_json::Value::from).or_else(|_| v.parse::<bool>().map(serde_json::Value::from)).unwrap_or_else(|_| serde_json::Value::from(v)));
            }
            let body = serde_json::json!({ "estate": estate, "main_plan": flag("--main-plan").unwrap_or_default(), "front_plan": flag("--front-plan").unwrap_or_default(), "data_gib": gib, "order": order, "test": w.contains(&"--test") });
            if gib == 0 && order.is_empty() && body["main_plan"] == "" && body["front_plan"] == "" {
                return Err(refuse("site-change-plan", "name what changes: --main-plan, --front-plan, --data-gib or --set"));
            }
            Ok(Ask { method: "POST", path: "/site/change-plan".into(), body: body.to_string(), what: What::ChangePlan })
        }
        (Some("site"), Some("cancel")) => Ok(Ask { method: "POST", path: "/site/cancel".into(), body: serde_json::json!({ "estate": site("cancel")? }).to_string(), what: What::Cancel }),
        (Some("site"), Some("terminate")) => {
            let estate = site("terminate")?;
            let confirm = flag("--confirm").ok_or_else(|| refuse("site-confirm", format!("--confirm <name>: type the site's name back ({estate}) to terminate it")))?;
            if w.contains(&"--backup") {
                return Err(refuse("terminate-no-backup", "backups are not offered at terminate"));
            }
            let body = serde_json::json!({ "estate": estate, "confirm": confirm });
            Ok(Ask { method: "POST", path: "/site/terminate".into(), body: body.to_string(), what: What::Terminate })
        }
        (Some("site"), Some("first-link")) => {
            let estate = site("first-link")?;
            // FIRST-LINK-1: no --to is the first admin's recorded address.
            let to = flag("--to").unwrap_or_default();
            let body = serde_json::json!({ "estate": estate, "to": to });
            Ok(Ask { method: "POST", path: "/site/first-link".into(), body: body.to_string(), what: What::FirstLink })
        }
        (Some("site"), Some("console")) => {
            let estate = site("console")?;
            let usage = || refuse("site-console", "`site console <site> <METHOD> <path> [--body-file <f>] [--content-type <t>] [--repo]`");
            let method = w.get(3).filter(|m| !m.starts_with("--")).map(|m| m.to_ascii_uppercase()).ok_or_else(usage)?;
            let path = w.get(4).filter(|p| p.starts_with('/')).map(|p| p.to_string()).ok_or_else(usage)?;
            let mut body = serde_json::json!({ "estate": estate, "method": method, "path": path, "repo": w.contains(&"--repo") });
            if let Some(t) = flag("--content-type") {
                body["content_type"] = serde_json::json!(t);
            }
            // The file is read where the request is sent (its bytes, base64: `body_b64`).
            if let Some(f) = flag("--body-file") {
                body[CONSOLE_BODY_FILE] = serde_json::json!(f);
            }
            Ok(Ask { method: "POST", path: "/site/console".into(), body: body.to_string(), what: What::Console })
        }
        (Some("site"), Some("kinds")) => Ok(Ask { method: "GET", path: format!("/site/kinds/{}", site("kinds")?), body: String::new(), what: What::Kinds }),
        (Some("site"), Some("do-answer")) => {
            let estate = site("do-answer")?;
            let seq = w.get(3).and_then(|n| n.parse::<u64>().ok()).ok_or_else(|| refuse("site-do", "`site do-answer <site> <seq>`: the order's seq `site do` said"))?;
            Ok(Ask { method: "GET", path: format!("/site/do/{estate}/{seq}"), body: String::new(), what: What::DoAnswer })
        }
        (Some("site"), Some("do")) => {
            let estate = site("do")?;
            let usage = || refuse("site-do", "`site do <site> <kind> [<param>=<value>]… [--file <param>=<path>] [--no-wait]` (`site kinds <site>` lists them)");
            let kind = w.get(3).filter(|k| !k.starts_with("--") && !k.contains('=')).map(|k| k.to_string()).ok_or_else(usage)?;
            let mut params = serde_json::Map::new();
            let mut file = None;
            let mut i = 4;
            while let Some(a) = w.get(i) {
                match *a {
                    "--file" => {
                        let (p, path) = w.get(i + 1).and_then(|f| f.split_once('=')).ok_or_else(usage)?;
                        file = Some((p.to_string(), path.to_string()));
                        i += 2;
                    }
                    "--no-wait" => i += 1,
                    kv => {
                        let (k, v) = kv.split_once('=').filter(|(k, _)| !k.is_empty()).ok_or_else(usage)?;
                        params.insert(k.to_string(), serde_json::Value::from(v));
                        i += 1;
                    }
                }
            }
            let mut body = serde_json::json!({ "estate": estate, "kind": kind, "params": params, "wait": !w.contains(&"--no-wait") });
            // The file is read where the request is sent (its bytes, base64: `file_b64`).
            if let Some((p, path)) = file {
                body[DO_FILE] = serde_json::json!(path);
                body["file_param"] = serde_json::json!(p);
            }
            Ok(Ask { method: "POST", path: "/site/do".into(), body: body.to_string(), what: What::Do })
        }
        (Some("site"), Some("refeed")) => {
            let estate = site("refeed")?;
            let only: Vec<&str> = w.iter().enumerate().filter(|(_, a)| **a == "--only").filter_map(|(i, _)| w.get(i + 1).copied()).collect();
            let body = serde_json::json!({ "estate": estate, "only": only });
            Ok(Ask { method: "POST", path: "/site/refeed".into(), body: body.to_string(), what: What::Refeed })
        }
        (Some("site"), Some("show")) => {
            let path = match w.get(2).filter(|s| !s.starts_with("--")) {
                Some(s) => format!("/site/show/{s}"),
                None => "/site/show".to_string(),
            };
            Ok(Ask { method: "GET", path, body: String::new(), what: What::Show })
        }
        (Some("site"), Some("add")) => {
            let estate = site("add")?;
            let blank = serde_json::json!({ "site": { "estate": estate, "operator_site": true } });
            let mut body = put_body(&blank, words, read_file)?;
            body["create"] = serde_json::Value::Bool(true);
            if body["domain"].as_str().unwrap_or("").is_empty() {
                return Err(refuse("site-domain", "--domain <apex>: the domain the site is laid under"));
            }
            if body["first_admin"].as_str().unwrap_or("").is_empty() {
                return Err(refuse("site-first-admin", "--first-admin <owner>: the site's owner's account"));
            }
            Ok(Ask { method: "POST", path: "/site/put".into(), body: body.to_string(), what: What::Put })
        }
        (Some("site"), Some("edit")) => {
            let estate = site("edit")?;
            Ok(Ask { method: "GET", path: format!("/site/show/{estate}"), body: String::new(), what: What::Edit })
        }
        (Some("estate"), Some(verb @ ("update" | "update-status" | "live-update" | "update-cancel"))) => {
            let estate = w.get(2).filter(|s| !s.starts_with("--")).map(|s| s.to_string()).ok_or_else(|| refuse("estate", format!("`estate {verb}` names the estate: a sold estate's company or a site's label")))?;
            match verb {
                "update" => Ok(Ask { method: "POST", path: "/estate/update".into(), body: serde_json::json!({ "estate": estate, "release": flag("--release").unwrap_or_default(), "front_first": w.contains(&"--front-first") }).to_string(), what: What::Update }),
                "update-status" => Ok(Ask { method: "GET", path: format!("/estate/update/{estate}"), body: String::new(), what: What::Update }),
                // ★ A32-7: the plan unless --apply (the kit rule): where the update stands.
                "update-cancel" if !w.contains(&"--apply") => Ok(Ask { method: "GET", path: format!("/estate/update/{estate}"), body: String::new(), what: What::UpdateCancelPlan }),
                "update-cancel" => Ok(Ask { method: "POST", path: "/estate/update/cancel".into(), body: serde_json::json!({ "estate": estate }).to_string(), what: What::Update }),
                _ => {
                    let on = match w.get(3).copied() {
                        Some("on") => true,
                        Some("off") => false,
                        _ => return Err(refuse("estate-live", "`estate live-update <estate> on|off`")),
                    };
                    Ok(Ask { method: "POST", path: "/estate/live-update".into(), body: serde_json::json!({ "estate": estate, "on": on }).to_string(), what: What::Update })
                }
            }
        }
        (Some("estate"), Some("adopt")) => adopt_request(&w, read_file),
        (Some("box"), Some("running")) => Ok(Ask { method: "GET", path: "/self/running".into(), body: String::new(), what: What::BoxRunning }),
        // ★ A32-6 (the kit rule): no `--apply`, no move — the plan is what the box runs and what
        // an update (or the way back) would do; only `--apply` stages and restarts the box.
        (Some("box"), Some("update")) if !w.contains(&"--apply") => Ok(Ask { method: "GET", path: "/self/running".into(), body: String::new(), what: What::BoxPlan }),
        (Some("box"), Some("update")) if w.contains(&"--back") => Ok(Ask { method: "POST", path: "/self/update".into(), body: serde_json::json!({ "back": true }).to_string(), what: What::BoxUpdate }),
        (Some("box"), Some("update")) => Ok(Ask { method: "POST", path: "/self/update".into(), body: String::new(), what: What::BoxUpdate }),
        (Some("box"), Some("certs")) => match flag("--try") {
            Some(host) => Ok(Ask { method: "POST", path: "/self/certs/try".into(), body: serde_json::json!({ "host": host }).to_string(), what: What::BoxUpdate }),
            None => Ok(Ask { method: "GET", path: "/self/certs".into(), body: String::new(), what: What::BoxCerts }),
        },
        (Some("box"), Some("inventory")) => Ok(Ask { method: "GET", path: "/cloud/inventory".into(), body: String::new(), what: What::Inventory }),
        (Some("estates"), None) => Ok(Ask { method: "GET", path: "/estates/overview".into(), body: String::new(), what: What::Estates }),
        (Some("box"), Some("plugins")) => Ok(Ask { method: "POST", path: "/box/plugins".into(), body: serde_json::json!({ "apply": w.contains(&"--apply") }).to_string(), what: What::Plugins }),
        _ => Err(refuse("usage", help())),
    }
}

/// Pure: **`estates`' words** (OVW-CLEAN): one line per estate that exists (`name product
/// standing domain — words`), then the quiet line and its names, then the slot's choices.
pub fn estates_words(v: &serde_json::Value) -> String {
    let g = |e: &serde_json::Value, k: &str| e[k].as_str().unwrap_or_default().to_string();
    let line = |e: &serde_json::Value| format!("{} {} {} {} — {}\n", g(e, "name"), g(e, "product"), g(e, "standing"), if g(e, "domain").is_empty() { "-".to_string() } else { g(e, "domain") }, g(e, "words"));
    let rows: Vec<&serde_json::Value> = v["estates"].as_array().into_iter().flatten().collect();
    let ended: Vec<&serde_json::Value> = v["ended"].as_array().into_iter().flatten().collect();
    let mut out = if rows.is_empty() { "no estate is installed or being laid\n".to_string() } else { rows.iter().map(|e| line(e)).collect() };
    if !ended.is_empty() {
        out.push_str(&format!("{} ended or never-installed:\n", ended.len()));
        out.extend(ended.iter().map(|e| format!("  {}", line(e))));
    }
    let list = |k: &str| v["slot"][k].as_array().into_iter().flatten().filter_map(|p| p.as_str()).collect::<Vec<_>>().join(", ");
    out.push_str(&format!("new estate: products {}; declared, started by name: {}\n", or_none(&list("products")), or_none(&list("declared"))));
    out
}

fn or_none(s: &str) -> String {
    if s.is_empty() { "none".to_string() } else { s.to_string() }
}

/// Pure: **`box inventory`'s words** (MAP-SEG 3): one line per row — `list id kind size zone
/// attached estate reference label monthly` — orphans first (the money no order owns), then
/// what is owned, then what is missing; a cloud that could not be listed says so.
pub fn inventory_words(v: &serde_json::Value) -> String {
    let mut out = String::new();
    for c in v["clouds"].as_array().into_iter().flatten() {
        let g = |r: &serde_json::Value, k: &str| r[k].as_str().unwrap_or_default().to_string();
        // MAP-3: an account two products list is ONE entry; it names them.
        let products: Vec<&str> = c["products"].as_array().into_iter().flatten().filter_map(|p| p.as_str()).collect();
        let named = if products.is_empty() { String::new() } else { format!(" ({})", products.join(", ")) };
        out.push_str(&format!("cloud {}{named}: {}\n", g(c, "provider"), if c["available"].as_bool() == Some(true) { "listed".to_string() } else { format!("NOT listed: {}", g(c, "error")) }));
        let dash = |x: String| if x.is_empty() { "-".to_string() } else { x };
        for (list, rows) in [("orphan", "orphans"), ("owned", "owned")] {
            for r in c[rows].as_array().into_iter().flatten() {
                let size = if g(r, "kind") == "server" { "server".to_string() } else { format!("{}GiB", r["gib"].as_u64().unwrap_or(0)) };
                let monthly = if r["priced"].as_bool() == Some(true) { format!("{}/mo", r["monthly_minor"].as_u64().unwrap_or(0)) } else { "unpriced".to_string() };
                let title = if g(r, "title").is_empty() { String::new() } else { format!(" {:?}", g(r, "title")) };
                out.push_str(&format!("{list} {} {} {size} {} {} estate={} ref={} res={} {monthly}{title}\n", g(r, "id"), g(r, "kind"), dash(g(r, "zone")), dash(g(r, "attached_to")), dash(g(r, "estate")), dash(g(r, "reference")), dash(g(r, "resource"))));
            }
        }
        for b in c["backups"].as_array().into_iter().flatten() {
            out.push_str(&format!("backup {} {}GiB {} estate={} ref={} {}\n", g(b, "id"), b["gib"].as_u64().unwrap_or(0), dash(g(b, "zone")), dash(g(b, "estate")), dash(g(b, "reference")), g(b, "words")));
        }
        for m in c["missing"].as_array().into_iter().flatten() {
            out.push_str(&format!("missing {} estate={} ref={} {}\n", g(m, "id"), g(m, "estate"), g(m, "reference"), g(m, "resource")));
        }
        out.push_str(&format!("{} orphan(s), {} owned, {} backup(s), {} missing, {} not ours (no monetize label)\n", c["orphans"].as_array().map_or(0, Vec::len), c["owned"].as_array().map_or(0, Vec::len), c["backups"].as_array().map_or(0, Vec::len), c["missing"].as_array().map_or(0, Vec::len), c["foreign_count"].as_u64().unwrap_or(0)));
    }
    if out.is_empty() {
        out.push_str("this box lists no cloud\n");
    }
    out
}

/// Pure: **`estate adopt`'s one request** — the site, its three machines (the server ids from
/// `--front`/`--main`/`--twin`, over what `--machines` (a JSON array the kit wrote: role,
/// server, hostname, title, plan, public_ipv4, utility_ipv4, disks) records of them), the
/// zone, the releases known, `--again`. `--feed-out` is required: the answer's secret goes
/// to a file, never to the terminal.
pub fn adopt_request(w: &[&str], read_file: &dyn Fn(&Path) -> Result<String, String>) -> Result<Ask, String> {
    let flag = |name: &str| w.iter().position(|a| *a == name).and_then(|i| w.get(i + 1)).map(|v| v.to_string());
    let site = w.get(2).filter(|s| !s.starts_with("--")).map(|s| s.to_string()).ok_or_else(|| refuse("estate", "`estate adopt` names the site: its label, as the box declares it (`monetize site list`)"))?;
    if flag("--feed-out").is_none_or(|f| f.trim().is_empty()) {
        return Err(refuse("adopt-feed-out", "--feed-out <file>: where the front's feed lines (its shop door and its client key, a SECRET) are written — never to the terminal"));
    }
    let mut machines: Vec<serde_json::Value> = match flag("--machines") {
        Some(f) => {
            let v: serde_json::Value = serde_json::from_str(&read_file(Path::new(&f))?).map_err(|e| refuse("adopt-machines", format!("{f}: not JSON: {e}")))?;
            v.as_array().cloned().ok_or_else(|| refuse("adopt-machines", format!("{f}: a JSON array of machines (role, server, …)")))?
        }
        None => Vec::new(),
    };
    for role in ["front", "main", "twin"] {
        let named = flag(&format!("--{role}"));
        match machines.iter_mut().find(|m| m["role"].as_str() == Some(role)) {
            Some(m) => {
                if let Some(id) = named {
                    m["server"] = serde_json::Value::String(id);
                }
            }
            None => match named {
                Some(id) => machines.push(serde_json::json!({ "role": role, "server": id })),
                None => return Err(refuse("adopt-machine", format!("--{role} <server id>: the {role}'s server (or a --machines file naming it)"))),
            },
        }
    }
    let mut release_of = serde_json::Map::new();
    for (i, a) in w.iter().enumerate() {
        if *a == "--release-of" {
            let (role, id) = w.get(i + 1).and_then(|v| v.split_once('=')).ok_or_else(|| refuse("adopt-release", "--release-of <role>=<release id>"))?;
            release_of.insert(role.trim().to_string(), serde_json::Value::String(id.trim().to_string()));
        }
    }
    let body = serde_json::json!({ "estate": site, "zone": flag("--zone").unwrap_or_default(), "machines": machines, "release_of": release_of, "again": w.contains(&"--again") });
    Ok(Ask { method: "POST", path: "/estate/adopt".into(), body: body.to_string(), what: What::Adopt })
}

/// Pure: `--seed-key <file>`'s one OpenSSH PUBLIC key line (empty: none); a private key is refused.
fn seed_key_of(file: Option<String>, read_file: &dyn Fn(&Path) -> Result<String, String>) -> Result<String, String> {
    let Some(f) = file else { return Ok(String::new()) };
    let line = read_file(Path::new(&f))?.trim().to_string();
    if !line.starts_with("ssh-") || line.contains('\n') {
        return Err(refuse("site-seed-key", format!("{f} is not one OpenSSH PUBLIC key line (make it with `ssh-keygen -y -f <the seed key>`); a private key is never sent")));
    }
    Ok(line)
}

/// Pure: **the front's feed lines out of the box's adopt answer** — `shop_door.toml` and
/// `secret.shop_client_key`, as the JSON object `--feed-out` holds.
pub fn adopt_feed(body: &str) -> Result<String, String> {
    let v: serde_json::Value = serde_json::from_str(body.trim()).map_err(|e| format!("the box's answer is not JSON: {e}"))?;
    let feed = v["feed"].as_object().filter(|f| f.contains_key("shop_door.toml") && f.keys().any(|k| k.starts_with("secret."))).ok_or("the box's answer carries no feed lines (shop_door.toml, secret.shop_client_key)")?;
    serde_json::to_string_pretty(feed).map(|t| t + "\n").map_err(|e| e.to_string())
}

/// **Write a secret to a NEW file, 0600** — refused when the file is there already.
pub fn write_secret_file(path: &Path, text: &str) -> Result<(), String> {
    use std::io::Write as _;
    use std::os::unix::fs::OpenOptionsExt as _;
    let mut f = std::fs::OpenOptions::new().write(true).create_new(true).mode(0o600).open(path).map_err(|e| refuse("adopt-feed-out", format!("{}: {e} (a new file: an earlier one is never overwritten)", path.display())))?;
    f.write_all(text.as_bytes()).and_then(|_| f.sync_all()).map_err(|e| format!("{}: {e}", path.display()))
}
/// Pure: **the next look at a plugin job** — `GET /box/plugins/<job>` while the box says it is
/// `queued` or `running`; `None` once it is `done` or `failed` (or the answer is no job).
pub fn plugins_next(body: &str) -> Option<Ask> {
    let v: serde_json::Value = serde_json::from_str(body.trim()).ok()?;
    let job = v["job"].as_str().filter(|j| !j.is_empty())?;
    matches!(v["state"].as_str(), Some("queued" | "running")).then(|| Ask { method: "GET", path: format!("/box/plugins/{job}"), body: String::new(), what: What::Plugins })
}

/// How often `site first-link` looks at a handed-off site's main's answer again, and for how
/// long at most (HOL-MAIL): the main pulls its order on its next round.
pub const FIRST_LINK_EVERY: std::time::Duration = std::time::Duration::from_secs(5);
pub const FIRST_LINK_FOR: std::time::Duration = std::time::Duration::from_secs(300);

/// Pure: **the order `site first-link` queued for a handed-off site's main** (`asked`, the
/// signed `sign_in_again`'s seq) — 0 when no main was asked.
pub fn first_link_asked(body: &str) -> u64 {
    serde_json::from_str::<serde_json::Value>(body.trim()).ok().and_then(|v| v["asked"].as_u64()).unwrap_or(0)
}

/// Pure: **the main's answer to the order `asked`, in a site's status** — `None` while the
/// newest order it answered is older; `Ok` minted and mailed, `Err` refused by name.
pub fn first_link_answer(estate: &str, asked: u64, status_body: &str) -> Option<Result<String, String>> {
    let v: serde_json::Value = serde_json::from_str(status_body.trim()).ok()?;
    let answered = v["first_link_answered"].as_u64().unwrap_or(0);
    if answered == 0 || answered < asked {
        return None;
    }
    let words = v["first_admin_door_words"].as_str().unwrap_or_default();
    Some(if v["first_link_mailed"].as_bool().unwrap_or(false) {
        Ok(format!("{estate}'s main minted a new sign-in link and mailed it (order {answered}): {words}\n"))
    } else {
        Err(format!("REFUSED by {estate}'s main (order {answered}): {words}"))
    })
}

/// One request at the box's door, as [`follow_first_link`] asks it (the real [`door`], or a test's).
pub type AskDoor<'a> = dyn FnMut(&Ask) -> Result<(u16, String), String> + 'a;

/// **A handed-off site's first link, as its MAIN answered it** (HOL-MAIL, holger-rs 2026-10-27):
/// the door only queued the signed order `asked`; the site's status is looked at through
/// `ask_door` every `every` until its main answered that order ([`first_link_answer`]), for at
/// most `for_` (looked at once at least). Not answered in time: an error that says nothing is
/// known to be mailed — never a promise.
pub fn follow_first_link(estate: &str, asked: u64, ask_door: &mut AskDoor<'_>, every: std::time::Duration, for_: std::time::Duration) -> Result<String, String> {
    let look = Ask { method: "GET", path: format!("/site/status/{estate}"), body: String::new(), what: What::Status };
    let started = std::time::Instant::now();
    loop {
        let (status, body) = ask_door(&look)?;
        if status != 200 {
            return said(What::Status, status, &body);
        }
        if let Some(answer) = first_link_answer(estate, asked, &body) {
            return answer;
        }
        if started.elapsed() >= for_ {
            return Err(format!(
                "{estate}'s main has not answered the sign-in order {asked} within {} s: nothing is known to be mailed; `site status {estate}` says its answer once it pulls the order",
                for_.as_secs()
            ));
        }
        std::thread::sleep(every);
    }
}

/// Pure: **the main's answer to the `refeed` order `asked`, in a site's status** — `None`
/// while the newest order it answered is older; `Ok` kept, `Err` refused by name.
pub fn refeed_answer(estate: &str, asked: u64, status_body: &str) -> Option<Result<String, String>> {
    let v: serde_json::Value = serde_json::from_str(status_body.trim()).ok()?;
    let answered = v["refeed_answered"].as_u64().unwrap_or(0);
    if answered == 0 || answered < asked {
        return None;
    }
    let words = v["refeed_words"].as_str().unwrap_or_default();
    Some(if v["refeed_kept"].as_bool().unwrap_or(false) {
        Ok(format!("{words}\n"))
    } else {
        Err(format!("REFUSED by {estate}'s main (order {answered}): {words}"))
    })
}

/// **A laid site's refeed, as its MAIN answered it** (MAIL-RELAY 2026-10-28): the door only
/// queued the signed order `asked`; the site's status is looked at through `ask_door` every
/// `every` until its main answered that order ([`refeed_answer`]), for at most `for_` (looked
/// at once at least). Not answered in time: an error that says nothing is known to be kept.
pub fn follow_refeed(estate: &str, asked: u64, ask_door: &mut AskDoor<'_>, every: std::time::Duration, for_: std::time::Duration) -> Result<String, String> {
    let look = Ask { method: "GET", path: format!("/site/status/{estate}"), body: String::new(), what: What::Status };
    let started = std::time::Instant::now();
    loop {
        let (status, body) = ask_door(&look)?;
        if status != 200 {
            return said(What::Status, status, &body);
        }
        if let Some(answer) = refeed_answer(estate, asked, &body) {
            return answer;
        }
        if started.elapsed() >= for_ {
            return Err(format!(
                "{estate}'s main has not answered the refeed order {asked} within {} s: nothing is known to be kept; `site status {estate}` says its answer once it pulls the order",
                for_.as_secs()
            ));
        }
        std::thread::sleep(every);
    }
}

/// How often `box plugins` looks at its job again, and for how long at most.
pub const PLUGINS_EVERY: std::time::Duration = std::time::Duration::from_secs(2);
pub const PLUGINS_FOR: std::time::Duration = std::time::Duration::from_secs(900);

/// Pure: **a put, from a shown site with the flags over it** — what is not given stays as
/// shown. The secrets are read from their files; nothing else carries them.
pub fn put_body(shown: &serde_json::Value, words: &[String], read_file: &dyn Fn(&Path) -> Result<String, String>) -> Result<serde_json::Value, String> {
    let w: Vec<&str> = words.iter().map(String::as_str).collect();
    let flag = |name: &str| w.iter().position(|a| *a == name).and_then(|i| w.get(i + 1)).map(|v| v.to_string());
    let s = &shown["site"];
    let word = |k: &str, f: &str| flag(f).unwrap_or_else(|| s[k].as_str().unwrap_or_default().to_string());
    let yes = |k: &str, f: &str| -> Result<bool, String> {
        match flag(f).as_deref() {
            None => Ok(s[k].as_bool().unwrap_or(false)),
            Some("yes") => Ok(true),
            Some("no") => Ok(false),
            Some(o) => Err(refuse("site-flag", format!("{f} yes|no, not {o:?}"))),
        }
    };
    let n = |k: &str, f: &str| -> Result<u64, String> {
        match flag(f) {
            None => Ok(s["terms"][k].as_u64().unwrap_or(0)),
            Some(v) => v.trim().parse().map_err(|_| refuse("site-flag", format!("{f} <a number>, not {v:?}"))),
        }
    };
    let live = match flag("--mode").as_deref() {
        None => s["live"].as_bool().unwrap_or(false),
        Some("live") => true,
        Some("test") => false,
        Some(o) => return Err(refuse("site-flag", format!("--mode test|live, not {o:?}"))),
    };
    let secret = |f: &str| -> Result<String, String> {
        match flag(f) {
            None => Ok(String::new()),
            Some(file) => {
                let v = read_file(Path::new(&file))?.trim().to_string();
                if v.is_empty() || v.contains('\n') {
                    return Err(refuse("site-secret-file", format!("{file} holds one secret on one line")));
                }
                Ok(v)
            }
        }
    };
    Ok(serde_json::json!({
        "estate": s["estate"].as_str().unwrap_or_default(),
        "create": false,
        "domain": word("domain", "--domain"),
        "product": word("product", "--product"),
        "first_admin": word("first_admin", "--first-admin"),
        "operator_site": yes("operator_site", "--operator-site")?,
        "terminable": yes("terminable", "--terminable")?,
        "vendor": word("vendor", "--vendor"),
        "vendor_secret_key": secret("--stripe-key-file")?,
        "vendor_webhook_secret": secret("--webhook-secret-file")?,
        "live": live,
        "terms": { "term_days": n("term_days", "--term-days")?, "term_months": n("term_months", "--term-months")?, "grace_hours": n("grace_hours", "--grace-hours")?, "margin_bp": n("margin_bp", "--margin-bp")? },
        "media_from": word("media_from", "--media-from"),
    }))
}

/// Pure: `edit`'s put, from the box's show of the site and the flags given.
pub fn edit_body(shown_body: &str, words: &[String], read_file: &dyn Fn(&Path) -> Result<String, String>) -> Result<Ask, String> {
    let shown: serde_json::Value = serde_json::from_str(shown_body.trim()).map_err(|e| format!("the box's show is not JSON: {e}"))?;
    let body = put_body(&shown, words, read_file)?;
    Ok(Ask { method: "POST", path: "/site/put".into(), body: body.to_string(), what: What::Put })
}

/// Pure: `site installer-log <site> <vm> [--since <n>]` — one read at the door.
fn installer_log_request(w: &[&str]) -> Result<Ask, String> {
    let mut free = Vec::new();
    let mut since = None;
    let mut it = w.iter().skip(2);
    while let Some(a) = it.next() {
        if *a == "--since" {
            since = Some(it.next().and_then(|n| n.parse::<u64>().ok()).ok_or_else(|| refuse("site-installer-log", "--since <n>: the first line to read, a whole number (0: from the start)"))?);
        } else {
            free.push(*a);
        }
    }
    let site = free.first().filter(|s| !s.starts_with("--")).ok_or_else(|| refuse("site", "`site installer-log` names the site: its label, as the box declares it (`monetize site list`)"))?;
    let vm = free.get(1).ok_or_else(|| refuse("site-installer-log", format!("name the box: `monetize site installer-log {site} <vm>` (front, main, twin)")))?;
    Ok(Ask { method: "GET", path: format!("/site/installer-log/{site}/{vm}{}", since.map(|n| format!("?since={n}")).unwrap_or_default()), body: String::new(), what: What::InstallerLog })
}

/// Pure: `site narrate <site> [<vm>] [--tail <n>]` — one read at the door.
fn narrate_request(w: &[&str]) -> Result<Ask, String> {
    let mut free = Vec::new();
    let mut tail = None;
    let mut it = w.iter().skip(2);
    while let Some(a) = it.next() {
        if *a == "--tail" {
            let n = it.next().and_then(|n| n.parse::<u32>().ok()).filter(|n| *n > 0).ok_or_else(|| refuse("site-narrate-tail", "--tail <n>: how many of the box's last lines, a whole number of at least 1"))?;
            tail = Some(n);
        } else {
            free.push(*a);
        }
    }
    let site = free.first().filter(|s| !s.starts_with("--")).ok_or_else(|| refuse("site", "`site narrate` names the site: its label, as the box declares it (`monetize site list`)"))?;
    match (free.get(1), tail) {
        (None, Some(_)) => Err(refuse("site-narrate-tail", format!("--tail reads one box's log: name it (`monetize site narrate {site} <vm> --tail <n>`)"))),
        (None, None) => Ok(Ask { method: "GET", path: format!("/site/narrate/{site}"), body: String::new(), what: What::Narrate }),
        (Some(vm), tail) => Ok(Ask { method: "GET", path: format!("/site/narrate/{site}/{vm}{}", tail.map(|n| format!("?tail={n}")).unwrap_or_default()), body: String::new(), what: What::NarrateText }),
    }
}

/// Pure: **the run's own `why` as one line** (`<head><why> (since <when>)`), empty when none.
fn why_line(head: &str, why: &str, since: &str) -> String {
    match (why.trim(), since.trim()) {
        ("", _) => String::new(),
        (w, "") => format!("{head}{w}\n"),
        (w, t) => format!("{head}{w} (since {t})\n"),
    }
}

/// Pure: **the box's answer, as the operator reads it** — its own words; `Err` for a refusal.
pub fn said(what: What, status: u16, body: &str) -> Result<String, String> {
    let v: serde_json::Value = serde_json::from_str(body.trim()).unwrap_or(serde_json::Value::Null);
    let s = |k: &str| v[k].as_str().unwrap_or_default().to_string();
    if status != 200 {
        let words = if v["words"].is_string() { format!("{} ({})", s("words"), s("refused")) } else { body.trim().chars().take(600).collect() };
        return Err(format!("REFUSED by the box (HTTP {status}): {words}"));
    }
    Ok(match what {
        What::List => {
            let mut out = String::new();
            for site in v["sites"].as_array().into_iter().flatten() {
                let g = |k: &str| site[k].as_str().unwrap_or_default();
                out.push_str(&format!("{}\n  domain       {d}, www.{d}, git.{d} — already fed\n  Stripe       {} — already fed\n  DNS          {}\n  cloud        {} — already fed\n  first admin  {}\n  state        {} — {}\n", g("estate"), g("vendor"), g("dns_words"), g("cloud_words"), g("first_admin"), g("state"), g("words"), d = g("domain")));
                if !g("checkout_url").is_empty() {
                    out.push_str(&format!("  pay here     {}\n", g("checkout_url")));
                }
                out.push_str(&why_line("  why          ", g("why"), g("why_since")));
                out.push_str(&match (site["orderable"].as_bool().unwrap_or(false), g("why_not")) {
                    (true, _) => "  can be ordered now\n".to_string(),
                    (false, why) => format!("  cannot be ordered: {why}\n"),
                });
                out.push_str(if site["terminable"].as_bool().unwrap_or(false) { "  terminable\n" } else { "  protected from terminate\n" });
            }
            let sizes: Vec<&str> = v["sizes"].as_array().into_iter().flatten().filter_map(|z| z.as_str()).collect();
            out.push_str(&format!("sizes: {}\n", sizes.join(", ")));
            out
        }
        What::Order => format!("{}\nreference: {}\npay {} here:\n{}\n", s("words"), s("reference"), s("price_words"), s("checkout_url")),
        What::Status => format!(
            "{} — {} ({})\n{}\n{}{}{}{}",
            s("company"),
            s("state"),
            s("phase"),
            s("words"),
            why_line("why: ", &s("why"), &s("why_since")),
            if s("paid_until").is_empty() { String::new() } else { format!("paid until {}\n", s("paid_until")) },
            if s("first_admin_door").is_empty() { String::new() } else { format!("first admin: {} — {}\n", s("first_admin_door"), s("first_admin_door_words")) },
            if s("refeed_words").is_empty() { String::new() } else { format!("refeed: {}\n", s("refeed_words")) }
        ),
        What::Cancel => format!("{}\n", s("words")),
        What::Do => format!("{}\n", s("words")),
        What::DoAnswer => match (v["answered"].as_bool().unwrap_or(false), v["ok"].as_bool().unwrap_or(false)) {
            (false, _) => format!("{}: not pulled yet\n", s("kind")),
            (true, true) => format!("{}\n", s("detail")),
            (true, false) => return Err(format!("REFUSED by the site's box ({}): {}", s("kind"), s("detail"))),
        },
        What::Kinds => {
            let mut out = String::new();
            for k in v["kinds"].as_array().into_iter().flatten() {
                let g = |n: &str| k[n].as_str().unwrap_or_default().to_string();
                let params: Vec<String> = k["params"].as_array().into_iter().flatten().map(|p| format!("{}{}", p["name"].as_str().unwrap_or_default(), if p["required"].as_bool().unwrap_or(false) { "*" } else { "" })).collect();
                out.push_str(&format!("{}  ({}) {}\n  params: {}{}\n", g("kind"), if g("role").is_empty() { "primary".to_string() } else { g("role") }, g("words"), if params.is_empty() { "none".to_string() } else { params.join(", ") }, if g("file").is_empty() { String::new() } else { format!("; file: {}", g("file")) }));
            }
            if out.is_empty() { "the site's product declares no order kinds\n".into() } else { out }
        }
        What::ChangePlan => format!(
            "{}\nreference: {}\nstatus: {}\n{}",
            s("words"),
            s("reference"),
            s("status"),
            if s("checkout_url").is_empty() { String::new() } else { format!("pay {} here:\n{}\n", s("price_words"), s("checkout_url")) }
        ),
        What::FirstLink | What::Refeed => format!("{}\n", s("words")),
        What::Console => format!("HTTP {}\n{}{}", v["status"].as_u64().unwrap_or(0), s("body"), if s("body").ends_with('\n') || s("body").is_empty() { "" } else { "\n" }),
        What::TestOrder => format!("{}\nreference: {}\n{} held with the test card: nobody pays at a page\n", s("words"), s("reference"), s("price_words")),
        What::NarrateText | What::InstallerLog => body.to_string(),
        What::Settle => format!("{}\nphase: {}\n", s("words"), s("phase")),
        What::Narrate => {
            let mut out = format!("{} — {}\n{}\n", s("estate"), if v["narrated"].as_bool().unwrap_or(false) { "narrated" } else { "NOT narrated" }, s("words"));
            for vm in v["vms"].as_array().into_iter().flatten() {
                let g = |k: &str| vm[k].as_str().unwrap_or_default();
                match vm["lines"].as_u64().unwrap_or(0) {
                    0 => out.push_str(&format!("  {:<5} nothing heard yet\n", g("vm"))),
                    n => out.push_str(&format!("  {:<5} {n} lines, {} bytes kept, updated {}\n        last: {}\n", g("vm"), vm["bytes"].as_u64().unwrap_or(0), g("updated"), g("last"))),
                }
            }
            out.push_str(&format!("one box's log: monetize site narrate {} <vm> [--tail <n>]\n", s("estate")));
            out
        }
        What::Put | What::Edit => {
            let changed: Vec<&str> = v["changed"].as_array().into_iter().flatten().filter_map(|c| c.as_str()).collect();
            format!("{}\n{}", s("words"), changed.iter().map(|c| format!("  {c}\n")).collect::<String>())
        }
        What::Show => {
            let site = &v["site"];
            let g = |k: &str| site[k].as_str().unwrap_or_default().to_string();
            let b = |k: &str| if site[k].as_bool().unwrap_or(false) { "yes" } else { "no" };
            let secret = |set: &str, fp: &str| if site[set].as_bool().unwrap_or(false) { format!("set ({})", g(fp)) } else { "not set".to_string() };
            let t = |k: &str| match site["terms"][k].as_u64().unwrap_or(0) { 0 => "the shop's".to_string(), n => n.to_string() };
            let list = |k: &str| v[k].as_array().into_iter().flatten().filter_map(|x| x.as_str()).collect::<Vec<_>>().join(", ");
            let mut out = format!(
                "{} ({})\n  domain         {}\n  product        {}\n  first admin    {}\n  operator site  {}\n  terminable     {}\n  Stripe         {} ({})\n  secret key     {}\n  webhook secret {}\n  terms          days {} · months {} · grace hours {} · margin bp {}\n  media from     {}\n",
                g("estate"),
                if g("source").is_empty() { "new".to_string() } else { g("source") },
                g("domain"),
                g("product"),
                g("first_admin"),
                b("operator_site"),
                b("terminable"),
                g("vendor"),
                if site["live"].as_bool().unwrap_or(false) { "live" } else { "test" },
                secret("secret_key_set", "secret_key_fingerprint"),
                secret("webhook_secret_set", "webhook_secret_fingerprint"),
                t("term_days"),
                t("term_months"),
                t("grace_hours"),
                t("margin_bp"),
                if g("media_from").is_empty() { "as it is".to_string() } else { g("media_from") },
            );
            out.push_str(&format!("choices: products {} · Stripe accounts {} · media {}\nthe shop's: {} · sizes {}\n", list("products"), list("vendors"), list("media_sites"), v["currency"].as_str().unwrap_or_default(), list("sizes")));
            for h in v["history"].as_array().into_iter().flatten().filter_map(|x| x.as_str()) {
                out.push_str(&format!("  {h}\n"));
            }
            out
        }
        What::UpdateCancelPlan => format!("{}PLAN only: nothing was cancelled; say `estate update-cancel <estate> --apply` to cancel the open update (the boxes done stay done)\n", said(What::Update, status, body)?),
        What::BoxPlan => format!("{}PLAN only: nothing was staged and the box did not restart; say `box update --apply` (or `box update --back --apply`) to do it\n", said(What::BoxRunning, status, body)?),
        What::BoxRunning => format!(
            "runs     app {} (slot {}), BASE {}\nlive     {}\nnext     app {} (build {}), BASE {}{}\nupdate   {}\nback     {}\n",
            s("app"),
            s("slot"),
            s("base"),
            if v["live_update"].as_bool().unwrap_or(false) { "on" } else { "OFF" },
            s("next_app"),
            s("next_build"),
            s("next_base"),
            if s("next_key").is_empty() { String::new() } else { format!(" (from {})", s("next_key")) },
            s("plan"),
            if s("back").is_empty() { "(this box does not say)".to_string() } else { s("back") }
        ),
        What::BoxUpdate => format!("{}\n", s("words")),
        // The box's words, one block per certificate (edda estate-front's lines); nothing decided here.
        What::BoxCerts => {
            let certs = v["certificates"].as_array().cloned().unwrap_or_default();
            if certs.is_empty() {
                return Ok("no certificate is known to this box yet\n".to_string());
            }
            let mut out = String::new();
            for c in &certs {
                let hosts: Vec<&str> = c["hosts"].as_array().into_iter().flatten().filter_map(|h| h.as_str()).collect();
                out.push_str(&format!("{} ({})\n", c["host"].as_str().unwrap_or_default(), hosts.join(", ")));
                for l in c["lines"].as_array().into_iter().flatten().filter_map(|l| l.as_str()) {
                    out.push_str(&format!("  {l}\n"));
                }
            }
            out
        }
        // The feed is NEVER said: [`run`] writes it to `--feed-out`.
        What::Adopt => format!("{}\nrun: {} ({}) at {}\nclient key (public): {}\nfeed lines: {}\n", s("words"), s("company"), s("reference"), s("host"), s("client_key"), v["feed"].as_object().map(|f| f.keys().cloned().collect::<Vec<_>>().join(", ")).unwrap_or_default()),
        What::Inventory => inventory_words(&v),
        What::Estates => estates_words(&v),
        What::Plugins => {
            if s("state") == "failed" {
                return Err(format!("REFUSED the plugin job {} failed: {}", s("job"), s("error")));
            }
            let mut out = String::new();
            for r in v["rows"].as_array().into_iter().flatten() {
                let g = |k: &str| r[k].as_str().unwrap_or_default();
                out.push_str(&format!("{} ({}): {}{}\n", g("name"), g("role"), g("outcome"), if g("detail").is_empty() { String::new() } else { format!(" — {}", g("detail")) }));
            }
            if !matches!(s("state").as_str(), "done") {
                out.push_str(&format!("the job {} is {} — `monetize box plugins` again to see it\n", s("job"), s("state")));
            } else if !v["apply"].as_bool().unwrap_or(false) {
                out.push_str("nothing was written: add --apply to take them (live, no restart)\n");
            }
            out
        }
        What::Update => {
            let mut out = format!("{}\nrelease sold now: {}\nlive updates: {}\n", s("host"), s("release"), if v["live_on"].as_bool().unwrap_or(false) { "on" } else { "OFF (keep media only)" });
            for role in ["twin", "main", "front"] {
                out.push_str(&format!("  {role:<5} runs {}\n", v["release_of"][role].as_str().unwrap_or("(unknown)")));
            }
            for st in v["steps"].as_array().into_iter().flatten() {
                let g = |k: &str| st[k].as_str().unwrap_or_default();
                out.push_str(&format!("  step {:<5} {}{}{}\n", g("role"), g("state"), if g("how").is_empty() { String::new() } else { format!(" ({})", g("how")) }, if g("words").is_empty() { String::new() } else { format!(" — {}", g("words")) }));
            }
            out.push_str(&format!("{}\n", s("words")));
            out
        }
        What::Terminate => {
            format!("{}\n", s("words"))
        }
    })
}

/// **One request at the box's door**: mutual TLS with the operator's certificate under the
/// box's installer CA (`<material>/` as the installer keeps it). `(status, body)`.
pub fn door(t: &Target, ask: &Ask) -> Result<(u16, String), String> {
    use nordisk_pairing::installer::{CA_CERT, INSTALLER_CERT, INSTALLER_KEY};
    use std::net::ToSocketAddrs as _;
    if t.door.trim().is_empty() {
        return Err(refuse("door", format!("which box? --door <host:8475> (or ${DOOR_ENV})")));
    }
    if t.material.as_os_str().is_empty() {
        return Err(refuse("material", format!("--material <dir> (or ${MATERIAL_ENV}): the directory holding the operator's certificate for the box's door")));
    }
    let read = |f: &str| std::fs::read_to_string(t.material.join(f)).map_err(|e| refuse("material", format!("{}: {e}", t.material.join(f).display())));
    let me = nordisk_pairing::mtls::Issued { cert_pem: read(INSTALLER_CERT)?, key_pem: read(INSTALLER_KEY)? };
    let cfg = nordisk_pairing::mtls::client_config(&read(CA_CERT)?, &me).map_err(|e| refuse("material", e))?;
    let dial = Dial {
        addr: t.door.clone(),
        sock: first_address(&t.door, t.door.to_socket_addrs())?,
        name: nordisk_pairing::mtls::vm_name(),
        cfg,
        host: nordisk_pairing::mtls::VM_SERVER_NAME.to_string(),
        headers: Vec::new(),
        wait: std::time::Duration::from_secs(120),
    };
    https(&dial, ask)
}

/// **Where one HTTPS request goes** (the one client, [`https`]): the address, the TLS name
/// and roots it is checked against, the `Host:` and the extra headers, and how long the
/// answer may take.
struct Dial {
    /// `host:port`, as the error words say it.
    addr: String,
    sock: std::net::SocketAddr,
    name: rustls::pki_types::ServerName<'static>,
    cfg: std::sync::Arc<rustls::ClientConfig>,
    host: String,
    headers: Vec<(String, String)>,
    wait: std::time::Duration,
}

/// The first address a name resolves to, or the refusal naming it.
fn first_address(addr: &str, found: std::io::Result<impl Iterator<Item = std::net::SocketAddr>>) -> Result<std::net::SocketAddr, String> {
    found.map_err(|e| format!("{addr}: {e}"))?.next().ok_or_else(|| format!("{addr}: no address"))
}

/// Pure: **one request's bytes**: HTTP/1.1, one JSON request, `Connection: close`, the extra
/// headers after `Host:`.
pub fn request_text(host: &str, headers: &[(String, String)], ask: &Ask) -> String {
    let extra: String = headers.iter().map(|(k, v)| format!("{k}: {v}\r\n")).collect();
    format!("{} {} HTTP/1.1\r\nHost: {host}\r\n{extra}Content-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", ask.method, ask.path, ask.body.len(), ask.body)
}

/// **The one HTTPS client** (RA1-4): the box's door and a product's front both. One TCP
/// dial (10 s), one TLS stream, the request, the answer read to its close (or the dial's
/// wait), then [`http_answer`]. It goes nowhere but the address it is given.
fn https(d: &Dial, ask: &Ask) -> Result<(u16, String), String> {
    use std::io::{Read as _, Write as _};
    let addr = &d.addr;
    let mut tcp = std::net::TcpStream::connect_timeout(&d.sock, std::time::Duration::from_secs(10)).map_err(|e| format!("{addr}: {e}"))?;
    tcp.set_read_timeout(Some(d.wait)).map_err(|e| e.to_string())?;
    let conn = rustls::ClientConnection::new(d.cfg.clone(), d.name.clone()).map_err(|e| format!("{addr}: TLS: {e}"))?;
    let mut tls = rustls::StreamOwned::new(conn, &mut tcp);
    tls.write_all(request_text(&d.host, &d.headers, ask).as_bytes()).map_err(|e| format!("{addr}: {e}"))?;
    let mut buf = Vec::new();
    // A peer that closes without TLS's close_notify still sent its whole answer.
    if let Err(e) = tls.read_to_end(&mut buf) {
        if buf.is_empty() {
            return Err(format!("{addr}: {e}"));
        }
    }
    http_answer(&buf).map_err(|e| format!("{addr}: {e}"))
}

/// **One request at a product's front** (`front certs`): where, under which roots, with which
/// credential header, and the request.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FrontAsk {
    pub host: String,
    pub port: u16,
    /// A PEM file of the roots to trust instead of the web's (a front under its own CA).
    pub ca: Option<PathBuf>,
    /// The admin's credential: (`Authorization`, `Bearer …`) or (`Cookie`, …).
    pub header: (String, String),
    pub ask: Ask,
}

/// Pure: **`front certs --at https://<site>/<door> …`** as one request, or the refusal naming
/// what is missing. The door is the path the operator names: each product's front says its
/// own (RA1-3), and the core holds none. `read_file` reads the credential's file.
pub fn front_request_of(words: &[String], read_file: &dyn Fn(&Path) -> Result<String, String>) -> Result<FrontAsk, String> {
    let w: Vec<&str> = words.iter().map(String::as_str).collect();
    let flag = |name: &str| w.iter().position(|a| *a == name).and_then(|i| w.get(i + 1)).map(|v| v.to_string());
    let at = flag("--at").unwrap_or_default();
    let Some(rest) = at.strip_prefix("https://") else {
        return Err(refuse("front-at", "--at https://<the site>/<its certificate door> (the front's own name; https only)"));
    };
    if let Some(word) = w.get(2).filter(|a| !a.starts_with("--")) {
        return Err(refuse("front-door", format!("`front certs {word}`: name the front's certificate door in the address instead, --at https://<site>/<door> (the ONE box's own: `monetize box certs`)")));
    }
    let (authority, path) = rest.split_once('/').map(|(a, p)| (a, format!("/{p}"))).unwrap_or((rest, String::new()));
    let door = path.trim_end_matches('/');
    if door.is_empty() || door.contains(['?', '#']) || door.chars().any(char::is_whitespace) {
        return Err(refuse("front-door", format!("--at {at}: the front's certificate door as a path, https://<host>[:<port>]/<door> (the one its console page reads)")));
    }
    if authority.is_empty() {
        return Err(refuse("front-at", format!("--at {at}: https://<host>[:<port>]/<door>")));
    }
    let (host, port) = match authority.rsplit_once(':').filter(|(h, _)| !h.contains(':') || h.ends_with(']')) {
        Some((h, p)) => (h.trim_matches(['[', ']']).to_string(), p.parse::<u16>().map_err(|_| refuse("front-at", format!("--at {at}: the port is not a number")))?),
        None => (authority.trim_matches(['[', ']']).to_string(), 443),
    };
    let secret = |name: &str| -> Result<Option<String>, String> {
        let Some(f) = flag(name) else { return Ok(None) };
        let v = read_file(Path::new(&f))?.trim().to_string();
        if v.is_empty() {
            return Err(refuse("front-credential", format!("{name} {f}: the file is empty")));
        }
        Ok(Some(v))
    };
    let header = match (secret("--token-file")?, secret("--cookie-file")?) {
        (Some(t), None) => ("Authorization".to_string(), format!("Bearer {t}")),
        (None, Some(c)) => ("Cookie".to_string(), c),
        (Some(_), Some(_)) => return Err(refuse("front-credential", "--token-file or --cookie-file, not both")),
        (None, None) => return Err(refuse("front-credential", "the certificates are an admin's: --token-file <file> (an admin's bearer token) or --cookie-file <file> (a console session cookie, `<name>=<value>`)")),
    };
    let ask = match flag("--try") {
        Some(h) => Ask { method: "POST", path: format!("{door}/try"), body: serde_json::json!({ "host": h }).to_string(), what: What::BoxUpdate },
        None => Ask { method: "GET", path: door.to_string(), body: String::new(), what: What::BoxCerts },
    };
    Ok(FrontAsk { host, port, ca: flag("--ca").map(PathBuf::from), header, ask })
}

/// Pure: an HTTP/1.1 answer's status and body (a chunked body put back together).
pub fn http_answer(raw: &[u8]) -> Result<(u16, String), String> {
    let text = String::from_utf8_lossy(raw);
    let (head, body) = text.split_once("\r\n\r\n").ok_or("no HTTP answer")?;
    let status = head.split_whitespace().nth(1).and_then(|s| s.parse().ok()).ok_or("no HTTP answer")?;
    let chunked = head.lines().any(|l| l.split_once(':').is_some_and(|(k, v)| k.trim().eq_ignore_ascii_case("transfer-encoding") && v.to_ascii_lowercase().contains("chunked")));
    if !chunked {
        return Ok((status, body.to_string()));
    }
    let (mut out, mut rest) = (String::new(), body);
    while let Some((size, after)) = rest.split_once("\r\n") {
        let n = usize::from_str_radix(size.split(';').next().unwrap_or("").trim(), 16).map_err(|_| "a chunked answer that does not parse")?;
        if n == 0 || after.len() < n {
            break;
        }
        out.push_str(&after[..n]);
        rest = after[n..].trim_start_matches("\r\n");
    }
    Ok((status, out))
}

/// **One request at a product's front**: HTTPS under the web's roots (or `--ca`), the admin's
/// credential in its header. `(status, body)`.
pub fn front(fa: &FrontAsk) -> Result<(u16, String), String> {
    use rustls::pki_types::pem::PemObject as _;
    use std::net::ToSocketAddrs as _;
    let mut roots = rustls::RootCertStore::empty();
    match &fa.ca {
        Some(p) => {
            for c in rustls::pki_types::CertificateDer::pem_file_iter(p).map_err(|e| refuse("front-ca", format!("{}: {e}", p.display())))? {
                roots.add(c.map_err(|e| refuse("front-ca", format!("{}: {e}", p.display())))?).map_err(|e| refuse("front-ca", format!("{}: {e}", p.display())))?;
            }
        }
        None => roots.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned()),
    }
    let cfg = rustls::ClientConfig::builder_with_provider(std::sync::Arc::new(rustls::crypto::ring::default_provider()))
        .with_safe_default_protocol_versions()
        .map_err(|e| e.to_string())?
        .with_root_certificates(roots)
        .with_no_client_auth();
    let addr = format!("{}:{}", fa.host, fa.port);
    let dial = Dial {
        sock: first_address(&addr, (fa.host.as_str(), fa.port).to_socket_addrs())?,
        name: rustls::pki_types::ServerName::try_from(fa.host.clone()).map_err(|e| format!("{}: {e}", fa.host))?,
        cfg: std::sync::Arc::new(cfg),
        host: if fa.port == 443 { fa.host.clone() } else { addr.clone() },
        headers: vec![fa.header.clone(), ("Accept".to_string(), "application/json".to_string())],
        wait: std::time::Duration::from_secs(60),
        addr,
    };
    https(&dial, &fa.ask)
}

/// The whole command: parse, ask the door once, say the answer.
pub fn run(args: &[String]) -> Result<String, String> {
    if args.is_empty() || args.iter().any(|a| a == "--help" || a == "-h") {
        return Ok(format!("{}\n", help()));
    }
    if args.first().map(String::as_str) == Some("front") && args.get(1).map(String::as_str) == Some("certs") {
        let fa = front_request_of(args, &|p| std::fs::read_to_string(p).map_err(|e| refuse("front-credential", format!("{}: {e}", p.display()))))?;
        let (status, body) = front(&fa)?;
        if status != 200 {
            return Err(format!("REFUSED by the front (HTTP {status}): {}", body.trim().chars().take(600).collect::<String>()));
        }
        return said(fa.ask.what, status, &body).map(|out| if fa.ask.what == What::BoxCerts { out.replace("no certificate is known to this box yet", "no certificate is known to this front yet") } else { out });
    }
    let (target, words) = target_of(args, &|k| std::env::var(k).ok().filter(|v| !v.trim().is_empty()))?;
    let ask = request_of(&words, &|p| std::fs::read_to_string(p).map_err(|e| refuse("site-file", format!("{}: {e}", p.display()))))?;
    let ask = with_return(ask, &target.door);
    let ask = if ask.what == What::Console { console_body(ask, &|p| std::fs::read(p).map_err(|e| refuse("site-file", format!("{}: {e}", p.display()))))? } else { ask };
    let wait = ask.what == What::Do && serde_json::from_str::<serde_json::Value>(&ask.body).ok().is_some_and(|v| v["wait"].as_bool().unwrap_or(true));
    let ask = if ask.what == What::Do { do_file(ask, &|p| std::fs::read(p).map_err(|e| refuse("site-file", format!("{}: {e}", p.display()))))? } else { ask };
    let feed_out = words.iter().position(|a| a == "--feed-out").and_then(|i| words.get(i + 1)).map(PathBuf::from);
    if ask.what == What::Adopt {
        if let Some(p) = feed_out.as_ref().filter(|p| p.exists()) {
            return Err(refuse("adopt-feed-out", format!("{} is there already: a new file (an earlier one is never overwritten)", p.display())));
        }
    }
    let (status, body) = door(&target, &ask)?;
    if ask.what == What::Adopt && status == 200 {
        let p = feed_out.ok_or_else(|| refuse("adopt-feed-out", "--feed-out <file>"))?;
        // Written BEFORE anything is said: the answer is the only copy of the client key.
        write_secret_file(&p, &adopt_feed(&body)?)?;
        return said(What::Adopt, status, &body).map(|out| format!("{out}the front's feed lines are in {} (0600): feed them to the front, then delete the file\n", p.display()));
    }
    if ask.what == What::Plugins && status == 200 {
        let (mut body, started) = (body, std::time::Instant::now());
        while let Some(next) = plugins_next(&body) {
            if started.elapsed() > PLUGINS_FOR {
                break;
            }
            std::thread::sleep(PLUGINS_EVERY);
            let (st, b) = door(&target, &next)?;
            if st != 200 {
                return said(What::Plugins, st, &b);
            }
            body = b;
        }
        return said(What::Plugins, status, &body);
    }
    // HOL-MAIL: a handed-off site's main was asked; its answer is the answer, never a promise.
    if ask.what == What::FirstLink && status == 200 {
        let asked = first_link_asked(&body);
        if asked > 0 {
            let estate = serde_json::from_str::<serde_json::Value>(&ask.body).ok().and_then(|v| v["estate"].as_str().map(str::to_owned)).unwrap_or_default();
            eprint!("{}", said(What::FirstLink, status, &body)?);
            return follow_first_link(&estate, asked, &mut |a: &Ask| door(&target, a), FIRST_LINK_EVERY, FIRST_LINK_FOR);
        }
    }
    // MONETIZE-FIX: a product order was queued; the box's answer is the answer.
    if ask.what == What::Do && status == 200 && wait {
        let v: serde_json::Value = serde_json::from_str(body.trim()).unwrap_or(serde_json::Value::Null);
        if let Some(seq) = v["seq"].as_u64().filter(|s| *s > 0) {
            let estate = serde_json::from_str::<serde_json::Value>(&ask.body).ok().and_then(|v| v["estate"].as_str().map(str::to_owned)).unwrap_or_default();
            eprint!("{}", said(What::Do, status, &body)?);
            return follow_do(&estate, seq, &mut |a: &Ask| door(&target, a), FIRST_LINK_EVERY, FIRST_LINK_FOR);
        }
    }
    // MAIL-RELAY: the main was fed its keys again; its answer is the answer.
    if ask.what == What::Refeed && status == 200 {
        let asked = first_link_asked(&body);
        if asked > 0 {
            let estate = serde_json::from_str::<serde_json::Value>(&ask.body).ok().and_then(|v| v["estate"].as_str().map(str::to_owned)).unwrap_or_default();
            eprint!("{}", said(What::Refeed, status, &body)?);
            return follow_refeed(&estate, asked, &mut |a: &Ask| door(&target, a), FIRST_LINK_EVERY, FIRST_LINK_FOR);
        }
    }
    if ask.what == What::Edit && status == 200 {
        let put = edit_body(&body, &words, &|p| std::fs::read_to_string(p).map_err(|e| refuse("site-secret-file", format!("{}: {e}", p.display()))))?;
        let (status, body) = door(&target, &put)?;
        return said(What::Put, status, &body);
    }
    said(ask.what, status, &body)
}

/// The request's key naming the file a `site do` order carries (replaced by its bytes,
/// `file_b64`, by [`do_file`]).
pub const DO_FILE: &str = "file";

/// **A `site do` request with its file's bytes in it** (`file_b64`), read by `read`; the
/// request's own keys (`wait`, the file param's name) are taken out.
pub fn do_file(mut ask: Ask, read: &dyn Fn(&Path) -> Result<Vec<u8>, String>) -> Result<Ask, String> {
    let mut v: serde_json::Value = serde_json::from_str(&ask.body).map_err(|e| e.to_string())?;
    if let Some(f) = v.get(DO_FILE).and_then(|f| f.as_str()).map(PathBuf::from) {
        let bytes = read(&f)?;
        v["file_b64"] = serde_json::json!(base64_of(&bytes));
    }
    if let Some(o) = v.as_object_mut() {
        o.remove(DO_FILE);
        o.remove("file_param");
        o.remove("wait");
    }
    ask.body = v.to_string();
    Ok(ask)
}

/// **A product order, as its box answered it**: the door only queued the signed order `seq`;
/// its answer is looked at through `ask_door` every `every` until the box answered, for at most
/// `for_` (looked at once at least). Not answered in time: an error naming how to look later.
pub fn follow_do(estate: &str, seq: u64, ask_door: &mut AskDoor<'_>, every: std::time::Duration, for_: std::time::Duration) -> Result<String, String> {
    let look = Ask { method: "GET", path: format!("/site/do/{estate}/{seq}"), body: String::new(), what: What::DoAnswer };
    let started = std::time::Instant::now();
    loop {
        let (status, body) = ask_door(&look)?;
        let v: serde_json::Value = serde_json::from_str(body.trim()).unwrap_or(serde_json::Value::Null);
        if status != 200 || v["answered"].as_bool().unwrap_or(false) {
            return said(What::DoAnswer, status, &body);
        }
        if started.elapsed() >= for_ {
            return Err(format!("{estate}'s box has not answered order {seq} within {} s: `site do-answer {estate} {seq}` says its answer once it pulls the order", for_.as_secs()));
        }
        std::thread::sleep(every);
    }
}

/// The request's key naming the file a `site console` call sends (replaced by its bytes,
/// `body_b64`, by [`console_body`]).
pub const CONSOLE_BODY_FILE: &str = "body_file";

/// **A `site console` request with its file's bytes in it** (`body_b64`), read by `read`.
pub fn console_body(mut ask: Ask, read: &dyn Fn(&Path) -> Result<Vec<u8>, String>) -> Result<Ask, String> {
    let mut v: serde_json::Value = serde_json::from_str(&ask.body).map_err(|e| e.to_string())?;
    if let Some(f) = v.get(CONSOLE_BODY_FILE).and_then(|f| f.as_str()).map(PathBuf::from) {
        let bytes = read(&f)?;
        v["body_b64"] = serde_json::json!(base64_of(&bytes));
        if let Some(o) = v.as_object_mut() {
            o.remove(CONSOLE_BODY_FILE);
        }
        ask.body = v.to_string();
    }
    Ok(ask)
}

/// Pure: standard base64 with padding.
pub fn base64_of(b: &[u8]) -> String {
    const A: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
    let mut out = String::with_capacity(b.len().div_ceil(3) * 4);
    for c in b.chunks(3) {
        let n = (u32::from(c[0]) << 16) | (u32::from(*c.get(1).unwrap_or(&0)) << 8) | u32::from(*c.get(2).unwrap_or(&0));
        out.push(A[(n >> 18) as usize & 63] as char);
        out.push(A[(n >> 12) as usize & 63] as char);
        out.push(if c.len() > 1 { A[(n >> 6) as usize & 63] as char } else { '=' });
        out.push(if c.len() > 2 { A[n as usize & 63] as char } else { '=' });
    }
    out
}

/// Pure: **the box's own Sites page, from its door** (`monetize.ignalina.dk:8475` →
/// `https://monetize.ignalina.dk/sites`); empty for a door named by an address or a bare name
/// (no page the payer's browser could open is guessed).
pub fn sites_page_of(door: &str) -> String {
    let host = door.trim().rsplit_once(':').map(|(h, _)| h).unwrap_or(door.trim()).trim_matches(['[', ']']);
    if host.is_empty() || !host.contains('.') || host.parse::<std::net::IpAddr>().is_ok() {
        return String::new();
    }
    format!("https://{host}/sites")
}

/// Pure: an ORDER that names no `--return` comes back to the box's Sites page
/// ([`sites_page_of`]); any other request is unchanged.
pub fn with_return(mut ask: Ask, door: &str) -> Ask {
    if ask.what != What::Order {
        return ask;
    }
    let Ok(mut body) = serde_json::from_str::<serde_json::Value>(&ask.body) else { return ask };
    if body["return_url"].as_str().unwrap_or("").trim().is_empty() {
        body["return_url"] = serde_json::Value::String(sites_page_of(door));
        ask.body = body.to_string();
    }
    ask
}

#[cfg(test)]
mod tests {
    /// ★ **`front certs`** (A7; RA1-3): the front's certificate door is the address the
    /// operator names (`--at https://<site>/<door>`), so the core holds no product's door;
    /// try now is that door's `/try`; the admin's credential from its file; refused by name
    /// when the door, the address or the credential is missing or wrong.
    #[test]
    fn front_certs_asks_the_door_the_operator_names_with_the_admins_credential() {
        let w = |a: &[&str]| a.iter().map(|s| s.to_string()).collect::<Vec<_>>();
        let files = |p: &std::path::Path| match p.to_str() {
            Some("/t") => Ok(" tok-1 \n".to_string()),
            Some("/c") => Ok("console_session=s1".to_string()),
            Some("/empty") => Ok("\n".to_string()),
            _ => Err("no such file".to_string()),
        };
        let h = front_request_of(&w(&["front", "certs", "--at", "https://a.example/front/certs", "--token-file", "/t"]), &files).unwrap();
        assert_eq!((h.host.as_str(), h.port, h.ask.method, h.ask.path.as_str(), h.ask.what), ("a.example", 443, "GET", "/front/certs", What::BoxCerts));
        assert_eq!(h.header, ("Authorization".to_string(), "Bearer tok-1".to_string()));
        let g = front_request_of(&w(&["front", "certs", "--at", "https://b.example:8443/api/front/certs/", "--cookie-file", "/c", "--try", "www.b.example", "--ca", "/ca.pem"]), &files).unwrap();
        assert_eq!((g.host.as_str(), g.port, g.ask.method, g.ask.path.as_str()), ("b.example", 8443, "POST", "/api/front/certs/try"));
        assert_eq!(g.ask.body, r#"{"host":"www.b.example"}"#);
        assert_eq!((g.header.0.as_str(), g.ca.as_deref()), ("Cookie", Some(std::path::Path::new("/ca.pem"))));
        let v6 = front_request_of(&w(&["front", "certs", "--at", "https://[::1]:9443/x/certs", "--token-file", "/t"]), &files).unwrap();
        assert_eq!((v6.host.as_str(), v6.port, v6.ask.path.as_str()), ("::1", 9443, "/x/certs"));
        for (a, why) in [
            (w(&["front", "certs"]), "[front-at]"),
            (w(&["front", "certs", "--token-file", "/t"]), "[front-at]"),
            (w(&["front", "certs", "--at", "http://x.example/front/certs", "--token-file", "/t"]), "[front-at]"),
            // The door is the site's own path: an address with none names no door.
            (w(&["front", "certs", "--at", "https://x.example", "--token-file", "/t"]), "[front-door]"),
            (w(&["front", "certs", "--at", "https://x.example/", "--token-file", "/t"]), "[front-door]"),
            (w(&["front", "certs", "--at", "https://x.example/c?x=1", "--token-file", "/t"]), "[front-door]"),
            // The old form named a product; the core knows no product's door.
            (w(&["front", "certs", "someproduct", "--at", "https://x.example/front/certs", "--token-file", "/t"]), "[front-door]"),
            (w(&["front", "certs", "--at", "https://x.example/front/certs"]), "[front-credential]"),
            (w(&["front", "certs", "--at", "https://x.example/front/certs", "--token-file", "/empty"]), "[front-credential]"),
            (w(&["front", "certs", "--at", "https://x.example/front/certs", "--token-file", "/t", "--cookie-file", "/c"]), "[front-credential]"),
        ] {
            let e = front_request_of(&a, &files).unwrap_err();
            assert!(e.contains(why), "{a:?}: {e}");
        }
    }

    /// ★ **One HTTPS client** (RA1-4): the box's door and a product's front go through the
    /// same request, the same TLS stream and the same answer parser; the runtime code dials
    /// one connection in one place.
    #[test]
    fn the_door_and_the_front_share_one_client() {
        let src = include_str!("lib.rs");
        let runtime = &src[..src.find("#[cfg(test)]").unwrap()];
        for needle in ["ClientConnection::new(", "TcpStream::connect_timeout(", "read_to_end(", "HTTP/1.1\\r\\n"] {
            assert_eq!(runtime.matches(needle).count(), 1, "{needle} is in more than one client");
        }
        assert_eq!(runtime.matches("http_answer(").count(), 2, "the one parser is defined once and called by the one client");
    }

    /// The one request's bytes: method and path, `Host:`, the extra headers, the JSON body
    /// and its length, `Connection: close`.
    #[test]
    fn one_request_is_http11_with_its_headers_and_a_close() {
        let ask = Ask { method: "POST", path: "/d/try".into(), body: r#"{"host":"h"}"#.into(), what: What::BoxUpdate };
        let t = request_text("a.example:8443", &[("Authorization".into(), "Bearer t".into())], &ask);
        assert_eq!(t, "POST /d/try HTTP/1.1\r\nHost: a.example:8443\r\nAuthorization: Bearer t\r\nContent-Type: application/json\r\nContent-Length: 12\r\nConnection: close\r\n\r\n{\"host\":\"h\"}");
        let get = Ask { method: "GET", path: "/self/certs".into(), body: String::new(), what: What::BoxCerts };
        assert!(request_text("vm", &[], &get).starts_with("GET /self/certs HTTP/1.1\r\nHost: vm\r\nContent-Type"));
    }

    /// An HTTP/1.1 answer, plain or chunked.
    #[test]
    fn an_http_answer_is_read_plain_or_chunked() {
        assert_eq!(http_answer(b"HTTP/1.1 200 OK\r\ncontent-length: 2\r\n\r\n{}").unwrap(), (200, "{}".to_string()));
        assert_eq!(http_answer(b"HTTP/1.1 401 X\r\nTransfer-Encoding: chunked\r\n\r\n4\r\nREFU\r\n3\r\nSED\r\n0\r\n\r\n").unwrap(), (401, "REFUSED".to_string()));
        assert!(http_answer(b"garbage").is_err());
    }

    /// ★ **`box certs [--try <host>]`** (owner 2026-10-25): the box's certificates as it says
    /// them, and an admin's "try now" — the box decides; the CLI only shows.
    #[test]
    fn box_certs_reads_the_certificates_and_try_names_the_host() {
        let no_file = |_: &Path| -> Result<String, String> { Err("no file".into()) };
        assert_eq!(request_of(&w(&["box", "certs"]), &no_file).unwrap(), Ask { method: "GET", path: "/self/certs".into(), body: String::new(), what: What::BoxCerts });
        let t = request_of(&w(&["box", "certs", "--try", "test2.example"]), &no_file).unwrap();
        assert_eq!((t.method, t.path.as_str(), t.what), ("POST", "/self/certs/try", What::BoxUpdate));
        assert_eq!(serde_json::from_str::<serde_json::Value>(&t.body).unwrap()["host"], "test2.example");
        let out = said(What::BoxCerts, 200, r#"{"certificates":[{"host":"test2.example","hosts":["test2.example","www.test2.example"],"lines":["valid until:  no valid certificate","last reason:  dns: x"]}]}"#).unwrap();
        assert_eq!(out, "test2.example (test2.example, www.test2.example)\n  valid until:  no valid certificate\n  last reason:  dns: x\n");
        assert!(said(What::BoxCerts, 200, r#"{"certificates":[]}"#).unwrap().contains("no certificate"));
    }

    use super::*;

    fn w(a: &[&str]) -> Vec<String> {
        a.iter().map(|s| s.to_string()).collect()
    }
    fn no_file(_: &Path) -> Result<String, String> {
        Err("no file".into())
    }

    /// ★ **`box plugins [--apply]`** (owner 2026-10-17): one POST that queues the job, then the
    /// job looked at until it is done or failed; one line per plugin in the box's words; a look
    /// without `--apply` says nothing was written; a failed job is a refusal by name.
    #[test]
    fn box_plugins_queues_the_job_follows_it_and_says_each_plugin() {
        assert_eq!(request_of(&w(&["box", "plugins"]), &no_file).unwrap(), Ask { method: "POST", path: "/box/plugins".into(), body: r#"{"apply":false}"#.into(), what: What::Plugins });
        assert_eq!(request_of(&w(&["box", "plugins", "--apply"]), &no_file).unwrap().body, r#"{"apply":true}"#);
        let queued = r#"{"job":"plugins-4","state":"queued","apply":true,"rows":[],"error":""}"#;
        assert_eq!(plugins_next(queued), Some(Ask { method: "GET", path: "/box/plugins/plugins-4".into(), body: String::new(), what: What::Plugins }));
        assert!(plugins_next(&queued.replace("queued", "running")).is_some());
        let done = r#"{"job":"plugins-4","state":"done","apply":true,"error":"","rows":[{"name":"stripe.wasm","role":"vendor","outcome":"replaced","detail":"[vendors.stripe.gunnar]","blake3":"eb45"},{"name":"upcloud.wasm","role":"cloud","outcome":"refused","detail":"[plugin-unsigned] did not verify","blake3":"3d6c"}]}"#;
        assert_eq!(plugins_next(done), None);
        let out = said(What::Plugins, 200, done).unwrap();
        assert!(out.contains("stripe.wasm (vendor): replaced — [vendors.stripe.gunnar]\n") && out.contains("upcloud.wasm (cloud): refused — [plugin-unsigned] did not verify\n"), "{out}");
        assert!(!out.contains("--apply"), "{out}");
        let look = said(What::Plugins, 200, &done.replace(r#""apply":true"#, r#""apply":false"#).replace("replaced", "would-replace")).unwrap();
        assert!(look.contains("stripe.wasm (vendor): would-replace") && look.contains("nothing was written") && look.contains("--apply"), "{look}");
        let failed = r#"{"job":"plugins-5","state":"failed","apply":true,"rows":[],"error":"[plugins-list-unsigned] the list did not verify"}"#;
        assert_eq!(plugins_next(failed), None);
        assert!(said(What::Plugins, 200, failed).unwrap_err().contains("[plugins-list-unsigned]"));
    }

    /// ★ **`site first-link <site> --to <address>`** (owner 2026-10-22): ONE request at the
    /// door, the address in the body; no `--to` is refused by name; the help names it.
    #[test]
    fn first_link_is_one_request_with_the_new_address() {
        let a = request_of(&w(&["site", "first-link", "holger-rs", "--to", "owner@example.invalid"]), &no_file).unwrap();
        assert_eq!((a.method, a.path.as_str(), a.what), ("POST", "/site/first-link", What::FirstLink));
        assert_eq!(serde_json::from_str::<serde_json::Value>(&a.body).unwrap(), serde_json::json!({ "estate": "holger-rs", "to": "owner@example.invalid" }));
        // FIRST-LINK-1: no --to is the first admin's recorded address (a handed-off site's main
        // mints the link).
        let a = request_of(&w(&["site", "first-link", "holger-rs"]), &no_file).unwrap();
        assert_eq!(serde_json::from_str::<serde_json::Value>(&a.body).unwrap(), serde_json::json!({ "estate": "holger-rs", "to": "" }));
        assert!(help().contains("site first-link <site> [--to <address>]"), "{}", help());
        assert!(request_of(&w(&["site", "first-link"]), &no_file).is_err(), "the site is named");

        assert_eq!(said(What::FirstLink, 200, r#"{"words":"holger-rs's first sign-in link is mailed to o***@example.invalid again"}"#).unwrap(), "holger-rs's first sign-in link is mailed to o***@example.invalid again\n");
    }

    /// ★ **A handed-off site's `site first-link` says its MAIN's answer, never a promise**
    /// (HOL-MAIL, holger-rs 2026-10-27: the CLI said "a new sign-in link is on its way" while the
    /// site's main refused, fed no relay). The door names the order it queued (`asked`); the CLI
    /// looks at the site's status until the main answered THAT order: refused by name with the
    /// main's reason (an error), minted and mailed, or not answered in time (an error that says
    /// nothing is known to be mailed). An older order's answer is not this one's.
    #[test]
    fn a_handed_off_sites_first_link_says_the_mains_answer_never_a_promise() {
        let status = |answered: u64, mailed: bool, words: &str| serde_json::json!({ "company": "holgerrs", "first_admin_door": "handed_off", "first_admin_door_words": words, "first_link_answered": answered, "first_link_mailed": mailed }).to_string();
        assert_eq!(first_link_asked(r#"{"words":"holger-rs is handed off: nothing is mailed until its main answers","asked":7}"#), 7);
        assert_eq!(first_link_asked(r#"{"words":"holger-rs's first sign-in link is mailed to o***@example.invalid again"}"#), 0, "no main asked: the words are the answer");
        let refused = "holger-rs is handed off; its main refused the last ask: this box cannot mail (mail is off: this box was fed no relay (secret.smtp_url), so it dials nothing), so it mints no sign-in link for ignalina it could not mail";
        let mut looks = vec![status(0, false, "holger-rs is handed off: a new first sign-in link was asked; its main has not pulled the order yet"), status(7, false, refused)].into_iter();
        let mut seen = Vec::new();
        let e = follow_first_link("holger-rs", 7, &mut |a: &Ask| {
            seen.push((a.method, a.path.clone()));
            Ok((200, looks.next().expect("two looks")))
        }, std::time::Duration::ZERO, std::time::Duration::from_secs(60))
        .unwrap_err();
        assert_eq!(seen, vec![("GET", "/site/status/holger-rs".to_string()); 2], "the site's status, until the main answered");
        assert!(e.starts_with("REFUSED by holger-rs's main") && e.contains("fed no relay (secret.smtp_url)") && !e.contains("on its way"), "{e}");

        let mailed = "holger-rs is handed off; its main answered the last ask: a new sign-in link for ignalina (24 hours) was mailed to f***@mock.invalid";
        let ok = follow_first_link("holger-rs", 8, &mut |_: &Ask| Ok((200, status(8, true, mailed))), std::time::Duration::ZERO, std::time::Duration::from_secs(60)).unwrap();
        assert!(ok.contains("holger-rs's main minted a new sign-in link and mailed it") && ok.contains("f***@mock.invalid"), "{ok}");

        // Only an older order is answered, and the time is up: nothing is known to be mailed.
        let e = follow_first_link("holger-rs", 9, &mut |_: &Ask| Ok((200, status(8, true, mailed))), std::time::Duration::ZERO, std::time::Duration::ZERO).unwrap_err();
        assert!(e.contains("holger-rs's main has not answered") && e.contains("nothing is known to be mailed") && e.contains("site status holger-rs"), "{e}");
        // A refused look is said as the box said it.
        let e = follow_first_link("holger-rs", 9, &mut |_: &Ask| Ok((503, r#"{"words":"monetize is not up","refused":"site-unavailable"}"#.into())), std::time::Duration::ZERO, std::time::Duration::from_secs(60)).unwrap_err();
        assert!(e.contains("REFUSED by the box (HTTP 503)"), "{e}");
    }

    /// ★ **`site console <site> <METHOD> <path>`** (OX-WGDS3 2026-10-28): ONE request at the
    /// door naming the site, the method, the path and query, the host (`--repo`) and the content
    /// type; a body file's bytes ride as base64; the answer says the front's status, then its body.
    #[test]
    fn site_console_is_one_request_and_says_the_fronts_answer() {
        let a = request_of(&w(&["site", "console", "test3-holger-rs", "post", "/-/sync/downstreams/enrol?name=lab-1&key=ab&via=front"]), &no_file).unwrap();
        assert_eq!((a.method, a.path.as_str(), a.what), ("POST", "/site/console", What::Console));
        assert_eq!(serde_json::from_str::<serde_json::Value>(&a.body).unwrap(), serde_json::json!({ "estate": "test3-holger-rs", "method": "POST", "path": "/-/sync/downstreams/enrol?name=lab-1&key=ab&via=front", "repo": false }));
        let a = request_of(&w(&["site", "console", "test3-holger-rs", "PUT", "/bundles/bundle-1.znippy", "--repo", "--body-file", "/tmp/b1", "--content-type", "application/octet-stream"]), &no_file).unwrap();
        let a = console_body(a, &|p| {
            assert_eq!(p, Path::new("/tmp/b1"));
            Ok(vec![0u8, 1, 2, 255, b'h', b'i', b'!'])
        })
        .unwrap();
        assert_eq!(serde_json::from_str::<serde_json::Value>(&a.body).unwrap(), serde_json::json!({ "estate": "test3-holger-rs", "method": "PUT", "path": "/bundles/bundle-1.znippy", "repo": true, "content_type": "application/octet-stream", "body_b64": "AAEC/2hpIQ==" }));
        for bad in [&["site", "console", "x"][..], &["site", "console", "x", "GET"], &["site", "console", "x", "GET", "nopath"]] {
            assert!(request_of(&w(bad), &no_file).is_err(), "{bad:?}");
        }
        assert!(help().contains("site console <site> <METHOD> <path>"), "{}", help());
        assert_eq!(said(What::Console, 200, r#"{"status":403,"body":"REFUSED [downstream-door] no"}"#).unwrap(), "HTTP 403\nREFUSED [downstream-door] no\n");
        assert_eq!(said(What::Console, 200, r#"{"status":200,"body":"{\"name\":\"cloud\"}\n"}"#).unwrap(), "HTTP 200\n{\"name\":\"cloud\"}\n");
        let refused = said(What::Console, 409, r#"{"words":"[site-console-not-allowed] DELETE /x is not a console call","refused":"FailedPrecondition"}"#).unwrap_err();
        assert!(refused.contains("[site-console-not-allowed]"), "{refused}");
        assert_eq!(base64_of(b""), "");
        assert_eq!(base64_of(b"f"), "Zg==");
        assert_eq!(base64_of(b"fo"), "Zm8=");
        assert_eq!(base64_of(b"foo"), "Zm9v");
    }

    /// ★ **`site do <site> <kind> …`** (MONETIZE-FIX 2026-11-01): ONE request naming the kind and
    /// its params; the file is read where the request is sent; the box's answer is followed until
    /// it pulled the order, and a refusal is said as one.
    #[test]
    fn site_do_queues_one_request_and_says_the_boxs_answer() {
        let a = request_of(&w(&["site", "do", "test3-holger-rs", "downstream_enrol", "name=lab-1", "via=front", "--file", "bundle=/tmp/b"]), &no_file).unwrap();
        assert_eq!((a.method, a.path.as_str(), a.what), ("POST", "/site/do", What::Do));
        let a = do_file(a, &|p| { assert_eq!(p, Path::new("/tmp/b")); Ok(b"hi".to_vec()) }).unwrap();
        let v: serde_json::Value = serde_json::from_str(&a.body).unwrap();
        assert_eq!(v, serde_json::json!({ "estate": "test3-holger-rs", "kind": "downstream_enrol", "params": { "name": "lab-1", "via": "front" }, "file_b64": "aGk=" }));
        assert!(request_of(&w(&["site", "do", "x"]), &no_file).is_err(), "the kind is named");
        assert!(request_of(&w(&["site", "do", "x", "k", "novalue"]), &no_file).is_err());
        assert_eq!(request_of(&w(&["site", "kinds", "x"]), &no_file).unwrap().path, "/site/kinds/x");
        assert_eq!(request_of(&w(&["site", "do-answer", "x", "7"]), &no_file).unwrap().path, "/site/do/x/7");
        assert!(help().contains("site do <site> <kind>"), "{}", help());
        let mut looks = vec![r#"{"kind":"downstream_enrol","answered":false}"#, r#"{"kind":"downstream_enrol","answered":true,"ok":true,"detail":"{\"name\":\"lab-1\"}"}"#].into_iter();
        let out = follow_do("x", 7, &mut |_: &Ask| Ok((200, looks.next().unwrap().to_string())), std::time::Duration::ZERO, std::time::Duration::from_secs(60)).unwrap();
        assert!(out.contains("lab-1"), "{out}");
        let e = follow_do("x", 7, &mut |_: &Ask| Ok((200, r#"{"kind":"k","answered":true,"ok":false,"detail":"no such downstream"}"#.into())), std::time::Duration::ZERO, std::time::Duration::ZERO).unwrap_err();
        assert!(e.contains("no such downstream"), "{e}");
        let e = follow_do("x", 7, &mut |_: &Ask| Ok((200, r#"{"kind":"k","answered":false}"#.into())), std::time::Duration::ZERO, std::time::Duration::ZERO).unwrap_err();
        assert!(e.contains("do-answer x 7"), "{e}");
    }

    /// ★ **`site change-plan <site>`** (MONETIZE-FIX 2026-11-01): ONE request at the door naming
    /// the site and what changes; nothing named is refused before the door is asked.
    #[test]
    fn site_change_plan_is_one_request_naming_what_changes() {
        let a = request_of(&w(&["site", "change-plan", "test2-gunnar-rs", "--data-gib", "60", "--test"]), &no_file).unwrap();
        assert_eq!((a.method, a.path.as_str(), a.what), ("POST", "/site/change-plan", What::ChangePlan));
        let v: serde_json::Value = serde_json::from_str(&a.body).unwrap();
        assert_eq!((v["estate"].as_str(), v["data_gib"].as_u64(), v["test"].as_bool()), (Some("test2-gunnar-rs"), Some(60), Some(true)));
        let a = request_of(&w(&["site", "change-plan", "lake", "--set", "be=3", "--set", "graph=true"]), &no_file).unwrap();
        let v: serde_json::Value = serde_json::from_str(&a.body).unwrap();
        assert_eq!(v["order"], serde_json::json!({ "be": 3, "graph": true }));
        assert!(request_of(&w(&["site", "change-plan", "lake"]), &no_file).unwrap_err().contains("name what changes"));
        assert!(request_of(&w(&["site", "change-plan", "lake", "--data-gib", "x"]), &no_file).is_err());
        assert!(request_of(&w(&["site", "change-plan"]), &no_file).is_err(), "the site is named");
        assert!(help().contains("site change-plan <site>"), "{}", help());
        let held = r#"{"words":"main's data disks grow to 60 GB","reference":"estate/x/change-1","status":"waiting_for_payment","checkout_url":"https://pay.invalid/1","price_words":"1.00 EUR"}"#;
        let out = said(What::ChangePlan, 200, held).unwrap();
        assert!(out.contains("change-1") && out.contains("https://pay.invalid/1"), "{out}");
    }

    /// ★ **`site refeed <site>`** (MAIL-RELAY + NIGHT-PUBREPO 2026-10-28): ONE request at the
    /// door naming the site and the groups asked (none: all), no value; the help names it; the
    /// door's answer is followed until the site's MAIN answered the order it queued: kept,
    /// refused by name with its reason, or not answered in time (nothing is known to be kept).
    #[test]
    fn site_refeed_is_one_request_and_says_the_mains_answer() {
        let a = request_of(&w(&["site", "refeed", "holger-rs"]), &no_file).unwrap();
        assert_eq!((a.method, a.path.as_str(), a.what), ("POST", "/site/refeed", What::Refeed));
        assert_eq!(serde_json::from_str::<serde_json::Value>(&a.body).unwrap(), serde_json::json!({ "estate": "holger-rs", "only": [] }));
        let a = request_of(&w(&["site", "refeed", "holger-rs", "--only", "mail_relay", "--only", "public_repositories"]), &no_file).unwrap();
        assert_eq!(serde_json::from_str::<serde_json::Value>(&a.body).unwrap(), serde_json::json!({ "estate": "holger-rs", "only": ["mail_relay", "public_repositories"] }));
        assert!(help().contains("site refeed <site>"), "{}", help());
        assert!(request_of(&w(&["site", "refeed"]), &no_file).is_err(), "the site is named");
        let fed = r#"{"words":"the mail relay relay.mock.invalid:465 (implicit TLS) as es***@mock.invalid fed again to holger-rs's main and twin: signed orders main 7, twin 8 (refeed)","asked":7}"#;
        assert_eq!(first_link_asked(fed), 7);
        assert!(said(What::Refeed, 200, fed).unwrap().contains("main 7"));
        let refused = said(What::Refeed, 412, r#"{"words":"[site-refeed-none] this ONE box holds nothing to feed holger-rs again","refused":"site-refused"}"#).unwrap_err();
        assert!(refused.contains("[site-refeed-none]"), "{refused}");

        let status = |answered: u64, kept: bool, words: &str| serde_json::json!({ "company": "holgerrs", "refeed_answered": answered, "refeed_kept": kept, "refeed_words": words }).to_string();
        let mut looks = vec![status(0, false, "holger-rs's main was fed its keys again (order 7); it has not pulled the order yet"), status(7, false, "holger-rs's main refused its keys (order 7): order 7: Holger does not know the order kind \"refeed\"")].into_iter();
        let e = follow_refeed("holger-rs", 7, &mut |a: &Ask| {
            assert_eq!((a.method, a.path.as_str()), ("GET", "/site/status/holger-rs"));
            Ok((200, looks.next().expect("two looks")))
        }, std::time::Duration::ZERO, std::time::Duration::from_secs(60))
        .unwrap_err();
        assert!(e.starts_with("REFUSED by holger-rs's main") && e.contains("does not know"), "{e}");
        let kept = "holger-rs's main kept its keys (order 8): kept public_repositories, secret.smtp_from, secret.smtp_url";
        let ok = follow_refeed("holger-rs", 8, &mut |_: &Ask| Ok((200, status(8, true, kept))), std::time::Duration::ZERO, std::time::Duration::from_secs(60)).unwrap();
        assert!(ok.contains("holger-rs's main kept its keys") && ok.contains("public_repositories"), "{ok}");
        let e = follow_refeed("holger-rs", 9, &mut |_: &Ask| Ok((200, status(8, true, kept))), std::time::Duration::ZERO, std::time::Duration::ZERO).unwrap_err();
        assert!(e.contains("holger-rs's main has not answered") && e.contains("nothing is known to be kept") && e.contains("site status holger-rs"), "{e}");
    }

    /// ★ **`site test-order <site>`** (owner: "monetize must be automatic"): ONE request at the
    /// door's own test verb — the order's words, never a return page (nobody pays at one); no
    /// `--admin` is refused by name; the answer says it was paid with the test card.
    #[test]
    fn site_test_order_is_one_request_at_the_doors_test_verb() {
        let a = request_of(&w(&["site", "test-order", "test2-gunnar-rs", "--size", "mini", "--admin", "ops@example.invalid"]), &no_file).unwrap();
        assert_eq!((a.method, a.path.as_str(), a.what), ("POST", "/site/test-order", What::TestOrder));
        assert_eq!(serde_json::from_str::<serde_json::Value>(&a.body).unwrap(), serde_json::json!({ "estate": "test2-gunnar-rs", "size": "mini", "email": "ops@example.invalid", "seed_key": "" }));
        assert!(request_of(&w(&["site", "test-order", "x"]), &no_file).unwrap_err().contains("[site-admin]"));
        assert!(request_of(&w(&["site", "test-order", "--admin", "a@b.invalid"]), &no_file).unwrap_err().contains("[site]"));
        assert!(request_of(&w(&["site", "test-order", "x", "--admin", "a@b.invalid", "--return", "https://x.example/"]), &no_file).unwrap_err().contains("[site-test-order-return]"), "no page, so no way back");
        assert!(help().contains("site test-order <site> [--size <step>] --admin <address>"), "{}", help());
        let out = said(What::TestOrder, 200, r#"{"reference":"estate/test2gunnarrs","company":"test2gunnarrs","checkout_url":"","price_words":"0.50 EUR","words":"test2-gunnar-rs is ordered and paid with the vendor's TEST card (pm_card_visa): this box lays it"}"#).unwrap();
        assert_eq!(out, "test2-gunnar-rs is ordered and paid with the vendor's TEST card (pm_card_visa): this box lays it\nreference: estate/test2gunnarrs\n0.50 EUR held with the test card: nobody pays at a page\n");
        let live = said(What::TestOrder, 409, r#"{"refused":"FailedPrecondition","words":"REFUSED [test-order-live-key] test2-gunnar-rs's vendor holds a LIVE key"}"#).unwrap_err();
        assert!(live.contains("[test-order-live-key]"), "{live}");
    }

    /// ★ Each command is ONE request at the box's door, and names nothing the box already
    /// knows of a declared site: no domain, no Stripe key, no DNS token, no cloud.
    #[test]
    fn each_command_is_one_request_at_the_door() {
        let a = request_of(&w(&["site", "list"]), &no_file).unwrap();
        assert_eq!((a.method, a.path.as_str(), a.body.as_str()), ("GET", "/site/list", ""));
        let a = request_of(&w(&["site", "order", "test3-gunnar-rs", "--size", "mini", "--admin", "ops@example.invalid"]), &no_file).unwrap();
        assert_eq!((a.method, a.path.as_str()), ("POST", "/site/order"));
        let body: serde_json::Value = serde_json::from_str(&a.body).unwrap();
        assert_eq!(body, serde_json::json!({ "estate": "test3-gunnar-rs", "size": "mini", "email": "ops@example.invalid", "seed_key": "", "return_url": "" }));
        assert_eq!(request_of(&w(&["site", "status", "gunnar-rs"]), &no_file).unwrap().path, "/site/status/gunnar-rs");
        assert_eq!(request_of(&w(&["site", "cancel", "gunnar-rs"]), &no_file).unwrap().body, r#"{"estate":"gunnar-rs"}"#);
        let t = request_of(&w(&["site", "terminate", "test3-gunnar-rs", "--confirm", "test3-gunnar-rs"]), &no_file).unwrap();
        assert_eq!(serde_json::from_str::<serde_json::Value>(&t.body).unwrap(), serde_json::json!({ "estate": "test3-gunnar-rs", "confirm": "test3-gunnar-rs" }));
        // No backup at terminate (owner 2026-10-22): the flag is refused by name, never ignored.
        let b = request_of(&w(&["site", "terminate", "test3-gunnar-rs", "--confirm", "test3-gunnar-rs", "--backup", "take"]), &no_file).unwrap_err();
        assert!(b.contains("[terminate-no-backup]"), "{b}");
        assert!(!help().contains("--backup"), "the help offers no backup");
        assert!(request_of(&w(&["site", "order", "x"]), &no_file).unwrap_err().contains("[site-admin]"));
        assert!(request_of(&w(&["site", "terminate", "x"]), &no_file).unwrap_err().contains("[site-confirm]"), "the name is typed back");
        assert!(request_of(&w(&["site", "order", "--size", "mini"]), &no_file).unwrap_err().contains("[site]"));
        assert!(request_of(&w(&["shop"]), &no_file).unwrap_err().contains("[usage]"));
    }

    /// ★ **`monetize site narrate <site> [vm] [--tail N]`** (2026-10-20): ONE read at the door —
    /// each box's summary, or one box's boot log printed as the box sent it.
    #[test]
    fn site_narrate_is_one_read_at_the_door() {
        let a = request_of(&w(&["site", "narrate", "test3-holger-rs"]), &no_file).unwrap();
        assert_eq!(a, Ask { method: "GET", path: "/site/narrate/test3-holger-rs".into(), body: String::new(), what: What::Narrate });
        let a = request_of(&w(&["site", "narrate", "test3-holger-rs", "main"]), &no_file).unwrap();
        assert_eq!((a.method, a.path.as_str(), a.what), ("GET", "/site/narrate/test3-holger-rs/main", What::NarrateText));
        let a = request_of(&w(&["site", "narrate", "test3-holger-rs", "main", "--tail", "50"]), &no_file).unwrap();
        assert_eq!(a.path, "/site/narrate/test3-holger-rs/main?tail=50");
        let a = request_of(&w(&["site", "narrate", "test3-holger-rs", "--tail", "50", "main"]), &no_file).unwrap();
        assert_eq!(a.path, "/site/narrate/test3-holger-rs/main?tail=50", "the flag anywhere");
        assert!(request_of(&w(&["site", "narrate", "test3-holger-rs", "main", "--tail", "x"]), &no_file).unwrap_err().contains("[site-narrate-tail]"));
        assert!(request_of(&w(&["site", "narrate", "test3-holger-rs", "--tail", "5"]), &no_file).unwrap_err().contains("[site-narrate-tail]"), "--tail reads one box");
        assert!(request_of(&w(&["site", "narrate"]), &no_file).unwrap_err().contains("[site]"));
        let out = said(What::Narrate, 200, r#"{"estate":"test3-holger-rs","narrated":true,"words":"heard here","vms":[{"vm":"main","lines":2,"bytes":8,"updated":"2026-10-20 16:10:00 UTC","last":"two"},{"vm":"front","lines":0,"bytes":0,"updated":"","last":""}]}"#).unwrap();
        assert!(out.starts_with("test3-holger-rs — narrated\nheard here\n"), "{out}");
        assert!(out.contains("  main ") && out.contains("2 lines") && out.contains("2026-10-20 16:10:00 UTC") && out.contains("two"), "{out}");
        assert!(out.contains("  front") && out.contains("nothing heard yet"), "{out}");
        assert!(out.contains("monetize site narrate test3-holger-rs <vm>"), "{out}");
        assert_eq!(said(What::NarrateText, 200, "a\nb\n").unwrap(), "a\nb\n", "the log as the box sent it");
        assert!(said(What::NarrateText, 404, r#"{"refused":"NotFound","words":"test3 has no machine \"x\""}"#).unwrap_err().contains("no machine"));
    }

    /// ★ **`monetize site installer-log <site> <vm> [--since N]`** (2026-10-20): ONE read at the
    /// door — the box's install log as the ONE box read it over the installer's link.
    #[test]
    fn site_installer_log_is_one_read_at_the_door() {
        let a = request_of(&w(&["site", "installer-log", "test3-holger-rs", "front"]), &no_file).unwrap();
        assert_eq!(a, Ask { method: "GET", path: "/site/installer-log/test3-holger-rs/front".into(), body: String::new(), what: What::InstallerLog });
        let a = request_of(&w(&["site", "installer-log", "test3-holger-rs", "--since", "40", "front"]), &no_file).unwrap();
        assert_eq!(a.path, "/site/installer-log/test3-holger-rs/front?since=40");
        assert!(request_of(&w(&["site", "installer-log", "test3-holger-rs"]), &no_file).unwrap_err().contains("[site-installer-log]"), "the box is named");
        assert!(request_of(&w(&["site", "installer-log", "test3-holger-rs", "front", "--since", "x"]), &no_file).unwrap_err().contains("[site-installer-log]"));
        assert!(request_of(&w(&["site", "installer-log"]), &no_file).unwrap_err().contains("[site]"));
        assert_eq!(said(What::InstallerLog, 200, "korp-start: network up\n").unwrap(), "korp-start: network up\n");
        assert!(said(What::InstallerLog, 404, r#"{"refused":"NotFound","words":"no install log was kept"}"#).unwrap_err().contains("no install log"));
    }

    /// ★ **`monetize site recheck|retry|abandon <site>`** (F5, 2026-10-20; abandon 2026-10-21): ONE request at the door each —
    /// the Sites page's "Check again" and "Set up again", the box's answer printed as it came.
    #[test]
    fn site_recheck_retry_and_abandon_are_one_request_at_the_door() {
        for action in ["recheck", "retry", "abandon"] {
            let a = request_of(&w(&["site", action, "test3-holger-rs"]), &no_file).unwrap();
            assert_eq!((a.method, a.path.as_str(), a.what), ("POST", "/site/settle", What::Settle));
            assert_eq!(serde_json::from_str::<serde_json::Value>(&a.body).unwrap(), serde_json::json!({ "estate": "test3-holger-rs", "action": action }));
            assert!(request_of(&w(&["site", action]), &no_file).unwrap_err().contains("[site]"));
        }
        assert_eq!(said(What::Settle, 200, r#"{"phase":"ready","words":"adopted: every box PAIRED on re-check"}"#).unwrap(), "adopted: every box PAIRED on re-check\nphase: ready\n");
        assert!(said(What::Settle, 409, r#"{"refused":"FailedPrecondition","words":"not recheckable"}"#).unwrap_err().contains("not recheckable"));
    }

    /// ★ **`monetize site open-pin <site>`** (F6, 2026-10-20): ONE request at the door — the
    /// guardian's pin windows opened again for the site's boxes not pinned yet.
    #[test]
    fn site_open_pin_is_one_request_at_the_door() {
        let a = request_of(&w(&["site", "open-pin", "test3-holger-rs"]), &no_file).unwrap();
        assert_eq!((a.method, a.path.as_str(), a.what, a.body.as_str()), ("POST", "/site/open-pin/test3-holger-rs", What::Settle, ""));
        assert!(request_of(&w(&["site", "open-pin"]), &no_file).unwrap_err().contains("[site]"));
        assert!(help().contains("site open-pin <site>"), "the help names it");
        assert_eq!(said(What::Settle, 200, r#"{"phase":"paired","words":"pin windows opened"}"#).unwrap(), "pin windows opened\nphase: paired\n");
    }

    /// ★ **`monetize estate update <estate>`** (owner 2026-10-14): one request at the door each —
    /// open the update, read it, switch live updates; the box's answer printed as it came.
    #[test]
    fn estate_update_is_one_request_at_the_door_and_its_answer_is_the_boxs() {
        let a = request_of(&w(&["estate", "update", "test2-gunnar-rs", "--release", "02abc"]), &no_file).unwrap();
        assert_eq!((a.method, a.path.as_str(), a.what), ("POST", "/estate/update", What::Update));
        assert_eq!(serde_json::from_str::<serde_json::Value>(&a.body).unwrap(), serde_json::json!({ "estate": "test2-gunnar-rs", "release": "02abc", "front_first": false }));
        let f = request_of(&w(&["estate", "update", "test2-gunnar-rs", "--front-first"]), &no_file).unwrap();
        assert_eq!(serde_json::from_str::<serde_json::Value>(&f.body).unwrap()["front_first"], serde_json::json!(true), "the front first, said");
        assert_eq!(request_of(&w(&["estate", "update-status", "acme"]), &no_file).unwrap().path, "/estate/update/acme");
        let l = request_of(&w(&["estate", "live-update", "acme", "off"]), &no_file).unwrap();
        assert_eq!(serde_json::from_str::<serde_json::Value>(&l.body).unwrap(), serde_json::json!({ "estate": "acme", "on": false }));
        assert!(request_of(&w(&["estate", "live-update", "acme"]), &no_file).unwrap_err().contains("[estate-live]"));
        assert!(request_of(&w(&["estate", "update"]), &no_file).unwrap_err().contains("[estate]"));
        // ★ A32-7: the cancel verb is a plan unless --apply.
        assert_eq!(request_of(&w(&["estate", "update-cancel", "acme"]), &no_file).unwrap(), Ask { method: "GET", path: "/estate/update/acme".into(), body: String::new(), what: What::UpdateCancelPlan });
        let c = request_of(&w(&["estate", "update-cancel", "acme", "--apply"]), &no_file).unwrap();
        assert_eq!((c.method, c.path.as_str(), c.what), ("POST", "/estate/update/cancel", What::Update));
        assert_eq!(serde_json::from_str::<serde_json::Value>(&c.body).unwrap(), serde_json::json!({ "estate": "acme" }));
        let out = said(What::Update, 200, r#"{"host":"test2.gunnar.rs","release":"02abc","release_of":{"twin":"02abc","main":"01"},"live_on":true,"state":"running","steps":[{"role":"twin","state":"done","how":"live","words":""},{"role":"main","state":"waiting","how":"","words":""}],"words":"update to 02abc"}"#).unwrap();
        assert!(out.contains("twin  runs 02abc") && out.contains("step twin  done (live)") && out.contains("front runs (unknown)"), "{out}");
        assert!(said(What::Update, 409, "no").is_err());
        // ★ A32-6 (the kit rule): `box update` without --apply is a PLAN — it asks what the box
        // runs and what an update would do, and moves nothing; --apply restarts the box.
        assert_eq!(request_of(&w(&["box", "update"]), &no_file).unwrap(), Ask { method: "GET", path: "/self/running".into(), body: String::new(), what: What::BoxPlan });
        assert_eq!(request_of(&w(&["box", "update", "--back"]), &no_file).unwrap(), Ask { method: "GET", path: "/self/running".into(), body: String::new(), what: What::BoxPlan });
        assert_eq!(request_of(&w(&["box", "update", "--apply"]), &no_file).unwrap(), Ask { method: "POST", path: "/self/update".into(), body: String::new(), what: What::BoxUpdate });
        // M-FOLLOW 3: the way back, the other slot's release that confirmed on the box.
        assert_eq!(request_of(&w(&["box", "update", "--back", "--apply"]), &no_file).unwrap(), Ask { method: "POST", path: "/self/update".into(), body: r#"{"back":true}"#.into(), what: What::BoxUpdate });
        let plan = said(What::BoxPlan, 200, r#"{"app":"aa","slot":"b","base":"bb","live_update":true,"next_app":"cc","next_build":"6260c99","next_base":"bb","plan":"Live","back":"app dd in the other slot"}"#).unwrap();
        assert!(plan.contains("update   Live") && plan.contains("PLAN only") && plan.contains("--apply"), "the plan says nothing moved and how to act: {plan}");
        assert_eq!(request_of(&w(&["box", "running"]), &no_file).unwrap().path, "/self/running");
        assert!(said(What::BoxRunning, 200, r#"{"app":"aa","slot":"b","base":"bb","live_update":true,"next_app":"cc","next_build":"6260c99","next_base":"bb","plan":"Live"}"#).unwrap().contains("update   Live"));
    }

    /// ★ **`estate adopt`** (owner 2026-10-17): ONE request at the door with the three servers
    /// (over what the kit's `--machines` file records), the secret answer written to a NEW 0600
    /// file and never said on the terminal.
    #[test]
    fn estate_adopt_is_one_request_and_its_secret_goes_to_a_file_never_the_terminal() {
        let files = |p: &Path| match p.to_str() {
            Some("m.json") => Ok(r#"[{"role":"front","server":"old-f","public_ipv4":"198.51.100.9","utility_ipv4":"10.0.0.9","plan":"1xCPU-2GB","disks":[{"id":"d-f","gib":25,"address":"virtio:0"}]}]"#.to_string()),
            _ => Err("no file".to_string()),
        };
        let a = request_of(&w(&["estate", "adopt", "gunnar-rs", "--machines", "m.json", "--front", "f-1", "--main", "m-1", "--twin", "t-1", "--release-of", "main=02abc", "--feed-out", "feed.json"]), &files).unwrap();
        assert_eq!((a.method, a.path.as_str(), a.what), ("POST", "/estate/adopt", What::Adopt));
        let b: serde_json::Value = serde_json::from_str(&a.body).unwrap();
        assert_eq!(b["estate"], "gunnar-rs");
        let ms = b["machines"].as_array().unwrap();
        assert_eq!(ms.iter().map(|m| (m["role"].as_str().unwrap(), m["server"].as_str().unwrap())).collect::<Vec<_>>(), vec![("front", "f-1"), ("main", "m-1"), ("twin", "t-1")], "the flags name the servers over the file");
        assert_eq!((ms[0]["public_ipv4"].as_str(), ms[0]["disks"][0]["gib"].as_u64()), (Some("198.51.100.9"), Some(25)), "the kit's record rides along");
        assert_eq!((b["release_of"]["main"].as_str(), b["again"].as_bool()), (Some("02abc"), Some(false)));
        assert!(request_of(&w(&["estate", "adopt", "gunnar-rs", "--front", "f", "--main", "m", "--twin", "t"]), &no_file).unwrap_err().contains("[adopt-feed-out]"), "the secret needs a file");
        assert!(request_of(&w(&["estate", "adopt", "gunnar-rs", "--front", "f", "--main", "m", "--feed-out", "x"]), &no_file).unwrap_err().contains("--twin"));
        let answer = r#"{"company":"gunnarrs","reference":"estate/gunnarrs","host":"gunnar.rs","client_key":"ab","feed":{"shop_door.toml":"door = \"https://monetize.ignalina.dk\"\n","secret.shop_client_key":"SEEDHEX"},"words":"gunnar-rs is adopted"}"#;
        let out = said(What::Adopt, 200, answer).unwrap();
        assert!(out.contains("gunnar-rs is adopted") && out.contains("secret.shop_client_key, shop_door.toml") && !out.contains("SEEDHEX"), "{out}");
        let feed: serde_json::Value = serde_json::from_str(&adopt_feed(answer).unwrap()).unwrap();
        assert_eq!(feed["secret.shop_client_key"], "SEEDHEX");
        let dir = std::env::temp_dir().join(format!("monetize-cli-adopt-{}", std::process::id()));
        let _ = std::fs::remove_dir_all(&dir);
        std::fs::create_dir_all(&dir).unwrap();
        let f = dir.join("feed.json");
        write_secret_file(&f, "x").unwrap();
        use std::os::unix::fs::PermissionsExt as _;
        assert_eq!(std::fs::metadata(&f).unwrap().permissions().mode() & 0o777, 0o600);
        assert!(write_secret_file(&f, "y").unwrap_err().contains("[adopt-feed-out]"), "never overwritten");
        let _ = std::fs::remove_dir_all(&dir);
    }

    /// ★ A seed key is its PUBLIC line, read from a file; a private key is never sent.
    #[test]
    fn the_seed_key_is_a_public_line_and_never_a_private_key() {
        let public = |_: &Path| Ok("ssh-ed25519 AAAAC3Nz installer-seed\n".to_string());
        let a = request_of(&w(&["site", "order", "s", "--admin", "a@b", "--seed-key", "k.pub"]), &public).unwrap();
        assert!(a.body.contains("ssh-ed25519 AAAAC3Nz installer-seed"));
        let private = |_: &Path| Ok("-----BEGIN OPENSSH PRIVATE KEY-----\nxx\n".to_string());
        assert!(request_of(&w(&["site", "order", "s", "--admin", "a@b", "--seed-key", "k"]), &private).unwrap_err().contains("[site-seed-key]"));
    }

    /// ★ **The run's own `why` is said, with since when** (test3-holger-rs 2026-10-22: it sat in
    /// "queued (held)", `queue_admits` wrote its reason into the run's `why`, and neither `site
    /// status` nor `site list` printed it). Not said when the box sets none.
    #[test]
    fn site_status_and_site_list_say_the_runs_why_and_since_when() {
        let st = said(What::Status, 200, r#"{"company":"test3-holger-rs","state":"queued","phase":"held","words":"Your Holger is paid and being prepared.","url":"","paid_until":"","why":"queued: the account's room holds 2 estates and 2 are laid","why_since":"2026-10-22 18:12Z"}"#).unwrap();
        assert!(st.contains("why: queued: the account's room holds 2 estates and 2 are laid (since 2026-10-22 18:12Z)"), "{st}");
        let quiet = said(What::Status, 200, r#"{"company":"c","state":"ready","phase":"captured","words":"ready","url":"","paid_until":"","why":"","why_since":""}"#).unwrap();
        assert!(!quiet.contains("why:") && !quiet.contains("first admin"), "{quiet}");
        // FIRST-LINK-1: the first-admin door, as monetize said it.
        let door = said(What::Status, 200, r#"{"company":"holgerrs","state":"ready","phase":"captured","words":"ready","url":"","paid_until":"","why":"","why_since":"","first_admin_door":"handed_off","first_admin_door_words":"holger-rs is handed off; its main answered the last ask: mailed to f***@mock.invalid"}"#).unwrap();
        assert!(door.contains("first admin: handed_off — holger-rs is handed off; its main answered the last ask: mailed to f***@mock.invalid\n"), "{door}");
        let list = said(What::List, 200, r#"{"sites":[{"estate":"test3-holger-rs","domain":"test3.holger.rs","vendor":"v","first_admin":"a","dns_words":"d","dns_writes":true,"cloud_words":"c","state":"queued","words":"Your Holger is paid and being prepared.","checkout_url":"","orderable":false,"why_not":"it is already ordered","terminable":true,"why":"queued: the account's room is full","why_since":"2026-10-22 18:12Z"}],"sizes":[]}"#).unwrap();
        assert!(list.contains("  why          queued: the account's room is full (since 2026-10-22 18:12Z)"), "{list}");
    }

    /// ★ The box's answers are said as they come: where to pay, a refusal in its own words.
    #[test]
    fn the_boxs_answers_are_said_as_they_come() {
        let ordered = said(What::Order, 200, r#"{"reference":"estate/test3gunnarrs","company":"test3gunnarrs","checkout_url":"https://pay.example/x","price_words":"0.50 EUR","words":"ordered"}"#).unwrap();
        assert!(ordered.contains("https://pay.example/x") && ordered.contains("0.50 EUR") && ordered.contains("estate/test3gunnarrs"), "{ordered}");
        let refused = said(What::Order, 409, r#"{"refused":"FailedPrecondition","words":"gunnar-rs stands: its main called home"}"#).unwrap_err();
        assert!(refused.contains("gunnar-rs stands") && refused.contains("409"), "{refused}");
        let list = said(What::List, 200, r#"{"sites":[{"estate":"test3-gunnar-rs","domain":"test3.gunnar.rs","vendor":"stripe/gunnar-test2","first_admin":"ignalina","dns_words":"this box's one DNS token writes its zone","dns_writes":true,"cloud_words":"upcloud · se-sto1","state":"none","words":"Not laid by this box.","checkout_url":"","orderable":true,"why_not":"","terminable":true}],"sizes":["Mini","Small"]}"#).unwrap();
        assert!(list.contains("test3.gunnar.rs, www.test3.gunnar.rs, git.test3.gunnar.rs — already fed") && list.contains("can be ordered now") && list.contains("terminable") && list.contains("sizes: Mini, Small"), "{list}");
        let ended = said(What::Terminate, 200, r#"{"words":"x.example is terminated","done":true}"#).unwrap();
        assert!(!ended.contains("backup"), "{ended}");
    }

    /// ★ **The site book at the door** (2026-10-14): `add` is ONE put with the site's fields and
    /// its secrets read from FILES; `edit` reads the site, then puts it back with only the flags
    /// given changed; `show` prints a secret as set and a fingerprint, never a value.
    #[test]
    fn a_site_is_added_and_edited_at_the_door_and_its_secrets_come_from_files() {
        let files = |p: &Path| match p.to_str() {
            Some("key") => Ok("sk_test_SECRETKEY\n".to_string()),
            Some("hook") => Ok("whsec_SECRETHOOK".to_string()),
            _ => Err("no file".to_string()),
        };
        let a = request_of(&w(&["site", "add", "test5-gunnar-rs", "--domain", "test5.gunnar.rs", "--first-admin", "ignalina", "--stripe-key-file", "key", "--webhook-secret-file", "hook", "--term-days", "1", "--terminable", "yes"]), &files).unwrap();
        assert_eq!((a.method, a.path.as_str(), a.what), ("POST", "/site/put", What::Put));
        let b: serde_json::Value = serde_json::from_str(&a.body).unwrap();
        assert_eq!((b["estate"].as_str(), b["create"].as_bool(), b["domain"].as_str(), b["vendor_secret_key"].as_str(), b["vendor_webhook_secret"].as_str()), (Some("test5-gunnar-rs"), Some(true), Some("test5.gunnar.rs"), Some("sk_test_SECRETKEY"), Some("whsec_SECRETHOOK")));
        assert_eq!((b["terms"]["term_days"].as_u64(), b["terminable"].as_bool(), b["operator_site"].as_bool(), b["live"].as_bool()), (Some(1), Some(true), Some(true), Some(false)));
        assert!(request_of(&w(&["site", "add", "x", "--first-admin", "o"]), &files).unwrap_err().contains("[site-domain]"));
        assert!(request_of(&w(&["site", "add", "x", "--domain", "x.rs", "--first-admin", "o", "--mode", "maybe"]), &files).unwrap_err().contains("[site-flag]"));
        let e = request_of(&w(&["site", "edit", "test3-gunnar-rs", "--domain", "t3.gunnar.rs"]), &files).unwrap();
        assert_eq!((e.method, e.path.as_str(), e.what), ("GET", "/site/show/test3-gunnar-rs", What::Edit));
        let shown = r#"{"site":{"estate":"test3-gunnar-rs","domain":"test3.gunnar.rs","product":"gunnar","first_admin":"ignalina","operator_site":true,"terminable":true,"vendor":"stripe/gunnar-test2","live":false,"terms":{"term_days":1,"term_months":0,"grace_hours":1,"margin_bp":2500},"media_from":"","source":"fed","secret_key_set":true,"secret_key_fingerprint":"sha256:0123abcd","webhook_secret_set":true,"webhook_secret_fingerprint":"sha256:89abcdef"},"products":["gunnar"],"vendors":["stripe/gunnar-test2"],"media_sites":["test2-gunnar-rs"],"currency":"EUR","sizes":["Mini"],"history":["2026-10-14 12:00:00 UTC — console:op-1 — added test3-gunnar-rs: domain: test3.gunnar.rs"]}"#;
        let put = edit_body(shown, &w(&["site", "edit", "test3-gunnar-rs", "--domain", "t3.gunnar.rs"]), &files).unwrap();
        let p: serde_json::Value = serde_json::from_str(&put.body).unwrap();
        assert_eq!((p["domain"].as_str(), p["vendor"].as_str(), p["create"].as_bool(), p["terms"]["margin_bp"].as_u64(), p["vendor_secret_key"].as_str()), (Some("t3.gunnar.rs"), Some("stripe/gunnar-test2"), Some(false), Some(2500), Some("")), "only the flag moved; the held secrets are kept");
        let said_show = said(What::Show, 200, shown).unwrap();
        assert!(said_show.contains("secret key     set (sha256:0123abcd)") && said_show.contains("console:op-1") && said_show.contains("stripe/gunnar-test2 (test)"), "{said_show}");
        assert_eq!(request_of(&w(&["site", "show"]), &files).unwrap().path, "/site/show");
        let done = said(What::Put, 200, r#"{"words":"test5-gunnar-rs is added","changed":["domain: test5.gunnar.rs","Stripe secret key: set (sha256:0123abcd)"]}"#).unwrap();
        assert!(done.contains("test5-gunnar-rs is added") && done.contains("  Stripe secret key: set"), "{done}");
    }

    #[test]
    fn the_door_and_the_material_come_from_flags_or_the_environment() {
        let env = |k: &str| (k == DOOR_ENV).then(|| "box.example:8475".to_string());
        let (t, rest) = target_of(&w(&["site", "list", "--material", "/m"]), &env).unwrap();
        assert_eq!((t.door.as_str(), t.material.to_str().unwrap(), rest), ("box.example:8475", "/m", w(&["site", "list"])));
        let (t, _) = target_of(&w(&["--door", "other:8475", "site", "list"]), &env).unwrap();
        assert_eq!(t.door, "other:8475");
        assert!(door(&Target::default(), &Ask { method: "GET", path: "/site/list".into(), body: String::new(), what: What::List }).unwrap_err().contains("[door]"));
    }

    /// ★ A site order comes back to the box's own Sites page (live 2026-10-14 returned to the
    /// site being laid, which had no front): from the door's host unless `--return` names one.
    #[test]
    fn an_order_returns_to_the_boxs_sites_page() {
        assert_eq!(sites_page_of("monetize.ignalina.dk:8475"), "https://monetize.ignalina.dk/sites");
        assert_eq!(sites_page_of("10.0.0.5:8475"), "");
        assert_eq!(sites_page_of("[::1]:8475"), "");
        assert_eq!(sites_page_of(""), "");
        let order = request_of(&w(&["site", "order", "test2-gunnar-rs", "--admin", "ops@example.invalid"]), &no_file).unwrap();
        let back: serde_json::Value = serde_json::from_str(&with_return(order.clone(), "monetize.ignalina.dk:8475").body).unwrap();
        assert_eq!(back["return_url"], "https://monetize.ignalina.dk/sites");
        let named = request_of(&w(&["site", "order", "x", "--admin", "a@b.invalid", "--return", "https://elsewhere.example/sites"]), &no_file).unwrap();
        let kept: serde_json::Value = serde_json::from_str(&with_return(named, "monetize.ignalina.dk:8475").body).unwrap();
        assert_eq!(kept["return_url"], "https://elsewhere.example/sites");
        let list = request_of(&w(&["site", "list"]), &no_file).unwrap();
        assert_eq!(with_return(list.clone(), "monetize.ignalina.dk:8475"), list);
    }

    /// ★ **OVW-CLEAN: `estates`**, READ ONLY: one GET at the door; the words are the console's
    /// Overview — a line per estate that exists, the quiet line with its names, the slot.
    #[test]
    fn estates_is_one_read_and_says_the_overview_as_the_box_classifies_it() {
        let no_file = |_: &Path| Err::<String, String>("no file".into());
        assert_eq!(request_of(&w(&["estates"]), &no_file).unwrap(), Ask { method: "GET", path: "/estates/overview".into(), body: String::new(), what: What::Estates });
        let answer = r#"{"estates":[{"name":"gunnar-rs","product":"gunnar","standing":"installed","words":"installed: its machines have reported in","in_fleet":true,"domain":"gunnar.rs"}],
            "ended":[{"name":"acme2","product":"holger","standing":"ended","words":"terminated on 2026-10-27","in_fleet":true,"domain":""}],
            "slot":{"products":["gunnar","holger"],"declared":["test2-gunnar-rs"]},"stale_secs":null}"#;
        let out = said(What::Estates, 200, answer).unwrap();
        assert_eq!(
            out.lines().collect::<Vec<_>>(),
            ["gunnar-rs gunnar installed gunnar.rs — installed: its machines have reported in", "1 ended or never-installed:", "  acme2 holger ended - — terminated on 2026-10-27", "new estate: products gunnar, holger; declared, started by name: test2-gunnar-rs"],
            "{out}"
        );
        let empty = said(What::Estates, 200, r#"{"estates":[],"ended":[],"slot":{"products":[],"declared":[]}}"#).unwrap();
        assert_eq!(empty, "no estate is installed or being laid\nnew estate: products none; declared, started by name: none\n");
        assert!(help().contains("monetize … estates"));
    }

    /// ★ **MAP-SEG 3: `box inventory`**, READ ONLY: one GET at the door, and the words say per
    /// row its list, estate, order and label — the orphans first — so the owner's orphan list
    /// never needs a console session again. No secret is in the answer to print.
    #[test]
    fn box_inventory_is_one_read_and_names_each_rows_estate_order_and_label() {
        let no_file = |_: &Path| Err::<String, String>("no file".into());
        assert_eq!(request_of(&w(&["box", "inventory"]), &no_file).unwrap(), Ask { method: "GET", path: "/cloud/inventory".into(), body: String::new(), what: What::Inventory });
        let answer = r#"{"clouds":[{"provider":"upcloud","available":true,"error":"","foreign_count":11,
            "owned":[{"id":"server/00a","kind":"server","gib":0,"zone":"se-sto1","attached_to":"","estate":"test2-gunnar-rs","reference":"estate/test2gunnarrs","resource":"lay:front:server","priced":true,"monthly_minor":2200}],
            "orphans":[{"id":"storage/01b","kind":"disk","gib":40,"zone":"se-sto1","attached_to":"","estate":"holgerr34e","reference":"estate/holgerr34e","resource":"lay:main:disk1","priced":true,"monthly_minor":400}],
            "missing":[]}]}"#;
        let out = said(What::Inventory, 200, answer).unwrap();
        let lines: Vec<&str> = out.lines().collect();
        assert_eq!(lines[0], "cloud upcloud: listed");
        assert_eq!(lines[1], "orphan storage/01b disk 40GiB se-sto1 - estate=holgerr34e ref=estate/holgerr34e res=lay:main:disk1 400/mo", "{out}");
        assert_eq!(lines[2], "owned server/00a server server se-sto1 - estate=test2-gunnar-rs ref=estate/test2gunnarrs res=lay:front:server 2200/mo", "{out}");
        assert_eq!(lines[3], "1 orphan(s), 1 owned, 0 backup(s), 0 missing, 11 not ours (no monetize label)");
        // ★ MAP-3: an account two products list is named once with both; a backup is its own
        // line under its estate, never an orphan, and counted apart.
        let answer = r#"{"clouds":[{"provider":"upcloud","account":"upcloud","products":["gunnar","holger"],"available":true,"error":"","foreign_count":3,
            "owned":[],"orphans":[{"id":"storage/bk-gone","kind":"backup","gib":10,"zone":"se-sto1","attached_to":"","estate":"","reference":"estate/old","resource":"","title":"Simple backup of old-front system","priced":false}],
            "backups":[{"id":"storage/bk1","gib":10,"zone":"se-sto1","estate":"holger-rs","reference":"estate/holgerrs","words":"backup of holger-web-ui-holgerrs, 2026-10-27"}],
            "missing":[]}]}"#;
        let out = said(What::Inventory, 200, answer).unwrap();
        let lines: Vec<&str> = out.lines().collect();
        assert_eq!(lines[0], "cloud upcloud (gunnar, holger): listed", "{out}");
        assert_eq!(lines[1], "orphan storage/bk-gone backup 10GiB se-sto1 - estate=- ref=estate/old res=- unpriced \"Simple backup of old-front system\"", "{out}");
        assert_eq!(lines[2], "backup storage/bk1 10GiB se-sto1 estate=holger-rs ref=estate/holgerrs backup of holger-web-ui-holgerrs, 2026-10-27", "{out}");
        assert_eq!(lines[3], "1 orphan(s), 0 owned, 1 backup(s), 0 missing, 3 not ours (no monetize label)", "{out}");
        assert!(said(What::Inventory, 200, r#"{"clouds":[{"provider":"upcloud","available":false,"error":"401"}]}"#).unwrap().contains("NOT listed: 401"));
        assert!(help().contains("box inventory"));
    }
}