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

nordisk/gunnar

crates/gunnar-front/src/console.rs

//! **The web face of the estate — the gunnar console, served by vm1 itself.**
//!
//! The console is two halves of one Rust program (gunnar-ui): the BFF,
//! `gunnar-ui-server`, linked here as a library, and the browser half,
//! gunnar-ui-web's built `dist/` (Leptos → wasm), embedded in this binary at
//! build time (`build.rs`) and unpacked to `<data>/console/dist` at boot. Its
//! router is mounted on the front's own :443 ([`crate::web`]).
//!
//! **What the console makes itself and OFFERS the installer** (every visit):
//!
//! | offer | value | who takes it |
//! |---|---|---|
//! | `console_pubkey` | the console's own key (OpenSSH line) — it signs gunnar calls for logged-in people | the primary: principal `gunnar-ui`, machine, gunnar-admin |
//! | `guest_pubkey` | the console's GUEST key — anonymous visitors' reads | the primary: principal `gunnar-guest`, machine, NO grant |
//! | `operator_console_pubkey` | the console's credential for the estate's OPERATOR — his reads and the attestation of every SSH key he adds | the primary: the operator's account (fed `operator_name`), its FIRST key |
//!
//! The keys are minted here at first boot (`<data>/console/id`, `…/guest`,
//! `…/operator`, 0600) and kept across installations, like the clock key. The
//! operator's is also laid as the console's tenant SEED for that account
//! (`<data>/console/seeds/<operator>`, [`lay_operator_seed`]) once the feed
//! names him, so the console adopts the key the install enrolled instead of
//! minting one gunnar would want an attestation for (2026-10-04).
//!
//! **What it takes from the feed**: where the primary's control plane is
//! (`primary_utility`, else the fed peer of role primary, port 50051) and the
//! twin's (for the one-step restore); the web names ([`crate::web::WebConfig`],
//! the passkey relying party is the first); and optionally
//! `console_admin_token`, the break-glass bearer that mints the first
//! passkey — refused by name if it is guessable. When the primary moves (a
//! new feed names another), the console is rebuilt against it and swapped in
//! LIVE; the passkey store is opened once and kept.

use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::Duration;

use nordisk_pairing::join::JoinReply;
use nordisk_pairing::Role;

mod embedded {
    include!(concat!(env!("OUT_DIR"), "/console_dist.rs"));
}
pub use embedded::{DIST_SOURCE, FILES};

/// Under the data root: the console's own key (a gunnar admin, via the
/// `gunnar-ui` principal).
pub const IDENTITY_REL: &str = "console/id";
/// Under the data root: the guest key (no grant at all).
pub const GUEST_REL: &str = "console/guest";
/// Under the data root: the console's credential for the estate's OPERATOR
/// (offered as [`OFFER_OPERATOR_CONSOLE_PUBKEY`], enrolled at install on his
/// account as its first key).
pub const OPERATOR_REL: &str = "console/operator";
/// Under the data root: the console's tenant seeds, one private key per
/// principal id (`gunnar_ui_server::tenant::TenantKeys::with_seeds`) — the
/// operator's, laid from [`OPERATOR_REL`] by [`lay_operator_seed`].
pub const SEEDS_REL: &str = "console/seeds";
/// Under the data root: the passkey/session store.
pub const STATE_REL: &str = "console/state.redb";
/// Under the data root: where the embedded `dist/` is unpacked.
pub const DIST_REL: &str = "console/dist";

/// The offer key of the console's own key.
pub const OFFER_CONSOLE_PUBKEY: &str = gunnar_server::fed_principals::OFFER_CONSOLE_PUBKEY;
/// The offer key of the guest key.
pub const OFFER_GUEST_PUBKEY: &str = gunnar_server::fed_principals::OFFER_GUEST_PUBKEY;
/// The offer key of the console's credential for the operator.
pub const OFFER_OPERATOR_CONSOLE_PUBKEY: &str =
    gunnar_server::fed_principals::OFFER_OPERATOR_CONSOLE_PUBKEY;
/// The feed key of the break-glass bearer.
pub const FEED_ADMIN_TOKEN: &str = "console_admin_token";

/// **The operator's own address for an order's receipt** (g-receipts): fed by
/// `estate install`, handed to the console's monetize handle, named as the
/// payer of every order the OPERATOR places (`Monetize::payer_for`) so Stripe
/// mails its receipt. A tenant's order names nothing and monetize names the
/// address they signed up with.
pub const FEED_RECEIPTS_TO: &str = "console_receipts_to";

/// **The monetize product this console sells** (G15, 5.4): fed by `estate
/// install` (`console_product`), handed to the console as
/// [`ENV_CONSOLE_PRODUCT`] — `gunnar` on every estate (gunnar.rs and test2 alike):
/// the ONE monetize tells the sites apart by each estate's own bearer (UW-1).
pub const FEED_CONSOLE_PRODUCT: &str = "console_product";
/// What gunnar-ui reads its product id from (`gunnar_ui_server::monetize::ENV_PRODUCT`).
pub const ENV_CONSOLE_PRODUCT: &str = "GUNNAR_UI_PRODUCT";
/// The product of a console the feed names none for: gunnar.rs's own.
pub const CONSOLE_PRODUCT_DEFAULT: &str = "gunnar";

/// **A SOLD private Gunnar's console knows what it is** (private ownership
/// (b), 2026-10-07): the kit feeds vm1 the estate's name and its seller's
/// site; the console reads them from its environment ([`ENV_INSTANCE`],
/// [`ENV_PROVIDER`]) — it says "<company>'s Gunnar", and its "new instance"
/// goes to the seller's `/signup?plan=private` (its own monetize sells none).
pub const FEED_CONSOLE_INSTANCE: &str = "console_instance";
/// The seller's site, `https://<seller apex>` ([`FEED_CONSOLE_INSTANCE`]).
pub const FEED_CONSOLE_PROVIDER: &str = "console_provider";
/// **The public site's switch, as the ONE box feeds it** (TASKS-12, owner
/// 2026-10-25): `on` only for an operator site whose declaration says
/// `[estates.<label>] public_site = true` (gunnar.rs). Everything else, every
/// sold estate first, leaves the console's public site OFF (the airgap default).
pub const FEED_PUBLIC_SITE: &str = "console_public_site";
/// gunnar-ui's switch (`gunnar_public_site::ENV_SWITCH`): OFF unless `on`.
pub const ENV_PUBLIC_SITE: &str = "GUNNAR_UI_PUBLIC_SITE";

/// Pure: whether the feed turns the console's public site on.
pub fn public_site_of(feed: &BTreeMap<String, String>) -> bool {
    feed.get(FEED_PUBLIC_SITE)
        .is_some_and(|v| v.trim().eq_ignore_ascii_case("on"))
}

/// The console's own name for the estate (gunnar-ui `GUNNAR_UI_INSTANCE`).
pub const ENV_INSTANCE: &str = "GUNNAR_UI_INSTANCE";
/// The console's own name for its seller (gunnar-ui `GUNNAR_UI_PROVIDER`).
pub const ENV_PROVIDER: &str = "GUNNAR_UI_PROVIDER";

/// Pure: **the console's environment for a sold estate**, from the feed —
/// `(ENV_INSTANCE, <company>)` and `(ENV_PROVIDER, https://<seller>)`; empty
/// for the kit's own estates. The company is the kit's instance rule (a
/// lowercase letter, then letters and digits, at most 16); the seller an
/// `https://` site with no path. Anything else is refused by name.
pub fn sold_by(feed: &BTreeMap<String, String>) -> Result<Vec<(&'static str, String)>, String> {
    let get = |k: &str| {
        feed.get(k)
            .map(|v| v.trim().to_string())
            .filter(|v| !v.is_empty())
    };
    let (instance, seller) = match (get(FEED_CONSOLE_INSTANCE), get(FEED_CONSOLE_PROVIDER)) {
        (None, None) => return Ok(Vec::new()),
        (Some(i), Some(s)) => (i, s),
        _ => return Err(format!("REFUSED [console-sold-by] `{FEED_CONSOLE_INSTANCE}` and `{FEED_CONSOLE_PROVIDER}` come together: a sold estate names both")),
    };
    let named = instance.len() <= 16
        && instance.starts_with(|c: char| c.is_ascii_lowercase())
        && instance
            .chars()
            .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit());
    if !named {
        return Err(format!("REFUSED [console-sold-by] the fed `{FEED_CONSOLE_INSTANCE}` {instance:?} is not an instance's name (a lowercase letter, then letters and digits, at most 16)"));
    }
    let site = seller.trim_end_matches('/');
    let host = site.strip_prefix("https://").unwrap_or("");
    if host.is_empty()
        || !host
            .chars()
            .all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '-' || c == ':')
    {
        return Err(format!("REFUSED [console-sold-by] the fed `{FEED_CONSOLE_PROVIDER}` {seller:?} is not a seller's site (https://<host>, no path)"));
    }
    Ok(vec![
        (ENV_INSTANCE, instance),
        (ENV_PROVIDER, site.to_string()),
    ])
}

/// **What the console is told it is, given the feed and what it was told
/// before** ([`sold_by`]; wiring tracer B 2026-10-08). `before` is the last
/// pair applied (or remembered on the data disk across a restart).
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SoldDecision {
    /// Set exactly these (empty: the kit's own estate, nothing set).
    Apply(Vec<(&'static str, String)>),
    /// Keep what is applied: the feed lost or garbled a SOLD estate's pair.
    Keep(String),
    /// Build no console: a sold estate's pair is unreadable and nothing good
    /// was ever applied, so a console now would sell over a free monetize.
    Refuse(String),
}

/// Pure: [`SoldDecision`]. **Fails closed**: a SOLD estate's monetize is
/// free (`plugin.gunnar.free`), so a console that forgot it is sold would
/// offer purchases a free monetize grants for nothing.
pub fn sold_decision(
    feed: &BTreeMap<String, String>,
    before: Option<&[(&'static str, String)]>,
) -> SoldDecision {
    let was_sold = before.is_some_and(|b| !b.is_empty());
    match sold_by(feed) {
        // The feed says nothing of a seller, but this console was SOLD: a sold
        // estate never becomes a seller (an upgrade or a recharge run without
        // the order lost the pair) — keep it.
        Ok(now) if now.is_empty() && was_sold => SoldDecision::Keep(format!(
            "REFUSED [console-sold-by-dropped] this front's feed no longer names `{FEED_CONSOLE_INSTANCE}` / `{FEED_CONSOLE_PROVIDER}`, but its console was a SOLD estate's: it stays sold (an estate the shop laid never sells); feed the pair again"
        )),
        Ok(now) => SoldDecision::Apply(now),
        Err(why) if was_sold => SoldDecision::Keep(format!("{why}; the console keeps the pair it was told before")),
        Err(why) => SoldDecision::Refuse(why),
    }
}

/// **Hand the console what the feed says it is** ([`sold_decision`]) — its
/// environment, read by gunnar-ui before it first answers. Set (or cleared)
/// only when it changed. `false`: the console must not be built (a sold
/// estate's pair is unreadable and nothing good was applied before).
fn apply_sold_by(
    root: &Path,
    feed: &BTreeMap<String, String>,
    applied: &mut Option<Vec<(&'static str, String)>>,
) -> bool {
    if applied.is_none() {
        if let Some(remembered) = remembered_sold_by(root) {
            *applied = Some(remembered);
        }
    }
    let now = match sold_decision(feed, applied.as_deref()) {
        SoldDecision::Apply(now) => now,
        SoldDecision::Keep(why) => {
            tracing::error!("{why}");
            match applied.clone() {
                Some(kept) => kept,
                None => return false,
            }
        }
        SoldDecision::Refuse(why) => {
            tracing::error!("{why}: no console is built until the feed names a sold estate's pair it can read (a console now would sell over a free monetize)");
            return false;
        }
    };
    let changed = applied.as_ref() != Some(&now);
    for k in [ENV_INSTANCE, ENV_PROVIDER] {
        match now.iter().find(|(n, _)| *n == k) {
            Some((_, v)) => std::env::set_var(k, v),
            None => std::env::remove_var(k),
        }
    }
    if changed {
        if let Some((_, i)) = now.iter().find(|(n, _)| *n == ENV_INSTANCE) {
            tracing::info!(instance = %i, seller = %now.iter().find(|(n, _)| *n == ENV_PROVIDER).map(|(_, v)| v.as_str()).unwrap_or(""), "console: a sold private Gunnar — the console is told its name and its seller");
            if let Err(e) = remember_sold_by(root, &now) {
                tracing::error!(error = %format!("{e:#}"), "console: the sold-by pair cannot be remembered on the data disk");
            }
        }
    }
    *applied = Some(now);
    true
}

/// Under the data root: the last sold-by pair applied (wiring tracer B 2026-10-08).
pub const SOLD_BY_REL: &str = "console/sold-by";

/// **The sold-by pair this front applied last**, from the data disk (two
/// lines: the instance, the seller); `None` when none was ever applied, or
/// what is there is not a pair [`sold_by`] takes.
pub fn remembered_sold_by(root: &Path) -> Option<Vec<(&'static str, String)>> {
    let text = std::fs::read_to_string(root.join(SOLD_BY_REL)).ok()?;
    let mut lines = text.lines().map(str::trim);
    let feed: BTreeMap<String, String> = [
        (FEED_CONSOLE_INSTANCE.to_string(), lines.next()?.to_string()),
        (FEED_CONSOLE_PROVIDER.to_string(), lines.next()?.to_string()),
    ]
    .into_iter()
    .collect();
    sold_by(&feed).ok().filter(|p| !p.is_empty())
}

/// Remember the applied sold-by pair on the data disk (an upgrade in place
/// keeps /data), so a restart with a feed that lost it stays sold.
pub fn remember_sold_by(root: &Path, applied: &[(&'static str, String)]) -> anyhow::Result<()> {
    let get = |k: &str| {
        applied
            .iter()
            .find(|(n, _)| *n == k)
            .map(|(_, v)| v.as_str())
            .unwrap_or("")
    };
    let path = root.join(SOLD_BY_REL);
    if let Some(dir) = path.parent() {
        std::fs::create_dir_all(dir)?;
    }
    std::fs::write(
        &path,
        format!("{}\n{}\n", get(ENV_INSTANCE), get(ENV_PROVIDER)),
    )?;
    Ok(())
}

/// **The estate's own bearer at the ONE monetize** — a sealed secret slot (`secret.*`, vm1's
/// alone, sealed at rest like every fed secret): what the console presents on its calls to the
/// ONE box's edge (Signup, EstateOrder, Purchase, Products), so the box knows which SITE a
/// signup comes from and makes the account on THIS estate. The kit feeds the bearer the
/// estate's attach enrolment was handed; the door it is presented at is [`FEED_FLEET`].
pub const FEED_MONETIZE_BEARER: &str = "secret.monetize_bearer";
/// The ONE monetize's door (`https://monetize.ignalina.dk`): gunnar-server's `fleet` feed key.
pub const FEED_FLEET: &str = gunnar_server::feed::FEED_FLEET;

/// **Where the console sells, and with what**: the ONE monetize's door and the estate's own
/// bearer there. Never rendered with the bearer.
#[derive(Clone, PartialEq, Eq)]
pub struct MonetizeDoor {
    pub addr: String,
    pub bearer: String,
    /// The mock's seam (`fleet_at`, a bench `ip:port`): where the door is dialled, its name
    /// still what its certificate is checked under. `None`: the door itself.
    pub at: Option<String>,
    /// The bench's CA for the door's certificate (`fleet_ca`), beside `at` only.
    pub ca_pem: Option<String>,
}

impl std::fmt::Debug for MonetizeDoor {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("MonetizeDoor")
            .field("addr", &self.addr)
            .finish_non_exhaustive()
    }
}

/// Pure: **the console's monetize door, from the feed** — [`FEED_FLEET`] (plain, or the first
/// VM's offer) and [`FEED_MONETIZE_BEARER`]. No bearer: `None`, the console sells nothing (a
/// sold estate, the open build). A bearer with no door to present it at is refused by name.
pub fn monetize_door(feed: &BTreeMap<String, String>) -> Result<Option<MonetizeDoor>, String> {
    let Some(bearer) = feed
        .get(FEED_MONETIZE_BEARER)
        .map(|b| b.trim().to_string())
        .filter(|b| !b.is_empty())
    else {
        return Ok(None);
    };
    let fed = gunnar_server::feed::Fed::from_feed(feed);
    if let Some(why) =
        gunnar_server::feed::seam_refusal(fed.fleet_at.as_deref(), fed.fleet_ca.is_some())
    {
        return Err(why);
    }
    match fed.fleet {
        Some(addr) => Ok(Some(MonetizeDoor { addr, bearer, at: fed.fleet_at, ca_pem: fed.fleet_ca })),
        None => Err(format!(
            "REFUSED [console-monetize-no-door] the feed carries `{FEED_MONETIZE_BEARER}` and no `{FEED_FLEET}`: the estate's bearer has no ONE monetize to be presented at; the console sells nothing"
        )),
    }
}

/// The fed receipt address, if any — pure.
pub fn receipts_to(feed: &BTreeMap<String, String>) -> Option<String> {
    feed.get(FEED_RECEIPTS_TO)
        .map(|t| t.trim().to_string())
        .filter(|t| !t.is_empty())
}

/// The fed product id of this console — pure. Unfed or blank is
/// [`CONSOLE_PRODUCT_DEFAULT`]; anything else must be a monetize product id
/// (`^[a-z][a-z0-9-]{0,62}$`, gunnar-ui's own rule), else `Err` naming the key.
pub fn console_product(feed: &BTreeMap<String, String>) -> Result<String, String> {
    let id = feed
        .get(FEED_CONSOLE_PRODUCT)
        .map(|t| t.trim())
        .unwrap_or_default();
    if id.is_empty() {
        return Ok(CONSOLE_PRODUCT_DEFAULT.to_string());
    }
    let mut c = id.chars();
    let first = c.next().is_some_and(|c| c.is_ascii_lowercase());
    if !first
        || id.len() > 63
        || !c.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')
    {
        return Err(format!(
            "REFUSED [console-product] the feed's {FEED_CONSOLE_PRODUCT} = {id:?} is not a monetize product id (a-z first, then a-z 0-9 -, at most 63)"
        ));
    }
    Ok(id.to_string())
}
/// gunnar's control plane, on every box's utility address.
pub const CONTROL_PORT: u16 = 50051;

/// How often the feed is looked at for a moved primary.
const POLL: Duration = Duration::from_secs(2);

/// **An ed25519 key under `rel`**, minted (0600) if absent; answers its
/// public OpenSSH line with `comment`.
pub fn key_line(root: &Path, rel: &str, comment: &str) -> anyhow::Result<String> {
    // ONE writer, and the key ON DISK is the answer (release-3: the certify
    // key's mint race, crate::keys::certify_key — the same shape here).
    static MINT: std::sync::Mutex<()> = std::sync::Mutex::new(());
    let _one = MINT.lock().unwrap_or_else(|p| p.into_inner());
    let path = root.join(rel);
    if !std::fs::read(&path).map(|b| !b.is_empty()).unwrap_or(false) {
        let pem = gunnar_core::mint_ed25519()?;
        gunnar_server::pairing::write_secret(root, rel, std::str::from_utf8(&pem)?)?;
    }
    let pem = std::fs::read(&path).map_err(|e| anyhow::anyhow!("{}: {e}", path.display()))?;
    let key = gunnar_core::ssh_key::PrivateKey::from_openssh(&pem)
        .map_err(|e| anyhow::anyhow!("{}: {e}", path.display()))?;
    let mut line = key
        .public_key()
        .to_openssh()
        .map_err(|e| anyhow::anyhow!("{e}"))?;
    line.push(' ');
    line.push_str(comment);
    Ok(line)
}

/// **What the console offers**: its own key, its guest key and its credential
/// for the operator, minted at the first ask. Public values only.
pub fn offers(root: &Path) -> BTreeMap<String, String> {
    let mut o = BTreeMap::new();
    for (key, rel, comment) in [
        (OFFER_CONSOLE_PUBKEY, IDENTITY_REL, "gunnar-ui"),
        (OFFER_GUEST_PUBKEY, GUEST_REL, "gunnar-guest"),
        (
            OFFER_OPERATOR_CONSOLE_PUBKEY,
            OPERATOR_REL,
            gunnar_server::fed_principals::OPERATOR_CONSOLE_COMMENT,
        ),
    ] {
        match key_line(root, rel, comment) {
            Ok(line) => {
                o.insert(key.to_string(), line);
            }
            Err(e) => {
                tracing::error!(error = %format!("{e:#}"), key, "console: a key cannot be minted — not offered")
            }
        }
    }
    o
}

/// ★ **Lay the operator's tenant seed** (2026-10-04): when the feed names the
/// estate's operator (`operator_name`), the console's credential for him
/// ([`OPERATOR_REL`], the key offered as [`OFFER_OPERATOR_CONSOLE_PUBKEY`]) is
/// written as `<data>/console/seeds/<operator>` (0600), the file the console's
/// `TenantKeys` looks a principal's seed up by. `Ok(None)` when nothing names an
/// operator.
///
/// A name that cannot be a plain file name is refused (`TenantKeys` would never
/// look it up anyway). A seed laid for an operator the feed no longer names is
/// removed: the console must not hold a credential for an account nobody named.
pub fn lay_operator_seed(
    root: &Path,
    feed: &BTreeMap<String, String>,
) -> anyhow::Result<Option<String>> {
    let Some(operator) = gunnar_server::fed_principals::operator_of(feed) else {
        return Ok(None);
    };
    if operator.starts_with('.')
        || !operator
            .bytes()
            .all(|b| b.is_ascii_alphanumeric() || b == b'-' || b == b'_' || b == b'.')
    {
        anyhow::bail!("REFUSED [console-operator-name] the fed `{}` {operator:?} is not a plain account name; no seed is laid for it", gunnar_server::fed_principals::FEED_OPERATOR);
    }
    key_line(
        root,
        OPERATOR_REL,
        gunnar_server::fed_principals::OPERATOR_CONSOLE_COMMENT,
    )?;
    let pem = std::fs::read_to_string(root.join(OPERATOR_REL))
        .map_err(|e| anyhow::anyhow!("{}: {e}", root.join(OPERATOR_REL).display()))?;
    let rel = format!("{SEEDS_REL}/{operator}");
    if std::fs::read_to_string(root.join(&rel)).ok().as_deref() != Some(pem.as_str()) {
        gunnar_server::pairing::write_secret(root, &rel, &pem)?;
        tracing::info!(operator = %operator, "console: the credential for the operator is laid as his tenant seed — the console adopts the key the install enrolled");
    }
    if let Ok(dir) = std::fs::read_dir(root.join(SEEDS_REL)) {
        for entry in dir.flatten() {
            if entry.file_name().to_str() != Some(operator.as_str()) && entry.path().is_file() {
                let _ = std::fs::remove_file(entry.path());
                tracing::info!(seed = ?entry.file_name(), "console: a seed for an account the feed no longer names as the operator is removed");
            }
        }
    }
    {
        use std::os::unix::fs::PermissionsExt as _;
        let _ =
            std::fs::set_permissions(root.join(SEEDS_REL), std::fs::Permissions::from_mode(0o700));
    }
    Ok(Some(operator))
}

/// **Where the console dials**, from the fed reply — pure.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Target {
    /// The primary's control plane, `<overlay>:50051` (over the tunnel).
    pub control: String,
    /// The twin's, when a twin with a utility address is fed.
    pub twin: Option<String>,
    /// The elected primary's NAME (the verdict, as the peers carry it) — whose
    /// `offer.<name>.*` is the primary's own (the ticket key).
    pub primary: Option<String>,
}

/// What the console last served: target, (web name = the relying party, canonical name = its
/// origin; WWW-CANON), admin token, receipts address, product, the ONE monetize's door with the
/// estate's bearer, and its public site's switch.
type Served = (
    Target,
    (String, String),
    Option<String>,
    Option<String>,
    String,
    Option<MonetizeDoor>,
    bool,
);

pub fn target(reply: &JoinReply) -> Option<Target> {
    // Over the tunnel (WireGuard-only): the boxes' overlay addresses.
    let primary = crate::primary_overlay(reply)?;
    let twin = crate::twin_overlay(reply).map(|ip| format!("{ip}:{CONTROL_PORT}"));
    let name = reply
        .peers
        .iter()
        .find(|p| p.role == Role::Primary)
        .map(|p| p.name.clone());
    Some(Target {
        control: format!("{primary}:{CONTROL_PORT}"),
        twin,
        primary: name,
    })
}

/// The fed break-glass bearer, if any — refused by name when it is guessable.
pub fn admin_token(feed: &BTreeMap<String, String>) -> Result<Option<String>, String> {
    let Some(t) = feed
        .get(FEED_ADMIN_TOKEN)
        .map(|t| t.trim().to_string())
        .filter(|t| !t.is_empty())
    else {
        return Ok(None);
    };
    match gunnar_ui_server::auth::middleware::weak_admin_token(&t) {
        Some(why) => Err(format!("REFUSED [console-admin-token-weak] the fed `{FEED_ADMIN_TOKEN}` {why}; the console starts without a break-glass bearer")),
        None => Ok(Some(t)),
    }
}

/// **Unpack the embedded `dist/`** to `<data>/console/dist` (a file is only
/// rewritten when it differs; a file the build no longer carries is removed,
/// so `index.html` never names a stylesheet of another build). `None` when
/// this binary carries no console.
pub fn unpack(root: &Path) -> anyhow::Result<Option<PathBuf>> {
    if FILES.is_empty() {
        return Ok(None);
    }
    let dir = root.join(DIST_REL);
    std::fs::create_dir_all(&dir)?;
    let mut wanted = std::collections::BTreeSet::new();
    for (rel, bytes) in FILES {
        let p = dir.join(rel);
        wanted.insert(p.clone());
        if std::fs::read(&p).map(|b| b == *bytes).unwrap_or(false) {
            continue;
        }
        if let Some(parent) = p.parent() {
            std::fs::create_dir_all(parent)?;
        }
        let tmp = p.with_extension("unpacking");
        std::fs::write(&tmp, bytes)?;
        std::fs::rename(&tmp, &p)?;
    }
    for e in std::fs::read_dir(&dir)?.flatten() {
        if e.path().is_file() && !wanted.contains(&e.path()) {
            let _ = std::fs::remove_file(e.path());
        }
    }
    Ok(Some(dir))
}

/// **The console, opened once**: its keys, its passkey store and its files.
pub struct Console {
    root: PathBuf,
    store: gunnar_ui_server::state::StateStore,
    dist: Option<PathBuf>,
}

impl Console {
    pub fn open(root: &Path) -> anyhow::Result<Console> {
        let _ = offers(root);
        let state = root.join(STATE_REL);
        if let Some(parent) = state.parent() {
            gunnar_ui_server::state::create_private_dir(parent)?;
        }
        let store = gunnar_ui_server::state::StateStore::open(&state)
            .map_err(|e| anyhow::anyhow!("{}: {e}", state.display()))?;
        Ok(Console {
            root: root.to_path_buf(),
            store,
            dist: unpack(root)?,
        })
    }

    /// **The console's router against `target`**, its passkeys bound to
    /// `https://<host>`. Must run inside the tokio runtime (the event feed
    /// subscribes at once).
    pub fn router(
        &self,
        target: &Target,
        host: &str,
        admin_token: Option<String>,
        receipts_to: Option<&str>,
    ) -> anyhow::Result<axum::Router> {
        self.serving(target, host, admin_token, receipts_to)
            .map(|(r, _)| r)
    }

    /// [`Console::router`], and the event hub its pump feeds — what
    /// [`start`] stops when it replaces this console.
    pub fn serving(
        &self,
        target: &Target,
        host: &str,
        admin_token: Option<String>,
        receipts_to: Option<&str>,
    ) -> anyhow::Result<(axum::Router, gunnar_ui_server::sse::EventHub)> {
        self.serving_selling(
            target,
            host,
            admin_token,
            receipts_to,
            None,
            CONSOLE_PRODUCT_DEFAULT,
        )
    }

    /// [`Console::serving`], selling `product` through the ONE monetize at `door` with the
    /// estate's own bearer ([`monetize_door`]); `None` sells nothing.
    pub fn serving_selling(
        &self,
        target: &Target,
        host: &str,
        admin_token: Option<String>,
        receipts_to: Option<&str>,
        door: Option<&MonetizeDoor>,
        product: &str,
    ) -> anyhow::Result<(axum::Router, gunnar_ui_server::sse::EventHub)> {
        self.serving_selling_at(target, host, host, admin_token, receipts_to, door, product)
    }

    /// [`Console::serving_selling`] with its passkeys' relying party `rp_id` (the bare site) asked
    /// at `https://<origin_host>` (WWW-CANON: the canonical `www.<site>`, [`relying_party`]).
    #[allow(clippy::too_many_arguments)]
    pub fn serving_selling_at(
        &self,
        target: &Target,
        rp_id: &str,
        origin_host: &str,
        admin_token: Option<String>,
        receipts_to: Option<&str>,
        door: Option<&MonetizeDoor>,
        product: &str,
    ) -> anyhow::Result<(axum::Router, gunnar_ui_server::sse::EventHub)> {
        // The paid console's money half talks to the ONE monetize, not to one on this box —
        // with the estate's OWN bearer there, so the box knows which site a signup is from.
        #[cfg(feature = "metered")]
        let monetize = match door {
            Some(d) => Some(
                gunnar_ui_server::monetize::Monetize::connect_lazy_via(
                    &d.addr,
                    &d.bearer,
                    product,
                    d.at.as_deref().map(|at| (at, d.ca_pem.as_deref())),
                )
                .map_err(anyhow::Error::msg)?
                .with_receipts_to(receipts_to),
            ),
            None => None,
        };
        #[cfg(not(feature = "metered"))]
        let _ = (receipts_to, door, product);
        use gunnar_ui_server::{app::App, auth, grpc::Gunnar, signer::Signer};
        let signer = Arc::new(Signer::load(self.root.join(IDENTITY_REL))?);
        let gunnar = Gunnar::connect_lazy(&target.control, signer).map_err(anyhow::Error::msg)?;
        let guest = Gunnar::connect_lazy(
            &target.control,
            Arc::new(Signer::load(self.root.join(GUEST_REL))?),
        )
        .map_err(anyhow::Error::msg)?;
        let twin = match &target.twin {
            Some(t) => Some(
                gunnar_ui_server::api::twin::Twin::connect(&gunnar, t)
                    .map_err(anyhow::Error::msg)?,
            ),
            None => None,
        };
        let rp = relying_party(rp_id, origin_host);
        let webauthn =
            Arc::new(auth::Webauthn::new(rp, self.store.clone()).map_err(anyhow::Error::msg)?);
        // A32-14, stays a task: the console's event hub, for the console's life, ended with its runtime.
        let events = gunnar_ui_server::sse::EventHub::spawn(gunnar.clone());
        let app = App {
            webauthn: Some(webauthn),
            admin_token,
            events: events.clone(),
            #[cfg(feature = "metered")]
            monetize,
            reviewers: Arc::new(gunnar_ui_server::reviewer::ReviewerKeys::new(
                target.control.clone(),
                self.store.clone(),
            )),
            // The operator's seed (2026-10-04): the key the install enrolled on his
            // account, adopted at his first passkey instead of a fresh mint.
            tenants: Arc::new(
                gunnar_ui_server::tenant::TenantKeys::new(
                    target.control.clone(),
                    self.store.clone(),
                )
                .with_seeds(Some(self.root.join(SEEDS_REL))),
            ),
            gunnar,
            twin,
            guest: Some(guest),
            forges: Arc::new(gunnar_ui_server::forges::ForgeSessions::new()),
            forge_apps: Arc::new(gunnar_ui_server::forges::ForgeApps::from_env()),
            // gunnar-ui 053ecff (tenant import): the per-person import/sync limits, as the
            // standalone console takes them (gunnar-ui-server main.rs).
            imports: Arc::new(gunnar_ui_server::api::mirror_gate::Imports::from_env()),
            ui_dir: self.dist.clone(),
            caches: Default::default(),
            objects: Default::default(),
            readmes: Arc::new(gunnar_ui_server::readme::Cache::new(Some(
                self.store.clone(),
            ))),
        };
        Ok((gunnar_ui_server::serve::router(Arc::new(app)), events))
    }
}

/// Pure (WWW-CANON, owner 2026-10-30): **the console's relying party** — the RP ID `rp_id` (the
/// bare site, the registrable domain the passkeys were made for) and the one origin the console
/// asks them at and its origin guard takes, `https://<origin_host>` (the canonical `www.<site>`;
/// an origin of `rp_id`). Its invite links are built from that origin.
pub fn relying_party(rp_id: &str, origin_host: &str) -> gunnar_ui_server::auth::RelyingParty {
    gunnar_ui_server::auth::RelyingParty {
        rp_id: rp_id.to_string(),
        origin: format!("https://{origin_host}"),
        rp_name: "gunnar".to_string(),
    }
}

/// **The console now serving's event pump replaces the last one's.** `false`
/// when there was none. The replaced console's pump is to stop subscribing
/// to the main it was built against.
pub fn replace_pump(
    slot: &mut Option<gunnar_ui_server::sse::EventHub>,
    now: gunnar_ui_server::sse::EventHub,
) -> bool {
    match slot.replace(now) {
        Some(old) => {
            old.stop();
            true
        }
        None => false,
    }
}

/// **Run the console on `web` for the life of the process**: open it, and
/// every time the fed primary (or the names, or the bearer) changes, build it
/// against that and swap it in. Must run inside the tokio runtime.
/// **This front's certificates on the Administration page** (owner 2026-10-25): the
/// report and try now are `web::status`'s, read from this front's data disk.
struct FrontCertsOf(PathBuf);

impl gunnar_ui_server::front_certs::FrontCerts for FrontCertsOf {
    fn report(&self) -> serde_json::Value {
        crate::web::status::report_json(&self.0, std::time::SystemTime::now())
    }
    fn try_now(&self, host: &str) -> Result<String, String> {
        let words = crate::web::status::try_now(&self.0, host, std::time::SystemTime::now())?;
        tracing::warn!("console: an admin asked {host}'s certificate to be tried now");
        Ok(words)
    }
}

pub fn start(web: crate::web::Web, vm: Arc<crate::vm::FrontVm>, estate: String) {
    gunnar_ui_server::front_certs::install(Arc::new(FrontCertsOf(web.root.clone())));
    let console = match Console::open(web.root.as_path()) {
        Ok(c) => c,
        Err(e) => {
            tracing::error!(error = %format!("{e:#}"), "console: cannot open — the web face serves a refusal");
            web.app.set(crate::web::serve::placeholder("REFUSED [console-unopenable] the console's store or keys cannot be opened on this front's data disk"));
            return;
        }
    };
    if console.dist.is_none() {
        web.app.set(crate::web::serve::placeholder(
            "REFUSED [console-not-on-medium] this front medium was composed without the console's built files (gunnar-ui-web dist); compose it again with them",
        ));
        tracing::error!("console: this binary carries no built console (DIST_SOURCE = None)");
        return;
    }
    tracing::info!(dist = ?DIST_SOURCE, "console: the built console is on this medium");
    web.app.set(crate::web::serve::placeholder(
        "the console waits for the installer: no primary is known yet",
    ));
    // A32-14, stays a task: the console's follow loop on the front's runtime, async, ends with it.
    tokio::spawn(async move {
        let mut last: Option<Served> = None;
        let mut pump: Option<gunnar_ui_server::sse::EventHub> = None;
        let mut seeded: Option<Option<String>> = None;
        let mut sold: Option<Vec<(&'static str, String)>> = None;
        loop {
            if let Some(reply) = vm.latest() {
                // Before the console is built against this feed: what it is.
                // A SOLD estate's pair it cannot read builds no console (fails closed).
                if !apply_sold_by(&web.root, &reply.feed, &mut sold) {
                    web.app.set(crate::web::serve::placeholder(
                        "this Gunnar's console is not ready yet: its setup is incomplete, and the operator has been told",
                    ));
                    last = None;
                    tokio::time::sleep(POLL).await;
                    continue;
                }
                // The product it sells (G15): a bad id builds no console (fails closed).
                let product = match console_product(&reply.feed) {
                    Ok(p) => p,
                    Err(e) => {
                        tracing::error!("{e}");
                        web.app.set(crate::web::serve::placeholder(
                            "this Gunnar's console is not ready yet: its setup is incomplete, and the operator has been told",
                        ));
                        last = None;
                        tokio::time::sleep(POLL).await;
                        continue;
                    }
                };
                // The operator's tenant seed, laid once per name the feed gives.
                let named = gunnar_server::fed_principals::operator_of(&reply.feed);
                if seeded.as_ref() != Some(&named) {
                    // Said once per name: a refusal is not retried every poll.
                    if let Err(e) = lay_operator_seed(&web.root, &reply.feed) {
                        tracing::error!(error = %format!("{e:#}"), "console: the operator's tenant seed cannot be laid — his first login mints the console's credential instead");
                    }
                    seeded = Some(named);
                }
                let cfg = crate::web::WebConfig::from(&estate, &reply.feed);
                let token = match admin_token(&reply.feed) {
                    Ok(t) => t,
                    Err(e) => {
                        tracing::error!("{e}");
                        None
                    }
                };
                let receipts = receipts_to(&reply.feed);
                // Where it sells, with the estate's own bearer; a bearer with no door sells nothing.
                let door = match monetize_door(&reply.feed) {
                    Ok(d) => d,
                    Err(e) => {
                        tracing::error!("{e}");
                        None
                    }
                };
                let public = public_site_of(&reply.feed);
                if let Some(t) = target(&reply) {
                    let now = (
                        t.clone(),
                        (cfg.name().to_string(), cfg.canonical()),
                        token.clone(),
                        receipts.clone(),
                        product.clone(),
                        door.clone(),
                        public,
                    );
                    if last.as_ref() != Some(&now) {
                        // gunnar-ui reads it when the console's monetize handle is built.
                        std::env::set_var(ENV_CONSOLE_PRODUCT, &product);
                        // ...and its public site's switch when the console is built: on
                        // only where the ONE box fed it (gunnar.rs), else unset = off.
                        if public {
                            std::env::set_var(ENV_PUBLIC_SITE, "on");
                        } else {
                            std::env::remove_var(ENV_PUBLIC_SITE);
                        }
                        // WWW-CANON: the relying party is the first host, the origin the canonical www.
                        match console.serving_selling_at(
                            &t,
                            cfg.name(),
                            &cfg.canonical(),
                            token,
                            receipts.as_deref(),
                            door.as_ref(),
                            &product,
                        ) {
                            Ok((r, events)) => {
                                // `/download/…` (the signed gunnar CLI, E4) ahead of the console.
                                web.app.set(crate::downloads::serving(r));
                                let replaced = replace_pump(&mut pump, events);
                                tracing::info!(control = %t.control, twin = ?t.twin, host = cfg.name(), origin = %cfg.canonical(), replaced, sells_at = ?door.as_ref().map(|d| d.addr.as_str()), "console: serving against the primary — swapped in live");
                                last = Some(now);
                            }
                            Err(e) => {
                                tracing::error!(error = %format!("{e:#}"), "console: cannot be built against {}", t.control)
                            }
                        }
                    }
                }
            }
            tokio::time::sleep(POLL).await;
        }
    });
}

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

    /// ★ **The public site is ON only where the site's own declaration says so**
    /// (owner 2026-10-25: gunnar.rs; a sold private estate is the airgap default
    /// and is never indexed): the ONE box feeds an operator site whose
    /// `[estates.<label>] public_site = true` the key `console_public_site = on`,
    /// and only that turns the console's `GUNNAR_UI_PUBLIC_SITE` on.
    #[test]
    fn the_public_site_is_on_only_where_the_feed_says_on() {
        let feed = |pairs: &[(&str, &str)]| -> BTreeMap<String, String> {
            pairs
                .iter()
                .map(|(k, v)| (k.to_string(), v.to_string()))
                .collect()
        };
        assert_eq!(FEED_PUBLIC_SITE, "console_public_site");
        assert_eq!(ENV_PUBLIC_SITE, "GUNNAR_UI_PUBLIC_SITE");
        assert!(public_site_of(&feed(&[(FEED_PUBLIC_SITE, "on")])));
        assert!(public_site_of(&feed(&[(FEED_PUBLIC_SITE, " ON ")])));
        for off in [
            feed(&[]),
            feed(&[(FEED_PUBLIC_SITE, "off")]),
            feed(&[(FEED_PUBLIC_SITE, "")]),
            feed(&[(FEED_PUBLIC_SITE, "yes please")]),
        ] {
            assert!(!public_site_of(&off), "{off:?}");
        }
        // A sold estate's feed never carries it, and its console stays off.
        let sold = feed(&[
            (FEED_CONSOLE_INSTANCE, "acme"),
            (FEED_CONSOLE_PROVIDER, "https://gunnar.rs"),
        ]);
        assert!(!public_site_of(&sold));
    }

    /// ★ **A sold estate's console is told its name and its seller** (private
    /// ownership (b)): as `GUNNAR_UI_INSTANCE` / `GUNNAR_UI_PROVIDER`, from the
    /// kit's `console_instance` / `console_provider`; nothing for the kit's own
    /// estates; a malformed one refused by name (the console then says
    /// nothing of either).
    #[test]
    fn a_sold_estates_console_is_told_its_name_and_its_seller() {
        let feed = |pairs: &[(&str, &str)]| -> BTreeMap<String, String> {
            pairs
                .iter()
                .map(|(k, v)| (k.to_string(), v.to_string()))
                .collect()
        };
        let sold = feed(&[
            (FEED_CONSOLE_INSTANCE, " acme "),
            (FEED_CONSOLE_PROVIDER, "https://gunnar.rs/"),
        ]);
        assert_eq!(
            sold_by(&sold).unwrap(),
            vec![
                (ENV_INSTANCE, "acme".to_string()),
                (ENV_PROVIDER, "https://gunnar.rs".to_string())
            ]
        );
        assert!(
            sold_by(&BTreeMap::new()).unwrap().is_empty(),
            "the kit's own estates: nothing"
        );
        for bad in [
            feed(&[
                (FEED_CONSOLE_INSTANCE, "Acme"),
                (FEED_CONSOLE_PROVIDER, "https://gunnar.rs"),
            ]),
            feed(&[
                (FEED_CONSOLE_INSTANCE, "a-b"),
                (FEED_CONSOLE_PROVIDER, "https://gunnar.rs"),
            ]),
            feed(&[
                (FEED_CONSOLE_INSTANCE, "acme"),
                (FEED_CONSOLE_PROVIDER, "http://gunnar.rs"),
            ]),
            feed(&[
                (FEED_CONSOLE_INSTANCE, "acme"),
                (FEED_CONSOLE_PROVIDER, "https://gunnar.rs/signup"),
            ]),
            feed(&[(FEED_CONSOLE_INSTANCE, "acme")]),
            feed(&[(FEED_CONSOLE_PROVIDER, "https://gunnar.rs")]),
        ] {
            assert!(
                sold_by(&bad).unwrap_err().contains("[console-sold-by]"),
                "{bad:?}"
            );
        }
    }

    /// ★ **A sold console never starts selling** (wiring tracer B 2026-10-08):
    /// a SOLD estate's monetize is free (`plugin.gunnar.free`), so a console
    /// that forgot it is sold offers purchases a free monetize grants for
    /// nothing. A malformed pair after a good one keeps the good one; a pair
    /// that drops out of the feed after a good one keeps it too (a sold
    /// estate never becomes a seller); a malformed pair with nothing good
    /// before builds no console at all. The kit's own estates are untouched.
    #[test]
    fn a_sold_console_never_starts_selling() {
        let feed = |pairs: &[(&str, &str)]| -> BTreeMap<String, String> {
            pairs
                .iter()
                .map(|(k, v)| (k.to_string(), v.to_string()))
                .collect()
        };
        let good = feed(&[
            (FEED_CONSOLE_INSTANCE, "acme"),
            (FEED_CONSOLE_PROVIDER, "https://gunnar.rs"),
        ]);
        let garbled = feed(&[
            (FEED_CONSOLE_INSTANCE, "Acme"),
            (FEED_CONSOLE_PROVIDER, "https://gunnar.rs"),
        ]);
        let half = feed(&[(FEED_CONSOLE_INSTANCE, "acme")]);
        let applied = vec![
            (ENV_INSTANCE, "acme".to_string()),
            (ENV_PROVIDER, "https://gunnar.rs".to_string()),
        ];

        assert_eq!(
            sold_decision(&good, None),
            SoldDecision::Apply(applied.clone())
        );
        assert_eq!(
            sold_decision(&BTreeMap::new(), None),
            SoldDecision::Apply(Vec::new()),
            "the kit's own estate"
        );
        for bad in [&garbled, &half] {
            assert!(
                matches!(sold_decision(bad, None), SoldDecision::Refuse(ref why) if why.contains("[console-sold-by]")),
                "a garbled pair with nothing before builds no console: {bad:?}"
            );
            assert!(
                matches!(sold_decision(bad, Some(&applied)), SoldDecision::Keep(_)),
                "a garbled pair after a good one keeps the good one: {bad:?}"
            );
        }
        assert!(
            matches!(
                sold_decision(&BTreeMap::new(), Some(&applied)),
                SoldDecision::Keep(_)
            ),
            "a sold estate whose feed lost the pair (an upgrade without --laid) stays sold"
        );
        assert_eq!(
            sold_decision(&good, Some(&applied)),
            SoldDecision::Apply(applied.clone())
        );
    }

    /// The last good pair survives a restart of the front (an upgrade in
    /// place keeps /data): remembered under the data root, read back whole.
    #[test]
    fn the_sold_pair_is_remembered_on_the_data_disk() {
        let dir = tempfile::tempdir().unwrap();
        assert_eq!(
            remembered_sold_by(dir.path()),
            None,
            "nothing remembered on a fresh disk"
        );
        let applied = vec![
            (ENV_INSTANCE, "acme".to_string()),
            (ENV_PROVIDER, "https://gunnar.rs".to_string()),
        ];
        remember_sold_by(dir.path(), &applied).unwrap();
        assert_eq!(remembered_sold_by(dir.path()), Some(applied));
    }

    use nordisk_pairing::join::{Assigned, Peer};
    use std::net::Ipv4Addr;

    fn peer(name: &str, role: Role, ip: u8) -> Peer {
        Peer {
            name: name.into(),
            role,
            tunnel_pubkey: String::new(),
            overlay: Ipv4Addr::new(10, 44, 0, ip),
            endpoint: None,
            utility: Some(Ipv4Addr::new(10, 13, 8, ip)),
        }
    }

    fn reply(primary_utility: Option<Ipv4Addr>, peers: Vec<Peer>) -> JoinReply {
        JoinReply {
            estate: "gunnar.rs".into(),
            me: Assigned {
                name: "vm1".into(),
                overlay: nordisk_pairing::OVERLAY_FRONT,
            },
            cert_pem: None,
            role: Some(Role::Front),
            primary_overlay: None,
            primary_utility,
            peers,
            feed: BTreeMap::new(),
        }
    }

    /// **WireGuard-only** (owner 2026-10-04): the console — and monetize and
    /// the operator door, which dial its target — reach the primary and the
    /// twin on their OVERLAY addresses, through the front's own tun.
    #[test]
    fn the_console_dials_the_fed_primary_and_the_twin_over_the_tunnel() {
        let peers = vec![
            peer("vm1", Role::Front, 1),
            peer("vm2", Role::Primary, 2),
            peer("vm3", Role::Twin, 3),
        ];
        assert_eq!(
            target(&reply(None, peers.clone())),
            Some(Target {
                control: "10.44.0.2:50051".into(),
                twin: Some("10.44.0.3:50051".into()),
                primary: Some("vm2".into())
            })
        );
        // The door's own statement of the primary's overlay wins over the list;
        // its utility address is never dialled.
        let said = JoinReply {
            primary_overlay: Some(Ipv4Addr::new(10, 44, 0, 9)),
            ..reply(Some(Ipv4Addr::new(10, 13, 8, 9)), peers)
        };
        assert_eq!(target(&said).unwrap().control, "10.44.0.9:50051");
        assert_eq!(
            target(&reply(None, vec![peer("vm1", Role::Front, 1)])),
            None,
            "no primary yet: nothing to dial"
        );
    }

    /// The two keys are minted once (0600), offered as OpenSSH lines, never
    /// the private half, and they are exactly what the primary enrols.
    #[test]
    fn the_console_offers_its_own_key_and_its_guest_key_and_the_primary_enrols_both() {
        use std::os::unix::fs::PermissionsExt as _;
        let dir = tempfile::tempdir().unwrap();
        let o = offers(dir.path());
        assert!(
            o[OFFER_CONSOLE_PUBKEY].starts_with("ssh-ed25519 ")
                && o[OFFER_CONSOLE_PUBKEY].ends_with(" gunnar-ui")
        );
        assert!(
            o[OFFER_GUEST_PUBKEY].starts_with("ssh-ed25519 ")
                && o[OFFER_GUEST_PUBKEY].ends_with(" gunnar-guest")
        );
        assert_ne!(
            o[OFFER_CONSOLE_PUBKEY], o[OFFER_GUEST_PUBKEY],
            "the guest key must never be the admin key"
        );
        assert_eq!(offers(dir.path()), o, "minted once");
        assert!(o.values().all(|v| !v.contains("PRIVATE")));
        for rel in [IDENTITY_REL, GUEST_REL] {
            assert_eq!(
                std::fs::metadata(dir.path().join(rel))
                    .unwrap()
                    .permissions()
                    .mode()
                    & 0o777,
                0o600,
                "{rel}"
            );
        }
        // The console loads what was minted.
        gunnar_ui_server::signer::Signer::load(dir.path().join(IDENTITY_REL)).unwrap();
        // What the primary installs from vm1's offer (gunnar-server fed_principals).
        let feed: BTreeMap<String, String> = o
            .iter()
            .map(|(k, v)| (format!("offer.vm1.{k}"), v.clone()))
            .collect();
        let p = gunnar_server::fed_principals::parse(&feed).unwrap();
        let ui = p
            .iter()
            .find(|p| p.id == "gunnar-ui")
            .expect("gunnar-ui enrolled from the offer");
        assert!(ui.admin && ui.kind == gunnar_proto::PrincipalKind::Machine);
        let guest = p
            .iter()
            .find(|p| p.id == "gunnar-guest")
            .expect("gunnar-guest enrolled from the offer");
        assert!(!guest.admin, "the guest has NO grant");
    }

    /// ★ **The console's credential for the OPERATOR is minted here, offered,
    /// laid as his tenant seed, and enrolled by the primary as his account's
    /// FIRST key** (2026-10-04, test2: the owner's first passkey bound to
    /// nothing, and an account with a key of its own would have refused the
    /// console's at the first read — "awaiting attestation"). One key, three
    /// places: the offer (public), the seed the console adopts (private, 0600),
    /// and the operator's key table on the primary (public) — the same key.
    #[test]
    fn the_operators_console_credential_is_offered_laid_as_his_seed_and_enrolled_at_install() {
        use std::os::unix::fs::PermissionsExt as _;
        let dir = tempfile::tempdir().unwrap();
        let o = offers(dir.path());
        let line = o
            .get(OFFER_OPERATOR_CONSOLE_PUBKEY)
            .cloned()
            .expect("the console offers its credential for the operator");
        assert!(
            line.starts_with("ssh-ed25519 ")
                && line.ends_with(&format!(
                    " {}",
                    gunnar_server::fed_principals::OPERATOR_CONSOLE_COMMENT
                )),
            "{line}"
        );
        assert!(!line.contains("PRIVATE"));
        assert_ne!(
            line, o[OFFER_CONSOLE_PUBKEY],
            "never the console's own (server-wide) key"
        );
        assert_ne!(line, o[OFFER_GUEST_PUBKEY], "never the guest key");
        assert_eq!(offers(dir.path()), o, "minted once, kept");
        assert_eq!(
            std::fs::metadata(dir.path().join(OPERATOR_REL))
                .unwrap()
                .permissions()
                .mode()
                & 0o777,
            0o600
        );

        // The feed names the operator: his seed is laid, 0600, the SAME key.
        let mut feed: BTreeMap<String, String> = BTreeMap::from([(
            gunnar_server::fed_principals::FEED_OPERATOR.to_string(),
            "rickard".to_string(),
        )]);
        assert_eq!(
            lay_operator_seed(dir.path(), &feed).unwrap().as_deref(),
            Some("rickard")
        );
        let seed = dir.path().join(SEEDS_REL).join("rickard");
        assert_eq!(
            std::fs::metadata(&seed).unwrap().permissions().mode() & 0o777,
            0o600
        );
        let laid =
            gunnar_ui_server::signer::Signer::from_openssh(std::fs::read_to_string(&seed).unwrap())
                .expect("the console can load the seed");
        assert_eq!(
            laid.public_key_openssh().split_whitespace().nth(1),
            line.split_whitespace().nth(1),
            "the seed is the offered key"
        );
        // Again: unchanged. Another operator: his seed, and the old one gone.
        assert_eq!(
            lay_operator_seed(dir.path(), &feed).unwrap().as_deref(),
            Some("rickard")
        );
        feed.insert(
            gunnar_server::fed_principals::FEED_OPERATOR.to_string(),
            "henrik".to_string(),
        );
        assert_eq!(
            lay_operator_seed(dir.path(), &feed).unwrap().as_deref(),
            Some("henrik")
        );
        assert!(dir.path().join(SEEDS_REL).join("henrik").is_file());
        assert!(
            !seed.exists(),
            "a seed for an operator the feed no longer names is not left behind"
        );
        // Not a name a file can carry: refused, nothing laid.
        feed.insert(
            gunnar_server::fed_principals::FEED_OPERATOR.to_string(),
            "../console/id".to_string(),
        );
        assert!(lay_operator_seed(dir.path(), &feed).is_err());
        // Nothing named: nothing laid, nothing refused.
        assert_eq!(
            lay_operator_seed(dir.path(), &BTreeMap::new()).unwrap(),
            None
        );

        // What the primary does with the relayed offer: the operator's FIRST key.
        let data = tempfile::tempdir().unwrap();
        let stores =
            std::sync::Arc::new(gunnar_server::stores::ServerStores::open(data.path()).unwrap());
        let admin =
            gunnar_core::ssh_key::PrivateKey::from_openssh(gunnar_core::mint_ed25519().unwrap())
                .unwrap()
                .public_key()
                .to_openssh()
                .unwrap();
        stores.auth().bootstrap_admin(&admin).unwrap();
        let mut fed: BTreeMap<String, String> = o
            .iter()
            .map(|(k, v)| (format!("offer.vm1.{k}"), v.clone()))
            .collect();
        fed.insert("admin_pubkey".into(), admin);
        fed.insert(
            gunnar_server::fed_principals::FEED_OPERATOR.into(),
            "rickard".into(),
        );
        gunnar_server::fed_principals::install(&stores, &fed).unwrap();
        let keys = stores.auth().keys_of("rickard").unwrap();
        assert_eq!(
            keys.len(),
            1,
            "the operator's only key is the console's credential: {keys:?}"
        );
        assert_eq!(
            keys[0].fingerprint_sha256,
            laid.fingerprint(),
            "the key the primary enrolled is the seed the console adopts"
        );
    }

    /// ★ **g-receipts: the operator's receipt address is read off the feed**
    /// (`console_receipts_to`, fed by `estate install`), and handed to the
    /// console's monetize handle so the operator's own orders name it as their
    /// payer — MEASURED 2026-09-29: every order asked `receipt_email = None`.
    #[test]
    fn the_operators_receipt_address_is_read_off_the_feed() {
        let f = |t: &str| BTreeMap::from([(FEED_RECEIPTS_TO.to_string(), t.to_string())]);
        assert_eq!(FEED_RECEIPTS_TO, "console_receipts_to");
        assert_eq!(
            receipts_to(&f(" operator@example.com ")),
            Some("operator@example.com".to_string())
        );
        assert_eq!(receipts_to(&f("")), None);
        assert_eq!(
            receipts_to(&BTreeMap::new()),
            None,
            "not fed: the console names no payer"
        );
    }

    /// ★ **G15: the console's product id is read off the feed** — gunnar.rs and
    /// test2.gunnar.rs both sold `gunnar` (a const in gunnar-ui); now the kit
    /// feeds `console_product` and a bad id refuses instead of selling another
    /// product's plans.
    #[test]
    fn the_consoles_product_is_read_off_the_feed() {
        let f = |t: &str| BTreeMap::from([(FEED_CONSOLE_PRODUCT.to_string(), t.to_string())]);
        assert_eq!(FEED_CONSOLE_PRODUCT, "console_product");
        assert_eq!(ENV_CONSOLE_PRODUCT, "GUNNAR_UI_PRODUCT");
        assert_eq!(
            console_product(&f("gunnar-test2")),
            Ok("gunnar-test2".to_string())
        );
        assert_eq!(console_product(&f(" gunnar ")), Ok("gunnar".to_string()));
        assert_eq!(
            console_product(&f("")),
            Ok("gunnar".to_string()),
            "blank: gunnar.rs"
        );
        assert_eq!(
            console_product(&BTreeMap::new()),
            Ok("gunnar".to_string()),
            "unfed: gunnar.rs"
        );
        for bad in [
            "Gunnar",
            "gunnar test2",
            "-gunnar",
            "9gunnar",
            "gunnar_test2",
            &"g".repeat(64),
        ] {
            let e = console_product(&f(bad)).expect_err(bad);
            assert!(
                e.contains("console_product") && e.contains(bad.trim()),
                "{e}"
            );
        }
        assert!(
            console_product(&f(&"g".repeat(63))).is_ok(),
            "63 chars is an id"
        );
    }

    #[test]
    fn a_guessable_break_glass_token_is_refused_by_name() {
        let f = |t: &str| BTreeMap::from([(FEED_ADMIN_TOKEN.to_string(), t.to_string())]);
        assert_eq!(admin_token(&BTreeMap::new()), Ok(None));
        assert!(admin_token(&f("hunter2"))
            .unwrap_err()
            .contains("[console-admin-token-weak]"));
        let strong = "4f1c9a7e-2b3d-4e5f-8a9b-0c1d2e3f4a5b-77aa";
        assert_eq!(admin_token(&f(strong)), Ok(Some(strong.to_string())));
    }

    /// ★ **The console sells through the ONE box with the estate's OWN bearer** (owner
    /// 2026-10-11: gunnar.rs and test2.gunnar.rs on one monetize, each its own Stripe — the
    /// bearer is how the box knows the site): read off the feed's sealed secret slot and the
    /// `fleet` door; no bearer sells nothing; a bearer with no door is refused by name.
    #[test]
    fn the_estates_own_bearer_at_the_one_box_is_read_off_the_feed() {
        let feed = |kv: &[(&str, &str)]| {
            kv.iter()
                .map(|(k, v)| (k.to_string(), v.to_string()))
                .collect::<BTreeMap<_, _>>()
        };
        let door = |a: &str, b: &str| {
            Some(MonetizeDoor {
                addr: a.into(),
                bearer: b.into(),
                at: None,
                ca_pem: None,
            })
        };
        assert!(
            FEED_MONETIZE_BEARER.starts_with("secret."),
            "a sealed secret slot, vm1's alone"
        );
        assert_eq!(monetize_door(&feed(&[])), Ok(None));
        assert_eq!(
            monetize_door(&feed(&[(FEED_FLEET, "https://monetize.ignalina.dk")])),
            Ok(None),
            "no bearer: sells nothing"
        );
        assert_eq!(
            monetize_door(&feed(&[
                (FEED_FLEET, "https://monetize.ignalina.dk"),
                (FEED_MONETIZE_BEARER, " b0 \n")
            ])),
            Ok(door("https://monetize.ignalina.dk", "b0"))
        );
        assert_eq!(
            monetize_door(&feed(&[
                ("offer.vm1.fleet", "https://monetize.ignalina.dk"),
                (FEED_MONETIZE_BEARER, "b0")
            ])),
            Ok(door("https://monetize.ignalina.dk", "b0")),
            "the front's offer of the door"
        );
        let e = monetize_door(&feed(&[(FEED_MONETIZE_BEARER, "b0")])).unwrap_err();
        assert!(e.contains(FEED_FLEET) && e.contains("REFUSED"), "{e}");
        assert!(
            !format!("{:?}", door("https://x", "sekret-bearer")).contains("sekret"),
            "never rendered"
        );
    }

    /// ★ i54a run29 A2: on the mock the console dialled the ONE box by NAME (`dns error`, the
    /// paid signup 502), where the boxes dial the fed seam. The console takes the same seam
    /// (`fleet_at` + `fleet_ca`), on a bench address only — anything else refused by name.
    #[test]
    fn the_console_dials_the_one_box_where_the_mock_feeds_it() {
        let feed = |kv: &[(&str, &str)]| {
            kv.iter()
                .map(|(k, v)| (k.to_string(), v.to_string()))
                .collect::<BTreeMap<_, _>>()
        };
        let pem = "-----BEGIN CERTIFICATE-----\nMIIB\n-----END CERTIFICATE-----\n";
        let base = [
            (FEED_FLEET, "https://monetize-test.ignalina.dk"),
            (FEED_MONETIZE_BEARER, "b0"),
        ];
        let mut kv = base.to_vec();
        kv.extend([
            (gunnar_server::feed::FEED_FLEET_AT, "10.0.2.2:35219"),
            (gunnar_server::feed::FEED_FLEET_CA, pem),
        ]);
        let d = monetize_door(&feed(&kv)).unwrap().expect("a door");
        assert_eq!(
            (d.addr.as_str(), d.at.as_deref(), d.ca_pem.as_deref()),
            (
                "https://monetize-test.ignalina.dk",
                Some("10.0.2.2:35219"),
                Some(pem.trim())
            ),
            "as fed, trimmed"
        );
        assert!(!format!("{d:?}").contains("BEGIN CERTIFICATE"));
        let d = monetize_door(&feed(&base)).unwrap().expect("a door");
        assert_eq!(
            (d.at, d.ca_pem),
            (None, None),
            "no seam fed: the door itself"
        );
        let mut public = base.to_vec();
        public.push((gunnar_server::feed::FEED_FLEET_AT, "203.0.113.5:443"));
        assert!(
            monetize_door(&feed(&public))
                .unwrap_err()
                .contains("fleet-seam-bench-only"),
            "a public address is no seam"
        );
    }

    /// ★ **With the door, the console's money half is ON** (it was `monetize: None` on every
    /// estate: nothing fed it a credential) — the vendor's return page `/welcome` is served;
    /// without one it is not.
    #[cfg(feature = "metered")]
    #[tokio::test]
    async fn a_console_fed_its_door_sells_and_one_without_does_not() {
        use tower::ServiceExt as _;
        let dir = tempfile::tempdir().unwrap();
        let c = Console::open(dir.path()).unwrap();
        let t = Target {
            control: "127.0.0.1:1".into(),
            twin: None,
            primary: None,
        };
        let welcome = |r: axum::Router| async move {
            let req = http::Request::get("/welcome?session=cs_test_a1b2c3d4e5")
                .header("host", "gunnar.test")
                .extension(axum::extract::ConnectInfo(std::net::SocketAddr::from((
                    [127, 0, 0, 1],
                    9,
                ))))
                .body(axum::body::Body::empty())
                .unwrap();
            r.oneshot(req).await.unwrap().status()
        };
        let door = MonetizeDoor {
            addr: "https://monetize.example".into(),
            bearer: "estate-bearer-0123456789".into(),
            at: None,
            ca_pem: None,
        };
        let (selling, _) = c
            .serving_selling(&t, "gunnar.test", None, None, Some(&door), "gunnar")
            .unwrap();
        assert_eq!(
            welcome(selling).await,
            http::StatusCode::OK,
            "the return page of a console that sells"
        );
        let (bare, _) = c
            .serving_selling(&t, "gunnar.test", None, None, None, "gunnar")
            .unwrap();
        assert_ne!(
            welcome(bare).await,
            http::StatusCode::OK,
            "no door: no signup door either"
        );
    }

    /// ★ WWW-CANON (owner 2026-10-30: the site is `www.<site>`): **the console's passkeys keep the
    /// bare site as their relying party and are asked at `https://www.<site>`** — the passkeys
    /// made on the bare host (RP ID `gunnar.rs`) keep working, and the origin guard takes a
    /// state change from www and refuses one from the bare origin or another site.
    #[tokio::test]
    async fn the_consoles_origin_is_www_and_its_relying_party_the_bare_site() {
        use tower::ServiceExt as _;
        let rp = relying_party("gunnar.rs", "www.gunnar.rs");
        assert_eq!(
            (rp.rp_id.as_str(), rp.origin.as_str()),
            ("gunnar.rs", "https://www.gunnar.rs")
        );
        let dir = tempfile::tempdir().unwrap();
        let c = Console::open(dir.path()).unwrap();
        let t = Target {
            control: "127.0.0.1:1".into(),
            twin: None,
            primary: None,
        };
        let (r, _) = c
            .serving_selling_at(
                &t,
                "gunnar.test",
                "www.gunnar.test",
                None,
                None,
                None,
                "gunnar",
            )
            .unwrap();
        let logout = |origin: &'static str| {
            let req = http::Request::post("/auth/logout")
                .header("host", "www.gunnar.test")
                .header("origin", origin)
                .extension(axum::extract::ConnectInfo(std::net::SocketAddr::from((
                    [127, 0, 0, 1],
                    9,
                ))))
                .body(axum::body::Body::empty())
                .unwrap();
            r.clone().oneshot(req)
        };
        assert_ne!(
            logout("https://www.gunnar.test").await.unwrap().status(),
            http::StatusCode::FORBIDDEN,
            "www is the console's origin"
        );
        assert_eq!(
            logout("https://gunnar.test").await.unwrap().status(),
            http::StatusCode::FORBIDDEN,
            "the bare host serves no page that could send it"
        );
        assert_eq!(
            logout("https://evil.example").await.unwrap().status(),
            http::StatusCode::FORBIDDEN
        );
    }

    /// ★ GUN-SEO-GUEST (owner 2026-10-31; WWW-CANON): **behind this front, every bare-name
    /// address a crawler or a search result holds answers 301 to the same path and query on
    /// `https://www.<site>`**: a repository page, a file at depth, an issue search, the sitemap,
    /// llms.txt, robots.txt. www itself is the console (no redirect), and a CLI's gRPC call to
    /// the bare name is not redirected. The console is served through estate-front's
    /// `App::router_now` with the names the ONE box feeds (`<site>`, `www.<site>`, `git.<site>`),
    /// the path a live front takes.
    #[tokio::test]
    async fn a_bare_name_address_is_a_301_to_the_same_path_on_www() {
        use tower::ServiceExt as _;
        let dir = tempfile::tempdir().unwrap();
        let c = Console::open(dir.path()).unwrap();
        let t = Target {
            control: "127.0.0.1:1".into(),
            twin: None,
            primary: None,
        };
        let (r, _) = c
            .serving_selling_at(
                &t,
                "gunnar.test",
                "www.gunnar.test",
                None,
                None,
                None,
                "gunnar",
            )
            .unwrap();
        let app = estate_front::web::serve::App::new(r);
        app.set_hosts(&[
            "gunnar.test".to_owned(),
            "www.gunnar.test".to_owned(),
            "git.gunnar.test".to_owned(),
        ]);
        let ask = |host: &'static str, path: &'static str| {
            let req = http::Request::get(path)
                .header("host", host)
                .extension(axum::extract::ConnectInfo(std::net::SocketAddr::from((
                    [127, 0, 0, 1],
                    9,
                ))))
                .body(axum::body::Body::empty())
                .unwrap();
            app.router_now().oneshot(req)
        };
        for path in [
            "/",
            "/nordisk/gunnar",
            "/nordisk/gunnar/blob/docs/v2/README.md",
            "/nordisk/gunnar/issues?q=open%20bug",
            "/sitemap.xml",
            "/llms.txt",
            "/robots.txt",
        ] {
            let a = ask("gunnar.test", path).await.unwrap();
            assert_eq!(
                a.status(),
                http::StatusCode::MOVED_PERMANENTLY,
                "{path} on the bare name"
            );
            assert_eq!(
                a.headers()[http::header::LOCATION].to_str().unwrap(),
                format!("https://www.gunnar.test{path}"),
                "{path}: the same path and query on www"
            );
            let w = ask("www.gunnar.test", path).await.unwrap();
            assert!(
                !w.status().is_redirection(),
                "{path} on www is the console, not a redirect: {}",
                w.status()
            );
        }
        let grpc = http::Request::post("/gunnar.v1.Repos/List")
            .header("host", "gunnar.test")
            .header(http::header::CONTENT_TYPE, "application/grpc")
            .extension(axum::extract::ConnectInfo(std::net::SocketAddr::from((
                [127, 0, 0, 1],
                9,
            ))))
            .body(axum::body::Body::empty())
            .unwrap();
        assert!(
            !app.router_now()
                .oneshot(grpc)
                .await
                .unwrap()
                .status()
                .is_redirection(),
            "a CLI's gRPC call to the bare name stays there"
        );
    }

    /// With the console embedded, it unpacks, and a stale file of another
    /// build is removed; the router builds against a (lazy) primary.
    #[tokio::test]
    async fn the_embedded_console_unpacks_and_its_router_builds_against_the_fed_primary() {
        let dir = tempfile::tempdir().unwrap();
        if FILES.is_empty() {
            assert!(
                unpack(dir.path()).unwrap().is_none(),
                "no console embedded: nothing unpacked"
            );
        } else {
            std::fs::create_dir_all(dir.path().join(DIST_REL)).unwrap();
            std::fs::write(dir.path().join(DIST_REL).join("console-old.css"), b"x").unwrap();
            let d = unpack(dir.path()).unwrap().unwrap();
            assert!(d.join("index.html").is_file());
            assert!(
                !d.join("console-old.css").exists(),
                "a file of another build is gone"
            );
        }
        let c = Console::open(dir.path()).unwrap();
        let t = Target {
            control: "127.0.0.1:1".into(),
            twin: None,
            primary: None,
        };
        let _ = c.router(&t, "gunnar.test", None, None).unwrap();
        // Rebuilt against another primary on the SAME store (no second redb lock).
        let _ = c
            .router(
                &Target {
                    control: "127.0.0.1:2".into(),
                    twin: Some("127.0.0.1:3".into()),
                    primary: None,
                },
                "gunnar.test",
                None,
                None,
            )
            .unwrap();
        // ★ The console replaced by another stops pumping the main it was
        // built against (follow-up of recharge-main-front-cut-off): before, every
        // replaced console's pump re-subscribed to its OLD main for the life of
        // the process.
        let mut slot = None;
        let (_, first) = c.serving(&t, "gunnar.test", None, None).unwrap();
        assert!(
            !replace_pump(&mut slot, first.clone()),
            "nothing to replace yet"
        );
        let (_, second) = c
            .serving(
                &Target {
                    control: "127.0.0.1:4".into(),
                    twin: None,
                    primary: None,
                },
                "gunnar.test",
                None,
                None,
            )
            .unwrap();
        assert!(replace_pump(&mut slot, second.clone()));
        assert!(
            !first.pumping(),
            "the replaced console still pumps its old main's events"
        );
        assert!(second.pumping(), "the console serving now pumps");
        if !FILES.is_empty() {
            use tower::ServiceExt as _;
            let r = c.router(&t, "gunnar.test", None, None).unwrap();
            let css = FILES
                .iter()
                .find(|(p, _)| p.ends_with(".css"))
                .map(|(p, _)| format!("/{p}"))
                .unwrap();
            let req = http::Request::get(css.as_str())
                .header("host", "gunnar.test")
                .extension(axum::extract::ConnectInfo(std::net::SocketAddr::from((
                    [127, 0, 0, 1],
                    9,
                ))))
                .body(axum::body::Body::empty())
                .unwrap();
            let rsp = r.oneshot(req).await.unwrap();
            assert_eq!(
                rsp.status(),
                http::StatusCode::OK,
                "the embedded console's stylesheet {css} is served"
            );
        }
    }
}