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:?}");
}
}
}