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

nordisk/holger

holger-estate-front/src/console.rs

//! **holger's console on the front's :443** (feature `console`) — holger-ui's
//! thin server (`holger-ui-server`) linked in: its router is what the web face
//! (edda `estate-front::web`) serves, the built bundle is embedded at build
//! time and unpacked to `<data>/console/dist`.
//!
//! The console only serves the bundle and passes calls through to the
//! primary — gRPC-web to its gRPC door, the passkey door and the front page to
//! its HTTP door. The people, their sessions and every decision are holger's;
//! more room is bought in the ONE shop (holger's `RoomService` names it).
//!
//! | what | from |
//! |---|---|
//! | holger's gRPC door | the verdict: the primary's utility address, `:50051` |
//! | holger's HTTP door (passkeys, front page) | the primary's utility address, `:8443` |
//! | the proof behind a ceremony's client hint | the console's bearer to the primary, derived from the front's console key and the primary's offered one (A7 row 14, [`crate::console_key`]) — never anyone's credential |
//! | the shop's break-glass bearer | the fed `holger_console_token` ([`SHOP_BEARER_FEED`]): the front only holds it; a call that presents it is passed on with the primary's derived bearer ([`present`]) |
//!
//! **The shop's bearer (measured on the mock gate 2026-10-22, holger 8614b63d).** The ONE
//! shop's sale check reads `GET /-/releases` through this face with the bearer it fed the
//! front (`holger_console_token`, monetize-shop-pairing `sale_check`). Until A7 row 14 every
//! box held that same token as its console login; now each box derives its own, so the main
//! refused the shop's (`holger-front-3`, the reads are gated) and the site stayed `installing
//! (paired)`. The front, which the shop fed and which derives the primary's bearer, swaps one
//! for the other on the pass-through; no box holds the shop's secret.
//!
//! The doors and the proof are re-read live: a moved primary (a recharge) or a
//! new token swaps the router for the next connection, no restart.

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

use holger_ui_server::Upstreams;
use nordisk_pairing::join::JoinReply;

mod dist {
    include!(concat!(env!("OUT_DIR"), "/console_dist.rs"));
}

/// Where the bundle is unpacked, under the data root.
pub const DIST_REL: &str = "console/dist";
/// How often the feed is looked at again.
const POLL: Duration = Duration::from_secs(2);

/// Where the embedded bundle was read from at build time; `None` = not embedded.
pub fn dist_source() -> Option<&'static str> {
    dist::DIST_SOURCE
}

/// **The bundle this build reads** when `HOLGER_FRONT_CONSOLE_DIST` is unset: holger-ui's
/// metered `dist-metered/` under `metered`, its open `dist/` otherwise (A28 M13).
pub fn declared_dist() -> &'static str {
    dist::DECLARED
}

/// **Unpack the embedded bundle** to `<data>/console/dist` (a file rewritten only
/// when it differs; a file the build no longer carries removed). `None` when
/// this binary carries no bundle.
pub fn unpack(root: &Path) -> std::io::Result<Option<PathBuf>> {
    if dist::FILES.is_empty() {
        return Ok(None);
    }
    let dir = root.join(DIST_REL);
    std::fs::create_dir_all(&dir)?;
    let mut keep = std::collections::BTreeSet::new();
    for (rel, bytes) in dist::FILES {
        let p = dir.join(rel);
        keep.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)?;
        }
        std::fs::write(&p, bytes)?;
    }
    fn sweep(d: &Path, keep: &std::collections::BTreeSet<PathBuf>) {
        for e in std::fs::read_dir(d).into_iter().flatten().flatten() {
            let p = e.path();
            if p.is_dir() {
                sweep(&p, keep);
            } else if !keep.contains(&p) {
                let _ = std::fs::remove_file(&p);
            }
        }
    }
    sweep(&dir, &keep);
    Ok(Some(dir))
}

/// **What the console runs with, from the feed** — pure.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Wiring {
    /// The primary's gRPC door; `None` until the verdict names one.
    pub appliance: Option<String>,
    /// The primary's HTTP door (`<utility>:8443`): passkeys and the front page.
    pub door: Option<String>,
    /// The console's bearer — the proof behind a ceremony's client hint.
    pub token: Option<String>,
    /// The shop's break-glass bearer fed to the front ([`SHOP_BEARER_FEED`]); a call that
    /// presents it is passed on with [`Wiring::token`] instead ([`present`]).
    pub shop_bearer: Option<String>,
    /// **Where and as whom this site sells** (A12, the shared buy flow): fed to an operator
    /// site's front only; `None` here sells nothing itself.
    #[cfg(feature = "metered")]
    pub shop: Option<crate::buy::ShopFeed>,
    /// Why a fed value was not taken.
    pub refused: Vec<String>,
}

impl Wiring {
    pub fn of(reply: &JoinReply) -> Wiring {
        Wiring::of_with(crate::console_key::adopted(), reply)
    }

    /// Pure: [`Wiring::of`] with the front's console key `key` (the bearer is the primary's).
    pub fn of_with(key: Option<[u8; 32]>, reply: &JoinReply) -> Wiring {
        let mut refused = Vec::new();
        let token = match crate::console_key::bearers_with(key, reply) {
            Ok(b) => b.and_then(|b| {
                crate::primary_control(reply).and_then(|c| b.at(&c).map(str::to_string))
            }),
            Err(e) => {
                refused.push(e);
                None
            }
        };
        // The console's bearer and every person's login go to the primary over wg0 only
        // on an estate (lane E1): an address off the tunnel is not wired.
        let mut on_tunnel = |what: &str, at: Option<String>| {
            at.filter(|a| {
                crate::wg_only::check(what, a)
                    .map_err(|e| refused.push(e))
                    .is_ok()
            })
        };
        let (appliance, door) = (
            on_tunnel("the console's appliance", crate::primary_control(reply)),
            on_tunnel("the console's door", crate::primary_door(reply)),
        );
        let shop_bearer = reply
            .feed
            .get(SHOP_BEARER_FEED)
            .map(|t| t.trim().to_string())
            .filter(|t| !t.is_empty());
        #[cfg(feature = "metered")]
        let shop = match crate::buy::shop_feed_of(&reply.feed) {
            Some(Ok(s)) => Some(s),
            Some(Err(why)) => {
                refused.push(format!("the site's buy flow: {why}"));
                None
            }
            None => None,
        };
        Wiring {
            appliance,
            door,
            token,
            shop_bearer,
            #[cfg(feature = "metered")]
            shop,
            refused,
        }
    }
}

/// **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`
/// (holger.rs). Everything else, every sold Holger first, leaves the console's public site
/// OFF (the airgap default).
pub const PUBLIC_SITE_FEED: &str = "console_public_site";
/// holger-ui's switch (`holger_public_site::ENV_SWITCH`): OFF unless `on`.
pub const ENV_PUBLIC_SITE: &str = "HOLGER_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(PUBLIC_SITE_FEED)
        .is_some_and(|v| v.trim().eq_ignore_ascii_case("on"))
}

/// The feed key of the shop's break-glass bearer (monetize-shop-pairing `HOLGER.console_token_feed`).
pub const SHOP_BEARER_FEED: &str = "holger_console_token";

/// Pure: **the `Authorization` a call is passed on with** — `Some(Bearer <primary's>)` when
/// it presents the shop's bearer and the front holds the primary's derived one, `None` (the
/// call's own header kept) otherwise. A person's bearer is never touched.
pub fn present(w: &Wiring, authorization: Option<&str>) -> Option<String> {
    let (shop, primary) = (w.shop_bearer.as_deref()?, w.token.as_deref()?);
    let given = authorization?.trim().strip_prefix("Bearer ")?.trim();
    let same = given.len() == shop.len()
        && given
            .bytes()
            .zip(shop.bytes())
            .fold(0u8, |acc, (a, b)| acc | (a ^ b))
            == 0;
    same.then(|| format!("Bearer {primary}"))
}

/// **The bearers a swap needs, now** (lane OX-WGDS3): the shop's (fed) and the primary's
/// console bearer (derived), as the console's follow loop last wired them; shared with the
/// front's outer layer ([`swap_layer`]), which swaps on EVERY route of :443 — the console's
/// pass-through and the repository route (`repo.<estate>`) alike — and counts each swap.
#[derive(Clone, Default)]
pub struct Swap {
    held: Arc<std::sync::RwLock<Option<(String, String)>>>,
    swapped: Arc<std::sync::atomic::AtomicU64>,
}

impl Swap {
    /// From now on, `shop` is passed on as `primary`; either `None`: nothing is swapped.
    pub fn set_bearers(&self, shop: Option<&str>, primary: Option<&str>) {
        *self.held.write().unwrap_or_else(|e| e.into_inner()) = shop
            .zip(primary)
            .map(|(s, p)| (s.to_string(), p.to_string()));
    }

    /// [`Swap::set_bearers`] from a wiring.
    pub fn set(&self, w: &Wiring) {
        self.set_bearers(w.shop_bearer.as_deref(), w.token.as_deref());
    }

    /// How many calls were passed on with the primary's bearer.
    pub fn swapped(&self) -> u64 {
        self.swapped.load(std::sync::atomic::Ordering::Relaxed)
    }

    /// **Swap `req`'s bearer** when it is the shop's: `true` when it was (and it is audited).
    pub fn apply(&self, req: &mut axum::extract::Request) -> bool {
        use axum::http::{header::AUTHORIZATION, HeaderValue};
        let held = self.held.read().unwrap_or_else(|e| e.into_inner()).clone();
        let Some((shop, primary)) = held else {
            return false;
        };
        let w = Wiring {
            appliance: None,
            door: None,
            token: Some(primary),
            shop_bearer: Some(shop),
            #[cfg(feature = "metered")]
            shop: None,
            refused: Vec::new(),
        };
        let Some(v) = present(
            &w,
            req.headers()
                .get(AUTHORIZATION)
                .and_then(|v| v.to_str().ok()),
        )
        .and_then(|t| HeaderValue::from_str(&t).ok()) else {
            return false;
        };
        req.headers_mut().insert(AUTHORIZATION, v);
        self.swapped
            .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
        let host = req
            .headers()
            .get(axum::http::header::HOST)
            .and_then(|h| h.to_str().ok())
            .map(str::to_string)
            .or_else(|| req.uri().host().map(str::to_string))
            .unwrap_or_default();
        tracing::info!(target: "holger_front::audit", method = %req.method(), path = %req.uri().path(), %host, "console: the shop's bearer is passed on as the primary's console bearer");
        true
    }
}

/// ★ **The front's outer layer on :443** (edda `Hooks::outer`, lane OX-WGDS3): the shop's bearer
/// swapped for the primary's console bearer on every route the front serves — the console's
/// pass-through and the repository route to the primary — and every swap audited ([`Swap`]).
/// A person's bearer and any other are passed on as they came.
pub fn swap_layer(swap: Swap) -> estate_front::web::serve::Outer {
    Arc::new(move |router: axum::Router| {
        let swap = swap.clone();
        router.layer(axum::middleware::from_fn(
            move |mut req: axum::extract::Request, next: axum::middleware::Next| {
                let swap = swap.clone();
                async move {
                    swap.apply(&mut req);
                    next.run(req).await
                }
            },
        ))
    })
}

/// **The console's router for `w`**: the bundle in `ui_dir`, holger's doors.
pub fn router(w: &Wiring, ui_dir: Option<PathBuf>) -> Result<axum::Router, String> {
    router_at(w, ui_dir, None)
}

/// [`router`], with this front's certificates for the admin page when `root` is given
/// ([`crate::certs_page`], owner 2026-10-25).
pub fn router_at(
    w: &Wiring,
    ui_dir: Option<PathBuf>,
    root: Option<PathBuf>,
) -> Result<axum::Router, String> {
    let grpc = w.appliance.as_deref().ok_or("no primary elected yet")?;
    let door = w.door.as_deref().ok_or("no primary elected yet")?;
    let swap = axum::middleware::map_request_with_state(
        Arc::new(w.clone()),
        |axum::extract::State(w): axum::extract::State<Arc<Wiring>>,
         mut req: axum::extract::Request| async move {
            use axum::http::{header::AUTHORIZATION, HeaderValue};
            let to = present(
                &w,
                req.headers()
                    .get(AUTHORIZATION)
                    .and_then(|v| v.to_str().ok()),
            );
            if let Some(v) = to.and_then(|t| HeaderValue::from_str(&t).ok()) {
                req.headers_mut().insert(AUTHORIZATION, v);
                tracing::info!(target: "holger_front::audit", method = %req.method(), path = %req.uri().path(), "console: the shop's bearer is passed on as the primary's console bearer");
            }
            req
        },
    );
    // A16: the buy pages' scripts are the shared crate's (monetize-site-buy, metered only).
    #[cfg(feature = "metered")]
    let scripts = Some(crate::buy::buy_scripts());
    #[cfg(not(feature = "metered"))]
    let scripts = None;
    // LOW-PRIV: a front that sells nothing here (no shop in its feed: a sold private Holger)
    // merges no relay, and its console's /signup says this Holger is private.
    #[cfg(feature = "metered")]
    let sells = w.shop.is_some();
    #[cfg(not(feature = "metered"))]
    let sells = false;
    let console = holger_ui_server::router_selling(
        ui_dir,
        Upstreams::new(grpc, door, w.token.clone()),
        scripts,
        sells,
    );
    let console = match root {
        Some(root) => {
            crate::certs_page::router(root, crate::certs_page::grpc_roles(grpc)).merge(console)
        }
        None => console,
    };
    // **The buy flow on the site** (A12): the shared relay's doors in front of the pages.
    #[cfg(feature = "metered")]
    let console = match &w.shop {
        Some(shop) => crate::buy::relay_router(shop, door, w.token.clone())?.merge(console),
        None => console,
    };
    // The person's session is the console's HttpOnly cookie (A34-38): it is their bearer on
    // the relay's doors as on the console's, once, around the whole site.
    Ok(holger_ui_server::with_session(console).layer(swap))
}

/// **Serve the console on the web face for the life of the process**: the
/// bundle unpacked, the router rebuilt when the primary or the token moves.
/// Must run inside the tokio runtime.
pub fn start(
    web: estate_front::web::Web,
    latest: Arc<dyn Fn() -> Option<JoinReply> + Send + Sync>,
    root: PathBuf,
) {
    start_swapping(web, latest, root, Swap::default())
}

/// [`start`], keeping `swap` (the front's outer layer, [`swap_layer`]) at the bearers it wires.
pub fn start_swapping(
    web: estate_front::web::Web,
    latest: Arc<dyn Fn() -> Option<JoinReply> + Send + Sync>,
    root: PathBuf,
    swap: Swap,
) {
    let ui_dir = match unpack(&root) {
        Ok(Some(d)) => {
            tracing::info!(dir = %d.display(), source = ?dist_source(), "console: the built bundle is unpacked");
            Some(d)
        }
        Ok(None) => {
            tracing::warn!("console: this binary carries no built bundle — only the pass-through to holger is served ([console-not-on-medium])");
            None
        }
        Err(e) => {
            tracing::error!(error = %e, "console: the bundle could not be unpacked");
            None
        }
    };
    // A32-14, stays a task: the console's follow loop, async on the front's runtime (the Holger UI parity lane's area).
    tokio::spawn(async move {
        let mut serving: Option<Wiring> = None;
        let mut said: Vec<String> = Vec::new();
        let mut public: Option<bool> = None;
        loop {
            if let Some(reply) = latest() {
                // The public site's switch, before the console is first built and whenever the
                // feed moves it: on only where the ONE box fed it (holger.rs), else unset = off.
                let now = public_site_of(&reply.feed);
                if public != Some(now) {
                    if now {
                        std::env::set_var(ENV_PUBLIC_SITE, "on");
                    } else {
                        std::env::remove_var(ENV_PUBLIC_SITE);
                    }
                    // The feed is the switch's INITIAL position (owner 2026-10-26): the stored
                    // position at holger is the authority, and until holger has answered the
                    // site stays OFF (RA2-1: holger-ui's switch fails closed).
                    holger_ui_server::public_site_fed(now);
                    tracing::info!(
                        on = now,
                        stored = holger_ui_server::public_site_switch_stored(),
                        "console: the public site's switch, as fed"
                    );
                    public = Some(now);
                }
                let w = Wiring::of(&reply);
                swap.set(&w);
                for l in &w.refused {
                    if !said.contains(l) {
                        tracing::error!("console: {l}");
                        said.push(l.clone());
                    }
                }
                if serving.as_ref() != Some(&w) && w.appliance.is_some() {
                    match router_at(&w, ui_dir.clone(), Some(root.clone())) {
                        Ok(r) => {
                            web.app.set(r);
                            tracing::info!(appliance = ?w.appliance, door = ?w.door, "console: served on :443, passing through to the primary");
                            serving = Some(w);
                        }
                        Err(e) => tracing::error!("console: {e}"),
                    }
                }
            }
            tokio::time::sleep(POLL).await;
        }
    });
}

/// The feed a console test is given (tests only).
#[doc(hidden)]
pub fn feed_of(pairs: &[(&str, &str)]) -> BTreeMap<String, String> {
    pairs
        .iter()
        .map(|(k, v)| (k.to_string(), v.to_string()))
        .collect()
}

#[cfg(test)]
mod declared_dist_tests {
    /// ★ A28 M13: **the paid front embeds the paid console.** holger-ui builds two bundles: the
    /// open `dist/` (no word about money, no sign-up, welcome or order page) and the metered
    /// `dist-metered/`. A metered front reads the metered one, so its buy routes find their
    /// pages; a front without `metered` reads the open one.
    #[test]
    fn the_front_reads_the_bundle_of_its_own_build() {
        let declared = super::declared_dist();
        if cfg!(feature = "metered") {
            assert!(declared.ends_with("holger-ui/dist-metered"), "{declared}");
        } else {
            assert!(declared.ends_with("holger-ui/dist"), "{declared}");
        }
    }
}

#[cfg(test)]
mod public_site_tests {
    use super::*;
    use std::collections::BTreeMap;

    /// ★ **The public site is ON only where the site's own declaration says so**
    /// (owner 2026-10-25: holger.rs; a sold private Holger 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 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!(PUBLIC_SITE_FEED, "console_public_site");
        assert!(public_site_of(&feed(&[(PUBLIC_SITE_FEED, "on")])));
        assert!(public_site_of(&feed(&[(PUBLIC_SITE_FEED, " ON ")])));
        for off in [
            feed(&[]),
            feed(&[(PUBLIC_SITE_FEED, "off")]),
            feed(&[(PUBLIC_SITE_FEED, "")]),
            feed(&[
                ("console_instance", "acme"),
                ("console_provider", "https://holger.rs"),
            ]),
        ] {
            assert!(!public_site_of(&off), "{off:?}");
        }
    }
}