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

nordisk/holger-ui

browser-tests/src/rig.rs

//! **The rig**: a real holger, the shared fake ONE box, and the console as holger's front
//! serves it, on loopback.
//!
//! * holger: holger-server-lib in this process, booted from holger-recipe's
//!   `estate_ron::medium()` (the declaration every estate ships: the intake chain, two-person
//!   approval, the audit chain, the duties) with its paths in a temporary directory, the scan
//!   in process, and the passkey plane a paired box adds from its feed, at `localhost`.
//! * the ONE box and Stripe: monetize-site-buy `testkit::FakeOneBox`.
//! * the console: holger-estate-front `console::router`, the very router the front serves on
//!   :443 (holger-ui-server's bundle and pass-through, the buy pages' shared scripts, and the
//!   shared relay merged in front), over the bundle [`crate::bundle::dist`] built.
//!
//! The browser reaches the console at `http://localhost:<port>`, a secure context, so the
//! passkey ceremonies run against the browser's virtual authenticator.

use std::collections::BTreeMap;
use std::io::Write as _;
use std::net::SocketAddr;
use std::path::{Path, PathBuf};

use holger_estate_front::buy::{self, ShopFeed};
use holger_estate_front::console::Wiring;
use holger_recipe::estate_ron;
use holger_server_lib::auth::AuthMethodConfig;
use holger_server_lib::Holger;
use monetize_site_buy::testkit::FakeOneBox;

/// holger's console bearer (the estate's static token, subject `console`).
pub const CONSOLE: &str = "console-bearer-of-sufficient-length-0123456789";
/// The site's own bearer at the ONE box.
pub const SITE_BEARER: &str = "holger-site-bearer-for-browser-tests";
/// The downstream connector [`Rig::start_with_graph`] declares (`sync.downstreams`).
pub const GRAPH_DOWNSTREAM: &str = "lab-holger";
/// The upstream sources [`Rig::start_with_graph`] declares: two upstreams and an upload door.
/// `cloud`'s door is a closed loopback port: its mirror never completes a cycle, as the board
/// must then say. It mirrors into the raw `bundles`: the estate's `crates` is the chain's cargo
/// door (holger c6ef70be), which holds nothing of its own, so a mirror into it is refused at
/// boot.
pub const GRAPH_SOURCES: &str = r#"    sources: [
        (name: "crates-io", kind: crates, serves: ["crates"]),
        (name: "cloud", kind: holger, url: "http://127.0.0.1:9", repositories: [(ron_from: "crates", ron_to: "bundles")]),
        (name: "team", kind: upload),
    ],
"#;
/// The names the graph's board must draw: each source and the downstream.
pub const GRAPH_ROWS: [&str; 4] = ["crates-io", "cloud", "team", GRAPH_DOWNSTREAM];
/// The wheel the console walks through the intake chain.
/// The front [`Rig::pair_with_front`] names, and the names it reports ([`Rig::front_reports`]).
pub const FRONT: &str = "front";
pub const FRONT_CONSOLE_HOST: &str = "www.holger.test";
pub const FRONT_REPO_HOST: &str = "repo.holger.test";
/// The path of holger's flow drawing, as the console asks for it (gRPC-web, `/grpc/…`).
pub const FLOW_SVG: &str = "/holger.v1.AdminService/FlowSvg";
/// What [`Rig::refuse_flow`] has the console answer each `FlowSvg` ask with (gRPC
/// `UNAVAILABLE`), as a holger that is restarting would.
pub const FLOW_DOWN: &str = "holger is restarting";

pub const WHEEL: &str = "demo_pkg-1.0.0-py3-none-any.whl";

/// **One running rig.** Dropped at the end of a test (holger's threads end with the process).
pub struct Rig {
    pub data: tempfile::TempDir,
    pub holger: Holger,
    pub fake: FakeOneBox,
    /// `http://localhost:<port>`: the console, as the browser reaches it.
    pub base: String,
    /// holger's HTTP door, `http://127.0.0.1:<port>` (the harness's own asks).
    pub door: String,
    /// How long the console holds `POST …/passkey/register/start` before passing it on, in
    /// milliseconds ([`Rig::slow_register_start`]); 0 passes it at once.
    slow_start_ms: std::sync::Arc<std::sync::atomic::AtomicU64>,
    slow_login_ms: std::sync::Arc<std::sync::atomic::AtomicU64>,
    /// [`Rig::sell_nothing`]: the console answers `GET /-/signup` as a private instance's
    /// holger does (no shop sells here).
    sells_nothing: std::sync::Arc<std::sync::atomic::AtomicBool>,
    /// [`Rig::offer_set`]: the one dependency set the set doors answer with (SEC-REVIEW D4).
    set: std::sync::Arc<std::sync::Mutex<Option<OfferedSet>>>,
    /// Every `FlowSvg` ask the console passed on, and whether it is refused ([`Rig::refuse_flow`]).
    flow: FlowAsks,
    _cfg: tempfile::NamedTempFile,
}

/// **The console's asks for the flow drawing** (F14): the zoom each one named, in order, and
/// the switch that has them refused.
#[derive(Clone, Default)]
struct FlowAsks {
    asked: std::sync::Arc<std::sync::Mutex<Vec<String>>>,
    refused: std::sync::Arc<std::sync::atomic::AtomicBool>,
}

/// The zoom a `FlowSvg` ask names: `FlowSvgRequest.open` (field 1, a short string), read off
/// its gRPC-web frame (a flag byte and a four-byte length, then the message; nothing open is
/// an empty message).
fn asked_open(frame: &[u8]) -> String {
    match frame.get(5..).unwrap_or_default() {
        [0x0a, len, rest @ ..] if *len < 0x80 && usize::from(*len) <= rest.len() => String::from_utf8_lossy(&rest[..usize::from(*len)]).into_owned(),
        _ => String::new(),
    }
}


/// **The dependency set the rig's set doors offer** ([`Rig::offer_set`]). The estate's medium
/// declares no pull-through source, so holger itself has no set to show; the rig answers
/// `GET /-/intake/sets` and `POST /-/intake/set/approve|refuse` in holger's shape and on
/// holger's rule (holger-server-lib `pull_intake::depset`, SEC-REVIEW D4): a vote naming
/// another revision than the set has now is refused with 409 and holger's
/// `holger-depset-4` words, and nothing is cast.
#[derive(Clone, Debug)]
pub struct OfferedSet {
    pub id: String,
    pub revision: String,
    /// The votes cast, as `(verb, revision named)`.
    pub votes: Vec<(String, String)>,
}

impl OfferedSet {
    fn sets_body(&self) -> String {
        serde_json::json!({
            "sets": [{
                "id": self.id, "revision": self.revision, "source": "pypi", "ecosystem": "python",
                "root": "app==1.0", "at": 1, "status": "awaiting-approval",
                "summary": "2 of 2 member(s) wait for approval; 0 of 2 approver(s) approved the whole set",
                "members": [
                    {"name": "pypi/app-1.0-py3-none-any.whl", "role": "requested", "via": "", "state": "policy-checked", "sha256": "aa", "approvals": 0, "approvers": [], "reason": "", "warnings": []},
                    {"name": "pypi/dep-1.0-py3-none-any.whl", "role": "dependency", "via": "app==1.0", "state": "policy-checked", "sha256": "bb", "approvals": 0, "approvers": [], "reason": "", "warnings": []}
                ],
                "blockers": [], "holds": [], "approvals_required": 2, "approvers": [], "awaiting": 2, "available": 0, "notes": []
            }],
            "approvals_required": 2
        })
        .to_string()
    }

    /// holger's answer to one vote: `(status, body)`.
    fn judge(&mut self, verb: &str, query: &str) -> (u16, String) {
        let named = query
            .split('&')
            .find_map(|p| p.strip_prefix("revision="))
            .unwrap_or("")
            .to_string();
        let label = format!("dependency set {} (app==1.0)", self.id);
        if named != self.revision {
            let said = if named.is_empty() { "none".to_string() } else { format!("'{named}'") };
            return (
                409,
                holger_server_lib::errcode::DEPSET_STALE.tag(format!(
                    "{label} is at revision {} and the vote names {said}: read the set again and judge what it shows",
                    self.revision
                )),
            );
        }
        self.votes.push((verb.to_string(), named));
        (200, serde_json::json!({"id": self.id, "status": "awaiting-approval", "moved": 2, "message": format!("owner {verb}d {label}: 2 vote(s) cast")}).to_string())
    }
}

/// The path whose answer [`Rig::slow_register_start`] holds back.
pub const REGISTER_START: &str = "/-/auth/passkey/register/start";
/// The sign-in's start, which the rig can hold back too (RA2-6).
pub const LOGIN_START: &str = "/-/auth/passkey/login/start";
/// holger's sign-up door: what the ONE shop sells on this root ([`Rig::sell_nothing`]).
pub const SIGNUP_DOOR: &str = "/-/signup";
/// What a private instance's holger answers on [`SIGNUP_DOOR`]: no shop, nothing sold.
pub const NOTHING_SOLD: &str = r#"{"account_url":"","private_offered":false,"private_url":"","signed_in":false,"account":"","plan":"","grow_account_url":"","instance":"","grow_instance_url":"","words":""}"#;

/// **The doors the rig declares for holger** (HUI-PORT): the gRPC door, the HTTP door and the
/// replication door, each a loopback port the rig HOLDS (a bound listener) from the moment it
/// picks it until [`Doors::release`], just before holger binds them. The replication door is
/// declared at its own held port (`replication_door.ron_bind`), never left at holger's default,
/// the gRPC port + 1, which nothing held.
struct Doors {
    grpc: u16,
    http: u16,
    replication: u16,
    held: Vec<std::net::TcpListener>,
}

impl Doors {
    fn hold() -> Doors {
        let held: Vec<std::net::TcpListener> = (0..3).map(|_| std::net::TcpListener::bind("127.0.0.1:0").expect("a loopback port for holger")).collect();
        let port = |i: usize| held[i].local_addr().unwrap().port();
        Doors { grpc: port(0), http: port(1), replication: port(2), held }
    }

    /// The declaration `ron` with its doors moved onto the held ports.
    fn declare(&self, ron: &str) -> String {
        let default = "replication_door: Some((ron_bind: None,";
        assert!(ron.contains(default), "the declaration has a replication door to move:\n{ron}");
        ron.replace("\"0.0.0.0:50051\"", &format!("\"127.0.0.1:{}\"", self.grpc))
            .replace("\"0.0.0.0:8443\"", &format!("\"127.0.0.1:{}\"", self.http))
            .replace(default, &format!("replication_door: Some((ron_bind: Some(\"127.0.0.1:{}\"),", self.replication))
    }

    /// Let go of every held port, right before holger opens its doors on them.
    fn release(self) {
        drop(self.held);
    }
}

/// The intel database a box gets by import, installed by hand: no advisory, a fresh snapshot.
fn install_intel(dir: &Path) {
    use holger_intel::chrono::Utc;
    use holger_intel::{AdvisorySet, InstalledIntel, LocalIntelDb};
    let set = AdvisorySet { schema: holger_intel::set::SET_SCHEMA.into(), generated_at: Utc::now(), sources: vec![], advisories: vec![] };
    let db = LocalIntelDb::open(dir);
    let bytes = set.to_json_bytes().unwrap();
    db.stage_generation(1, &bytes).unwrap();
    db.activate(&InstalledIntel {
        schema: holger_intel::db::INSTALLED_SCHEMA.into(),
        bundle_id: "holger-intel-1".into(),
        sequence: 1,
        supplier: holger_intel::DEFAULT_SUPPLIER.into(),
        snapshot_at: Utc::now(),
        imported_at: Utc::now(),
        candidate_digest: "00".into(),
        release_signer_fingerprint: "00".into(),
        advisories_file: LocalIntelDb::generation_file(1),
        advisories_sha256: holger_server_lib::security::blob_digest_hex(&bytes),
        advisories: 0,
    })
    .unwrap();
}

impl Rig {
    /// **Start everything.** The console's port is taken first: holger's passkey origin names it.
    pub async fn start() -> Rig {
        Self::start_with(None).await
    }

    /// **Start everything, the box an upstream** with a `sync:` block (Holger v2.0 H4): its
    /// sync key minted under the rig's data, one enrolled downstream `downstream` (its key
    /// minted here too; nothing ever acknowledges for it) and an acknowledgement deadline of
    /// `deadline_secs`. The board door (`GET /-/sync/board`) then serves this upstream's board.
    pub async fn start_upstream_of(downstream: &str, deadline_secs: u64) -> Rig {
        Self::start_with(Some((downstream.to_string(), deadline_secs))).await
    }

    /// **Start everything, the box declaring a graph** (HBOARD-1): the estate's declaration
    /// plus `sources:` (two upstream sources, `crates-io` served as fetched into `crates` and
    /// the upstream holger `cloud` mirrored into `crates`, and the upload door `team`) and a
    /// `sync:` block with ONE downstream connector, [`GRAPH_DOWNSTREAM`]. The board must
    /// draw each of them.
    pub async fn start_with_graph() -> Rig {
        Self::start_inner(Some((GRAPH_DOWNSTREAM.to_string(), 3600)), true, None).await
    }

    /// **Start everything, holger redrawing its flow every `secs`** (F14): the estate's
    /// declaration with `flow: (refresh_secs: secs)`, the interval holger's model hands the
    /// console (`0` is never).
    pub async fn start_with_flow_refresh(secs: u32) -> Rig {
        Self::start_inner(None, false, Some(secs)).await
    }

    async fn start_with(sync: Option<(String, u64)>) -> Rig {
        Self::start_inner(sync, false, None).await
    }

    async fn start_inner(sync: Option<(String, u64)>, graph: bool, flow_refresh: Option<u32>) -> Rig {
        let dist = crate::bundle::dist().to_path_buf();
        let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.expect("a loopback port for the console");
        let port = listener.local_addr().unwrap().port();
        let base = format!("http://localhost:{port}");

        let data = tempfile::tempdir().unwrap();
        let doors = Doors::hold();
        let (grpc, http) = (doors.grpc, doors.http);
        let d = data.path().display().to_string();
        let ron = estate_ron::medium()
            .replace(&format!("\"{}/", holger_recipe::packaging::BOX_HOLGER_DATA), &format!("\"{d}/holger/"))
            .replace(&format!("\"{}/", estate_ron::BOX_APPLIANCE_ROOT), &format!("\"{d}/"))
            .replace(&format!("\"{}/", estate_ron::RELEASE_ROOT), &format!("\"{d}/"))
            // The scan runs in this process; the shipped declaration keeps the sandbox required.
            .replace("rules: (sandbox: required),", "rules: (sandbox: off),")
            // No clamd on a test rig: the malware scan is the sandbox's, and the sandbox is off.
            .replace("clamd.sock\"), required: true)", "clamd.sock\"), required: false)");
        let ron = doors.declare(&ron);
        assert!(!ron.contains("\"/data"), "every path moved:\n{ron}");
        let ron = match &sync {
            None => ron,
            Some((downstream, deadline)) => {
                use holger_server_lib::sync::protocol::SyncKey;
                let keys = data.path().join("sync-keys");
                std::fs::create_dir_all(&keys).unwrap();
                let own = keys.join("cloud.key");
                SyncKey::open_or_mint(&own).expect("the upstream's sync key");
                let theirs = SyncKey::open_or_mint(&keys.join(format!("{downstream}.key"))).expect("the downstream's key").public_hex();
                let i = ron.rfind(')').expect("the declaration closes");
                format!(
                    "{}    sync: Some((name: \"cloud\", key: Some(\"{}\"), downstreams: [(name: \"{downstream}\", key: \"{theirs}\")], ack_deadline_secs: {deadline})),\n{}",
                    &ron[..i],
                    own.display(),
                    &ron[i..]
                )
            }
        };
        let ron = if graph {
            // `crates-io` serves `crates` as fetched, past the intake chain: since SEC-REVIEW
            // P5 (holger 7c6ebb3f) a box whose policy requires a scan refuses that at boot
            // (`holger-intake-17`). The graph is about what the board and the Security page
            // draw, so this box requires no scan; the estate's own declaration keeps its.
            let ron = ron.replacen("scan: Some((required: [vulnerability, sbom],", "scan: Some((required: [],", 1);
            let i = ron.rfind(')').expect("the declaration closes");
            format!("{}{GRAPH_SOURCES}{}", &ron[..i], &ron[i..])
        } else {
            ron
        };
        let ron = match flow_refresh {
            Some(secs) => {
                assert!(!ron.contains("\n    flow: "), "the declaration has a flow block of its own:\n{ron}");
                let i = ron.rfind(')').expect("the declaration closes");
                format!("{}    flow: (refresh_secs: {secs}),\n{}", &ron[..i], &ron[i..])
            }
            None => ron,
        };
        write_key_file(&data.path().join(estate_ron::CONSOLE_TOKEN_FILE), CONSOLE);
        std::fs::write(data.path().join(estate_ron::KORP_ROOT_STAGED), estate_ron::KORP_ROOT_PEM).unwrap();
        install_intel(&data.path().join("holger/intel"));
        let mut cfg = tempfile::NamedTempFile::new().unwrap();
        cfg.write_all(ron.as_bytes()).unwrap();
        let mut holger = holger_server_lib::read_ron_config(cfg.path()).expect("the estate's declaration parses");
        holger.resolve_estate(Some("localhost"));
        // The passkey plane, as a paired box adds it from the feed's `passkey_rp_id`.
        holger.auth.as_mut().expect("the estate declares auth").methods.push(AuthMethodConfig::Passkey {
            rp_id: "localhost".into(),
            rp_origin: base.clone(),
            rp_name: "holger".into(),
            session_key_file: None,
            bootstrap_until_first_admin: true,
        });
        holger.instantiate_backends().expect("the backends");
        holger_server_lib::wire_holger(&mut holger).expect("holger wires");
        // The console's path on this origin, as every estate box declares it (holger-recipe
        // `packaging`: `HOLGER_CONSOLE_PATH` = `BOX_CONSOLE_PATH`): holger's front page draws its
        // Log in from it. One value for every rig, set before the first holger of the process.
        static CONSOLE_PATH: std::sync::Once = std::sync::Once::new();
        CONSOLE_PATH.call_once(|| std::env::set_var("HOLGER_CONSOLE_PATH", holger_recipe::packaging::BOX_CONSOLE_PATH));
        doors.release();
        holger.start().expect("holger starts");
        let door = format!("http://127.0.0.1:{http}");
        wait_for(&format!("{door}/healthz")).await;

        let fake = FakeOneBox::spawn("holger", SITE_BEARER).await;
        // A ONE box that checks Holger's tickets, as the real one does: Holger's grow doors need a
        // checked, unspent ticket (owner 2026-10-26), and holger mints a fresh one per ask.
        fake.verify_tickets();
        let feed: BTreeMap<String, String> = [
            (buy::FEED_MONETIZE_BEARER, SITE_BEARER.to_string()),
            (buy::FEED_CONSOLE_PRODUCT, "holger".to_string()),
            (buy::FEED_FLEET, fake.url()),
            (buy::FEED_PASSKEY_RP, "localhost".to_string()),
        ]
        .into_iter()
        .map(|(k, v)| (k.to_string(), v))
        .collect();
        let shop: ShopFeed = buy::shop_feed_of(&feed).expect("the feed sells here").expect("a usable feed");
        let w = Wiring { appliance: Some(format!("127.0.0.1:{grpc}")), door: Some(format!("127.0.0.1:{http}")), token: Some(CONSOLE.into()), shop_bearer: None, shop: Some(shop), refused: Vec::new() };
        let app = holger_estate_front::console::router(&w, Some(dist)).expect("the console's router");
        // ★ A slow registration start (A29-20): right after paying, the start waits on the box
        // and its ONE box; the rig can hold it back as long as a buyer's did.
        let slow_start_ms = std::sync::Arc::new(std::sync::atomic::AtomicU64::new(0));
        let hold = slow_start_ms.clone();
        // ★ And a slow sign-in start (RA2-6): the plain Log in's start, held back the same way.
        let slow_login_ms = std::sync::Arc::new(std::sync::atomic::AtomicU64::new(0));
        let hold_login = slow_login_ms.clone();
        // ★ A root that sells nothing (HOL-LOGIN): a private instance's holger, its sign-up door
        // answering no shop.
        let sells_nothing = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
        let nothing = sells_nothing.clone();
        // ★ The flow drawing's asks (F14): written down with the zoom each names, and refused
        // while the test says so.
        let set: std::sync::Arc<std::sync::Mutex<Option<OfferedSet>>> = Default::default();
        let offered = set.clone();
        let flow = FlowAsks::default();
        let flow_asks = flow.clone();
        let app = app.layer(axum::middleware::from_fn(move |mut req: axum::extract::Request, next: axum::middleware::Next| {
            let private = req.uri().path() == SIGNUP_DOOR && nothing.load(std::sync::atomic::Ordering::SeqCst);
            let set_answer = {
                let mut guard = offered.lock().unwrap();
                match (guard.as_mut(), req.uri().path()) {
                    (Some(s), "/-/intake/sets") => Some((200, s.sets_body())),
                    (Some(s), "/-/intake/set/approve") => Some(s.judge("approve", req.uri().query().unwrap_or(""))),
                    (Some(s), "/-/intake/set/refuse") => Some(s.judge("refuse", req.uri().query().unwrap_or(""))),
                    _ => None,
                }
            };
            let ms = match req.uri().path() {
                REGISTER_START => hold.load(std::sync::atomic::Ordering::SeqCst),
                LOGIN_START => hold_login.load(std::sync::atomic::Ordering::SeqCst),
                _ => 0,
            };
            let flow = flow_asks.clone();
            async move {
                if let Some((status, body)) = set_answer {
                    use axum::response::IntoResponse as _;
                    let ctype = if status == 200 { "application/json" } else { "text/plain" };
                    return (axum::http::StatusCode::from_u16(status).unwrap(), [(axum::http::header::CONTENT_TYPE, ctype)], body).into_response();
                }
                if req.uri().path().ends_with(FLOW_SVG) {
                    let (parts, body) = req.into_parts();
                    let frame = axum::body::to_bytes(body, 1 << 20).await.unwrap_or_default();
                    flow.asked.lock().unwrap().push(asked_open(&frame));
                    if flow.refused.load(std::sync::atomic::Ordering::SeqCst) {
                        use axum::response::IntoResponse as _;
                        let message = FLOW_DOWN.replace(' ', "%20");
                        return ([("content-type", "application/grpc-web+proto"), ("grpc-status", "14"), ("grpc-message", message.as_str())], "").into_response();
                    }
                    req = axum::extract::Request::from_parts(parts, axum::body::Body::from(frame));
                }
                if private {
                    use axum::response::IntoResponse as _;
                    return ([(axum::http::header::CONTENT_TYPE, "application/json")], NOTHING_SOLD).into_response();
                }
                if ms > 0 {
                    tokio::time::sleep(std::time::Duration::from_millis(ms)).await;
                }
                next.run(req).await
            }
        }));
        tokio::spawn(async move { axum::serve(listener, app.into_make_service_with_connect_info::<SocketAddr>()).await });
        Rig { data, holger, fake, base, door, slow_start_ms, slow_login_ms, sells_nothing, set, flow, _cfg: cfg }
    }

    /// **This holger's estate has a front** (HOL-FLOW-FRONT): the pairing feed names it, as a
    /// paired box's pairing loop sets it ([`FRONT`]). Process-wide: a test that calls it runs
    /// in a test binary of its own.
    pub fn pair_with_front(&self) {
        use holger_server_lib::topology::{set_pairing, PairedBox, Pairing};
        set_pairing(Some(Pairing {
            estate: "localhost".into(),
            me: "main".into(),
            boxes: vec![
                PairedBox { name: "main".into(), role: "primary".into(), ..Default::default() },
                PairedBox { name: FRONT.into(), role: "front".into(), ..Default::default() },
            ],
            not_after_unix: 0,
        }));
    }

    /// ★ **The front reports its doors and routes to this holger** (HOL-FLOW-FRONT), the way the
    /// front does: the key this box keeps for its front (what its pairing loop derives from the
    /// fed console keys), a report proven under it, `POST`ed to holger's own HTTP door. Answers
    /// the door's status.
    pub fn front_reports(&self) -> u16 {
        use holger_server_lib::front_report::{self, front};
        let key = front::key_of(&[0x5a; 32]);
        front_report::keep_key(&self.holger.data_root(), &key).expect("the front's key is kept");
        let route = |host: &str, to: &str, what: &str| front::FrontRoute { host: host.into(), door: "https".into(), to: to.into(), what: what.into() };
        let report = front::FrontReport {
            front: FRONT.into(),
            at_unix: holger_server_lib::clock::now_unix_secs() as i64,
            doors: vec![
                front::FrontDoor { name: "https".into(), listen: "tcp/443".into(), what: "the web face: TLS with the estate's certificate".into() },
                front::FrontDoor { name: "http".into(), listen: "tcp/80".into(), what: "the certificate's challenges, and the way to https".into() },
            ],
            routes: vec![
                route(FRONT_CONSOLE_HOST, front::TO_CONSOLE, "holger-ui, and its pass-through to main's two doors"),
                route(FRONT_REPO_HOST, front::TO_MAIN, "main http :8443, gRPC :50051 (holger.*)"),
            ],
            console_url: format!("https://{FRONT_CONSOLE_HOST}"),
        };
        let sent = front::Sent { proof: front::proof(&key, &report), report };
        let body = serde_json::to_vec(&sent).expect("the report is JSON");
        match ureq::post(&format!("{}{}", self.door, front::PATH)).header("content-type", "application/json").send(&body[..]) {
            Ok(r) => r.status().as_u16(),
            Err(ureq::Error::StatusCode(c)) => c,
            Err(e) => panic!("the front's report could not be sent: {e}"),
        }
    }

    /// ★ **The graph's downstream reports its console's address** (HOL-FLOW-FRONT): one sync
    /// report from [`GRAPH_DOWNSTREAM`], signed with its key as its mirror signs (the report, its
    /// RON summary and the summary's console address, each with its own proof), heard by this
    /// upstream's hub. Only on a rig from [`Rig::start_with_graph`].
    pub fn downstream_reports_console(&self, console_url: &str) {
        use holger_server_lib::grpc::holger_grow_proto::{DownstreamPoint, DownstreamReportRequest, DownstreamSummary};
        use holger_server_lib::sync::protocol::SyncKey;
        use holger_server_lib::sync::reports::{console_text, report_text, summary_text};
        use base64::Engine as _;
        let hub = self.holger.intake().expect("the intake chain").sync().expect("this rig was started as an upstream");
        let key = SyncKey::open_or_mint(&self.data.path().join("sync-keys").join(format!("{GRAPH_DOWNSTREAM}.key"))).expect("the downstream's key");
        let now = holger_server_lib::clock::now_unix_secs() as i64;
        let sign = |text: &str| base64::engine::general_purpose::STANDARD.encode(key.sign_report(GRAPH_DOWNSTREAM, "cloud", text.as_bytes(), now).expect("signed").to_bytes());
        let mut req = DownstreamReportRequest {
            points: vec![DownstreamPoint { repository: "crates".into(), applied_boot: 0, applied_seq: 0, last_complete_unix_ms: now * 1000 }],
            every_secs: 60,
            summary: Some(DownstreamSummary {
                boundary: "Fetch".into(),
                repositories: vec!["crates".into(), "pypi".into()],
                first_import: "admin".into(),
                domain: "lab".into(),
                console_url: console_url.into(),
                ..Default::default()
            }),
            ..Default::default()
        };
        req.summary_proof = sign(&summary_text(&req).expect("a summary"));
        if let Some(text) = console_text(&req) {
            req.console_proof = sign(&text);
        }
        let proof = sign(&report_text(&req));
        hub.heard(&proof, &req, None, 0, 0, now * 1000).expect("the upstream hears its downstream");
    }

    /// **Issue an invalidation on this upstream** (what `holger sync invalidate` does): every
    /// enrolled downstream must acknowledge it within the deadline.
    pub fn invalidate(&self, coordinate: &str, detail: &str) {
        let hub = self.holger.intake().expect("the intake chain").sync().expect("this rig was started as an upstream");
        hub.invalidate("security", "browser-test", coordinate, None, holger_server_lib::sync::protocol::Reason::Malware, detail).expect("the invalidation");
    }

    /// **Hold every registration start back by `by`** before the console passes it on (a slow
    /// box right after paying); [`std::time::Duration::ZERO`] passes it at once again.
    pub fn slow_register_start(&self, by: std::time::Duration) {
        self.slow_start_ms.store(by.as_millis() as u64, std::sync::atomic::Ordering::SeqCst);
    }

    /// Hold the plain sign-in's start (`/-/auth/passkey/login/start`) back by `by` (RA2-6).
    pub fn slow_login_start(&self, by: std::time::Duration) {
        self.slow_login_ms.store(by.as_millis() as u64, std::sync::atomic::Ordering::SeqCst);
    }

    /// **This root sells nothing from now on** (a private instance): `GET /-/signup` answers
    /// [`NOTHING_SOLD`], as a holger with no shop does.
    pub fn sell_nothing(&self) {
        self.sells_nothing.store(true, std::sync::atomic::Ordering::SeqCst);
    }

    /// **The set doors offer one dependency set at `revision`** ([`OfferedSet`]).
    pub fn offer_set(&self, id: &str, revision: &str) {
        *self.set.lock().unwrap() = Some(OfferedSet { id: id.into(), revision: revision.into(), votes: Vec::new() });
    }

    /// **A re-pull changed the set**: its revision is `revision` now; the page still shows the old.
    pub fn repull_set(&self, revision: &str) {
        if let Some(s) = self.set.lock().unwrap().as_mut() {
            s.revision = revision.into();
        }
    }

    /// The votes the set doors took, as `(verb, revision named)`.
    pub fn set_votes(&self) -> Vec<(String, String)> {
        self.set.lock().unwrap().as_ref().map(|s| s.votes.clone()).unwrap_or_default()
    }

    /// **Every ask for the flow drawing** the console passed on (`AdminService.FlowSvg`), in
    /// order: the zoom each named (`main`, `""` for nothing open, `all`).
    pub fn flow_asks(&self) -> Vec<String> {
        self.flow.asked.lock().unwrap().clone()
    }

    /// **Refuse every ask for the flow drawing from now on** (`yes`), as a holger that is
    /// restarting answers ([`FLOW_DOWN`]); `false` passes them on again.
    pub fn refuse_flow(&self, yes: bool) {
        self.flow.refused.store(yes, std::sync::atomic::Ordering::SeqCst);
    }

    /// holger's own data root (`<tmp>/holger`).
    pub fn holger_root(&self) -> PathBuf {
        self.data.path().join("holger")
    }

    /// `base` + `path`.
    pub fn at(&self, path: &str) -> String {
        format!("{}{path}", self.base)
    }

    /// A way back from the fake vendor (`https://localhost/…`), on this rig's console.
    pub fn back(&self, url: &str) -> String {
        url.replacen("https://localhost", &self.base, 1)
    }

    /// **What the ONE box's pulled `AddPrincipal` makes on the box** once a paid sign-up's
    /// account exists (holger `fleet_box::declare_sold`): the sold record names the account's
    /// repository and the vendor's session, nobody's yet. The pull channel is not part of the
    /// browser's flow, so the rig writes the record the box writes.
    pub fn the_box_makes_the_account(&self, account: &str, session: &str) {
        holger_server_lib::sold::update(&self.holger_root(), |sold| {
            sold.insert(account.to_owned(), holger_server_lib::sold::SoldRepository { template: "bundles".into(), signup: session.to_owned(), private: true, ..Default::default() });
            Ok(())
        })
        .expect("the sold record");
    }

    /// One ask of holger's HTTP door with the console's bearer: `(status, body)`.
    pub fn console_post(&self, path_and_query: &str, body: Option<serde_json::Value>) -> (u16, String) {
        let agent = ureq::Agent::config_builder().http_status_as_error(false).build().new_agent();
        let mut res = match body {
            Some(b) => agent.post(&format!("{}{path_and_query}", self.door)).header("authorization", &format!("Bearer {CONSOLE}")).header("content-type", "application/json").send(b.to_string()),
            None => agent.post(&format!("{}{path_and_query}", self.door)).header("authorization", &format!("Bearer {CONSOLE}")).send_empty(),
        }
        .expect("holger's door answers");
        let status = res.status().as_u16();
        (status, res.body_mut().read_to_string().unwrap_or_default())
    }

    /// One GET of holger's HTTP door with the console's bearer: `(status, body)`.
    pub fn console_get(&self, path_and_query: &str) -> (u16, String) {
        let agent = ureq::Agent::config_builder().http_status_as_error(false).build().new_agent();
        let mut res = agent.get(&format!("{}{path_and_query}", self.door)).header("authorization", &format!("Bearer {CONSOLE}")).call().expect("holger's door answers");
        let status = res.status().as_u16();
        (status, res.body_mut().read_to_string().unwrap_or_default())
    }

    /// **An invite into `group`**, minted with the console's bearer (the admin's invite page
    /// stands in): the secret a `/login?invite=` link carries.
    pub fn invite(&self, display: &str, group: &str) -> String {
        let (status, body) = self.console_post("/-/auth/passkey/invites", Some(serde_json::json!({ "display": display, "groups": [group] })));
        assert_eq!(status, 200, "the console mints an invite: {body}");
        let v: serde_json::Value = serde_json::from_str(&body).unwrap();
        v["invite"].as_str().expect("the invite secret").to_owned()
    }
}

async fn wait_for(url: &str) {
    for _ in 0..400 {
        let url = url.to_owned();
        let ok = tokio::task::spawn_blocking(move || ureq::get(&url).call().map(|r| r.status().as_u16() == 200).unwrap_or(false)).await.unwrap_or(false);
        if ok {
            return;
        }
        tokio::time::sleep(std::time::Duration::from_millis(25)).await;
    }
    panic!("{url} never answered");
}

/// A small but well-formed wheel (stored-only zip with METADATA and WHEEL), written to `dir`.
pub fn write_wheel(dir: &Path) -> PathBuf {
    fn crc32(data: &[u8]) -> u32 {
        let mut crc = 0xFFFF_FFFFu32;
        for b in data {
            crc ^= *b as u32;
            for _ in 0..8 {
                crc = if crc & 1 != 0 { (crc >> 1) ^ 0xEDB8_8320 } else { crc >> 1 };
            }
        }
        !crc
    }
    fn member(out: &mut Vec<u8>, central: &mut Vec<u8>, name: &str, body: &[u8]) {
        let crc = crc32(body);
        let offset = out.len() as u32;
        out.extend_from_slice(&0x0403_4b50u32.to_le_bytes());
        out.extend_from_slice(&[20, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
        out.extend_from_slice(&crc.to_le_bytes());
        out.extend_from_slice(&(body.len() as u32).to_le_bytes());
        out.extend_from_slice(&(body.len() as u32).to_le_bytes());
        out.extend_from_slice(&(name.len() as u16).to_le_bytes());
        out.extend_from_slice(&0u16.to_le_bytes());
        out.extend_from_slice(name.as_bytes());
        out.extend_from_slice(body);
        central.extend_from_slice(&0x0201_4b50u32.to_le_bytes());
        central.extend_from_slice(&[20, 0, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
        central.extend_from_slice(&crc.to_le_bytes());
        central.extend_from_slice(&(body.len() as u32).to_le_bytes());
        central.extend_from_slice(&(body.len() as u32).to_le_bytes());
        central.extend_from_slice(&(name.len() as u16).to_le_bytes());
        central.extend_from_slice(&[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
        central.extend_from_slice(&offset.to_le_bytes());
        central.extend_from_slice(name.as_bytes());
    }
    let mut out = Vec::new();
    let mut central = Vec::new();
    member(&mut out, &mut central, "demo_pkg/__init__.py", b"__version__ = \"1.0.0\"\n");
    member(&mut out, &mut central, "demo_pkg-1.0.0.dist-info/METADATA", b"Metadata-Version: 2.1\nName: demo-pkg\nVersion: 1.0.0\nLicense-Expression: MIT\n");
    member(&mut out, &mut central, "demo_pkg-1.0.0.dist-info/WHEEL", b"Wheel-Version: 1.0\nGenerator: holger-test\nRoot-Is-Purelib: true\nTag: py3-none-any\n");
    let cd_offset = out.len() as u32;
    let cd_len = central.len() as u32;
    out.extend_from_slice(&central);
    out.extend_from_slice(&0x0605_4b50u32.to_le_bytes());
    out.extend_from_slice(&[0, 0, 0, 0, 3, 0, 3, 0]);
    out.extend_from_slice(&cd_len.to_le_bytes());
    out.extend_from_slice(&cd_offset.to_le_bytes());
    out.extend_from_slice(&0u16.to_le_bytes());
    let path = dir.join(WHEEL);
    std::fs::write(&path, out).unwrap();
    path
}

/// **A key file, as holger reads one** (`holger-key-1`): the console token is a bearer, so
/// holger refuses it when its group or others may read it (0600 or 0400 only).
fn write_key_file(path: &Path, contents: &str) {
    use std::os::unix::fs::{OpenOptionsExt as _, PermissionsExt as _};
    let mut f = std::fs::OpenOptions::new().write(true).create(true).truncate(true).mode(0o600).open(path).unwrap();
    // `mode` holds for a new file only: a file that was there keeps its mode, so close it too.
    f.set_permissions(std::fs::Permissions::from_mode(0o600)).unwrap();
    f.write_all(contents.as_bytes()).unwrap();
}

#[cfg(test)]
mod doors_tests {
    use super::*;

    /// ★ **The rig writes the console token as a key file, 0600** (Henrik, 2026-10-31): holger
    /// refuses a token file others can read (`holger-key-1`), so a rig that wrote it 0644 or
    /// 0664 (the umask) started no holger at all, and every browser test was red.
    #[test]
    fn the_rig_writes_the_console_token_0600() {
        use std::os::unix::fs::PermissionsExt as _;
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join("token");
        write_key_file(&path, "secret");
        assert_eq!(std::fs::metadata(&path).unwrap().permissions().mode() & 0o777, 0o600);
        // Written again over a file that was too open, it is closed again.
        std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o664)).unwrap();
        write_key_file(&path, "secret");
        assert_eq!(std::fs::metadata(&path).unwrap().permissions().mode() & 0o777, 0o600);
        assert_eq!(std::fs::read_to_string(&path).unwrap(), "secret");
    }

    /// ★ HUI-PORT: **every door the rig declares is a port it holds** until holger opens it. The
    /// replication door used to be left to holger's default (the gRPC port + 1), a port the rig
    /// never held, and a listener elsewhere could take it first: "the replication door cannot
    /// open on 127.0.0.1:<port>: Address already in use … REFUSING TO START".
    #[test]
    fn the_rig_declares_only_doors_it_holds() {
        let doors = Doors::hold();
        let ron = doors.declare(&estate_ron::medium());
        let (grpc, http, replication) = (doors.grpc, doors.http, doors.replication);
        assert!(ron.contains(&format!("\"127.0.0.1:{grpc}\"")) && ron.contains(&format!("\"127.0.0.1:{http}\"")), "the gRPC and HTTP doors are the held ports");
        assert!(ron.contains(&format!("replication_door: Some((ron_bind: Some(\"127.0.0.1:{replication}\")")), "the replication door is declared at a held port, not left at gRPC + 1:\n{ron}");
        for (door, port) in [("gRPC", grpc), ("HTTP", http), ("replication", replication)] {
            assert!(std::net::TcpListener::bind(("127.0.0.1", port)).is_err(), "the {door} door's port {port} is not held by the rig");
        }
        doors.release();
    }
}