holger-estate-front/src/buy.rs
//! **Holger's site in the shared buy flow** (monetize-site-buy, owner 2026-10-23, A12).
//!
//! The buyer stays on the site: holger-ui's pages call this front's buy doors (Gunnar's
//! contract, path for path — `/auth/signup/*`, `/auth/private/*`, `/paid/estate/*`,
//! `/api/account/subscription/*`, `/auth/buy/offer`), and the shared crate's relay answers them
//! over the SITE's own bearer at the ONE box (`secret.monetize_bearer`, fed to an operator
//! site's front only; monetize-shop-pairing `site_front_feed`). The only outside page is the
//! vendor's checkout.
//!
//! What is Holger's here is the crate's provider trait, [`HolgerSite`]: who a request's person
//! is and their own actor ticket are the PRIMARY's word (its doors `/-/buy/me` and
//! `/-/buy/ticket`, asked with the person's own `Authorization`; the primary signs with its
//! fleet key, the key the ONE box pinned), and whether a paid sign-up's account holds its
//! passkey is asked with the console's own bearer (`/-/buy/account`). Nothing of a person's is
//! kept here.
use std::collections::BTreeMap;
use std::sync::Arc;
use std::time::Duration;
use axum::http::{HeaderMap, StatusCode};
use monetize_site_buy::{Account, BuySite, Buyer, Door, Refusal, Relay, ShopLink, Who};
/// The fed keys an operator site's front is sold through (monetize-shop-pairing `contract`).
pub const FEED_MONETIZE_BEARER: &str = "secret.monetize_bearer";
pub const FEED_CONSOLE_PRODUCT: &str = "console_product";
pub const FEED_FLEET: &str = "fleet";
/// The console host the buyer reaches (holger server-cli `paired::PASSKEY_RP_FEED`).
pub const FEED_PASSKEY_RP: &str = "passkey_rp_id";
/// What a buyer reads when the primary does not answer about them.
pub const HOLGER_NOT_REACHED: &str = "Holger cannot be reached right now: try again in a minute.";
/// **Where and as whom this site sells**, from the feed. Never printed whole: the bearer is a
/// secret.
#[derive(Clone, PartialEq, Eq)]
pub struct ShopFeed {
pub door: Door,
bearer: String,
pub product: String,
/// `https://<console host>`: every way back from the vendor is on it.
pub origin: String,
}
impl std::fmt::Debug for ShopFeed {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ShopFeed")
.field("door", &self.door.url)
.field("product", &self.product)
.field("origin", &self.origin)
.finish_non_exhaustive()
}
}
/// Pure: **the site's shop, from the front's feed** — `None` where no bearer is fed (an open
/// front, a sold estate's front: it sells nothing itself); an error where a bearer is fed and
/// the rest is not usable.
pub fn shop_feed_of(feed: &BTreeMap<String, String>) -> Option<Result<ShopFeed, String>> {
let get = |k: &str| {
feed.get(k)
.map(|v| v.trim().to_owned())
.filter(|v| !v.is_empty())
};
let bearer = get(FEED_MONETIZE_BEARER)?;
Some((|| {
let product = get(FEED_CONSOLE_PRODUCT).ok_or(format!("the site's bearer is fed without `{FEED_CONSOLE_PRODUCT}`: which product does it sell?"))?;
let url = get(FEED_FLEET).ok_or(format!(
"the site's bearer is fed without `{FEED_FLEET}`: where is the ONE monetize?"
))?;
let rp = get(FEED_PASSKEY_RP).ok_or(format!("the site's bearer is fed without `{FEED_PASSKEY_RP}`: which host does the buyer come back to?"))?;
let at = get(nordisk_pairing::fleet::FEED_FLEET_AT);
let ca_pem = get(nordisk_pairing::fleet::FEED_FLEET_CA);
if let Some(why) = nordisk_pairing::fleet::seam_refusal(at.as_deref(), ca_pem.is_some()) {
return Err(why);
}
Ok(ShopFeed {
door: Door {
url: url.trim_end_matches('/').to_owned(),
at,
ca_pem,
},
bearer,
product,
origin: format!("https://{}", rp.to_ascii_lowercase()),
})
})())
}
/// **Holger's site**: the primary's HTTP door, and the console's own bearer.
#[derive(Clone)]
pub struct HolgerSite {
origin: String,
/// `http://<primary utility>:8443`, over the tunnel.
door: String,
console: Option<String>,
agent: ureq::Agent,
limits: Arc<monetize_site_buy::limit::Budgets>,
}
impl HolgerSite {
pub fn new(origin: &str, door: &str, console: Option<String>) -> HolgerSite {
let agent = ureq::Agent::config_builder()
.http_status_as_error(false)
.timeout_global(Some(Duration::from_secs(10)))
.build()
.new_agent();
let door = door.trim().trim_end_matches('/');
let door = if door.starts_with("http://") || door.starts_with("https://") {
door.to_owned()
} else {
format!("http://{door}")
};
HolgerSite {
origin: origin.trim_end_matches('/').to_owned(),
door,
console,
agent,
limits: Arc::default(),
}
}
/// One ask of the primary's door: `(status, json)`. The `authorization` is the person's own
/// (or the console's), passed as it came.
async fn ask(
&self,
method: &'static str,
path: String,
authorization: Option<String>,
body: Option<serde_json::Value>,
) -> Result<(u16, serde_json::Value), Refusal> {
let agent = self.agent.clone();
let url = format!("{}{path}", self.door);
let asked =
tokio::task::spawn_blocking(move || -> Result<(u16, serde_json::Value), String> {
let mut res = match (method, body) {
("POST", body) => {
let mut r = agent.post(&url).header("content-type", "application/json");
if let Some(a) = &authorization {
r = r.header("authorization", a);
}
r.send(body.unwrap_or(serde_json::Value::Null).to_string())
.map_err(|e| e.to_string())?
}
_ => {
let mut r = agent.get(&url);
if let Some(a) = &authorization {
r = r.header("authorization", a);
}
r.call().map_err(|e| e.to_string())?
}
};
let status = res.status().as_u16();
let text = res
.body_mut()
.with_config()
.limit(1 << 20)
.read_to_string()
.map_err(|e| e.to_string())?;
Ok((
status,
serde_json::from_str(&text).unwrap_or(serde_json::Value::Null),
))
})
.await;
match asked {
Ok(Ok(answer)) => Ok(answer),
Ok(Err(why)) => {
tracing::warn!(door = %self.door, "buy: the primary did not answer: {why}");
Err(Refusal::new(
StatusCode::SERVICE_UNAVAILABLE,
HOLGER_NOT_REACHED,
))
}
Err(e) => Err(Refusal::new(
StatusCode::SERVICE_UNAVAILABLE,
format!("{HOLGER_NOT_REACHED} ({e})"),
)),
}
}
async fn me(&self, headers: &HeaderMap) -> Result<serde_json::Value, Refusal> {
let authorization = headers
.get(axum::http::header::AUTHORIZATION)
.and_then(|v| v.to_str().ok())
.map(str::to_owned);
match self
.ask("GET", "/-/buy/me".into(), authorization, None)
.await?
{
(200, me) => Ok(me),
(status, me) => Err(refusal_of(status, &me)),
}
}
}
/// The primary's refusal as the buyer reads it.
fn refusal_of(status: u16, answer: &serde_json::Value) -> Refusal {
let words = answer
.get("error")
.and_then(|e| e.as_str())
.unwrap_or(HOLGER_NOT_REACHED);
Refusal::new(
StatusCode::from_u16(status).unwrap_or(StatusCode::BAD_GATEWAY),
words,
)
}
/// Pure: who the primary says asks.
pub fn who_of(me: &serde_json::Value) -> Who {
let s = |k: &str| {
me.get(k)
.and_then(|v| v.as_str())
.unwrap_or_default()
.to_owned()
};
match me.get("who").and_then(|v| v.as_str()) {
Some("person") if !s("account").is_empty() => Who::Person(Buyer {
account: s("account"),
tenant: s("tenant"),
member: None,
payer: String::new(),
}),
_ => Who::Visitor,
}
}
fn unhex(s: &str) -> Option<Vec<u8>> {
s.len().is_multiple_of(2).then_some(())?;
(0..s.len())
.step_by(2)
.map(|i| u8::from_str_radix(s.get(i..i + 2)?, 16).ok())
.collect()
}
impl BuySite for HolgerSite {
fn origin(&self) -> String {
self.origin.clone()
}
fn product_word(&self) -> String {
"Holger".to_owned()
}
fn pages(&self) -> monetize_site_buy::Pages {
// Gunnar's: the sign-up page's renew view (Renew, Stop renewing, Terminate over the shared
// doors); holger-ui sends the renewal mail's `/private?renew=` on there.
monetize_site_buy::Pages {
manage: "/signup?plan=private&renew=".into(),
..monetize_site_buy::Pages::default()
}
}
fn allowed(&self, door: &str, ip: &str, key: &str) -> bool {
// Gunnar's budgets (monetize-site-buy `limit`): per (address, key), and per address only
// for a key it has not asked about before, so the welcome page's own polling never runs
// out while a guesser does.
self.limits.allow(door, ip, key)
}
/// The primary's word on the person's session (`/-/auth/passkey/session`, `stepped_up`: a
/// user-verified login inside its window), asked with their own credential.
async fn stepped_up(&self, headers: &HeaderMap) -> Result<(), String> {
let authorization = headers
.get(axum::http::header::AUTHORIZATION)
.and_then(|v| v.to_str().ok())
.map(str::to_owned);
match self
.ask(
"POST",
"/-/auth/passkey/session".into(),
authorization,
Some(serde_json::json!({})),
)
.await
{
Ok((200, s)) if s.get("stepped_up").and_then(|v| v.as_bool()) == Some(true) => Ok(()),
Ok(_) => Err(monetize_site_buy::site::STEP_UP_WORDS.to_owned()),
Err(r) => Err(r.words),
}
}
async fn who(&self, headers: &HeaderMap) -> Result<Who, Refusal> {
if headers.get(axum::http::header::AUTHORIZATION).is_none() {
return Ok(Who::Visitor);
}
Ok(who_of(&self.me(headers).await?))
}
async fn ticket(
&self,
headers: &HeaderMap,
_buyer: &Buyer,
purpose: &str,
caps: &BTreeMap<String, u64>,
) -> Result<Vec<u8>, Refusal> {
let authorization = headers
.get(axum::http::header::AUTHORIZATION)
.and_then(|v| v.to_str().ok())
.map(str::to_owned);
match self
.ask(
"POST",
"/-/buy/ticket".into(),
authorization,
Some(serde_json::json!({ "purpose": purpose, "caps": caps })),
)
.await?
{
(200, t) => t
.get("ticket")
.and_then(|t| t.as_str())
.and_then(unhex)
.ok_or_else(|| Refusal::new(StatusCode::BAD_GATEWAY, HOLGER_NOT_REACHED)),
(status, t) => Err(refusal_of(status, &t)),
}
}
async fn held_caps(
&self,
headers: &HeaderMap,
_buyer: &Buyer,
) -> Result<BTreeMap<String, u64>, Refusal> {
let me = self.me(headers).await?;
Ok(me
.get("caps")
.cloned()
.and_then(|c| serde_json::from_value(c).ok())
.unwrap_or_default())
}
async fn signup_account(&self, user: &str) -> Result<Account, Refusal> {
let path = format!(
"/-/buy/account?user={}",
monetize_site_buy::rules::query_value(user)
);
let console = self.console.as_ref().map(|t| format!("Bearer {t}"));
match self.ask("GET", path, console, None).await? {
(200, a) => match a.get("account").and_then(|v| v.as_str()) {
Some("no_passkey") => Ok(Account::NoPasskey),
Some("has_passkey") => Ok(Account::HasPasskey),
_ => Err(Refusal::new(StatusCode::CONFLICT, format!("monetize says the account {user:?} exists, and Holger holds no such person yet; try again in a moment"))),
},
(status, a) => Err(refusal_of(status, &a)),
}
}
}
/// **Holger's settings for the buy pages' shared scripts** (monetize-site-buy `assets`, A16,
/// owner 2026-10-23): its names, its free account in the owner's words for packages, this tab's
/// bearer as the session, and holger's `/-/signup` and the relay's offer door as who is here.
pub fn holger_pages() -> monetize_site_buy::assets::Settings {
monetize_site_buy::assets::Settings {
product: "Holger".into(),
storage: "holger_".into(),
free_account: "holger/account".into(),
// RA5-41: the shared buy page's three lines are the site's words, in Holger's terms.
free_note: "Free: read and install only. Publishing needs a paid plan.".into(),
card_check: "A card check at Stripe (0 kr) proves you are a person.".into(),
beta_line: "Beta: we replicate your packages, but cannot guarantee them.".into(),
manage: "/signup?plan=private&renew=".into(),
account_plan: "account".into(),
here: "site".into(),
// The session is the console's HttpOnly cookie (A34-38), as Gunnar's is: no script keeps it.
bearer_key: String::new(),
status_url: "/-/signup".into(),
// Its `/-/signup` answers sign-up links: the shared script reads them as such (A32-24).
status_shape: monetize_site_buy::assets::STATUS_LINKS.into(),
offer_url: "/auth/buy/offer".into(),
// The owner's product address (2026-10-23), shown on the welcome page as Gunnar's is.
contact: "holger@holger.rs".into(),
}
}
/// **Where Holger's order page leads** (`/paid/{reference}`, `/cancel/{reference}`): "Try
/// again" is the account's buy page, "Back to your account" the plan pane, and a signed-out
/// reader signs in with the passkey login.
pub fn holger_back() -> monetize_site_buy::assets::Back {
monetize_site_buy::assets::Back {
again: "/signup?plan=account".into(),
account: "/plan".into(),
login: "/login".into(),
}
}
/// **The buy pages' scripts for holger-ui's thin server**: the shared crate's one copy, filled
/// with [`holger_pages`] (the order page's with [`holger_back`] too, T16).
pub fn buy_scripts() -> holger_ui_server::BuyScripts {
let s = holger_pages();
holger_ui_server::BuyScripts {
signup: monetize_site_buy::assets::buy_js(&s),
welcome: monetize_site_buy::assets::welcome_js(&s),
paid: monetize_site_buy::assets::paid_js(&s, &holger_back()),
}
}
/// **The relay's router for this site**, where the feed sells here: the shared crate's doors
/// over the site's bearer, the person asked of the primary's `door`.
pub fn relay_router(
shop: &ShopFeed,
door: &str,
console: Option<String>,
) -> Result<axum::Router, String> {
let link = ShopLink::connect_lazy(&shop.door, &shop.bearer, &shop.product)?;
let site = HolgerSite::new(&shop.origin, door, console);
Ok(monetize_site_buy::router(Arc::new(Relay::new(site, link))))
}
#[cfg(test)]
mod tests {
use super::*;
fn feed(pairs: &[(&str, &str)]) -> BTreeMap<String, String> {
pairs
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect()
}
/// ★ **The welcome page's own polling is never refused** (A16, Gunnar's budgets): one buyer
/// asks about one session every two seconds (31 asks in a minute), while thirty new sessions
/// a minute from one address is a guesser's budget.
#[test]
fn the_welcome_pages_polling_is_never_refused_and_a_guesser_is() {
let site = HolgerSite::new("https://holger.mock.invalid", "127.0.0.1:9", None);
assert!(
(0..40).all(|_| site.allowed("signup", "203.0.113.9", "cs_test_abcdefghij")),
"one session, polled"
);
let guesses = (0..60)
.filter(|i| site.allowed("signup", "198.51.100.7", &format!("cs_test_guess{i:04}")))
.count();
assert_eq!(guesses, 30);
}
/// ★ **Holger's order page runs the shared crate's script** (TASKS-4-v2 T16, A28 M11): the
/// ONE order page's script for every site, with Holger's words and ways on (its bearer, the
/// plan pane, the account's buy page, the passkey login) — the checks holger-ui made of its
/// copy before the copy went. Its behaviour (captured, the pay link) is the shared crate's
/// browser test.
#[test]
fn holgers_order_page_runs_the_shared_script_with_holgers_words() {
let paid = buy_scripts().paid;
assert_eq!(
paid,
monetize_site_buy::assets::paid_js(&holger_pages(), &holger_back())
);
assert!(!paid.contains("@@"), "every word filled in");
assert!(paid.contains("const BUY = { product: 'Holger', bearerKey: '', again: '/signup?plan=account', account: '/plan', login: '/login' };"), "Holger's words and ways on");
for words in [
"'/api/account/subscription/status?reference='",
"'/api/account/subscription/cancel'",
"'Cancel order'",
"'Nothing has been charged yet. Pay with Stripe to continue, or cancel the order.'",
"'Creating your payment link…'",
"'The payment link could not be created. Nothing has been charged — cancel the order and choose again, or contact support if it happens again.'",
"'Resume payment'",
"'Paid – prolonging'",
"'Prolonged until '",
"'Upgraded to '",
"'Raised to '",
"'Your plan runs until '",
"' after payment'",
] {
assert!(paid.contains(words), "the order page lacks {words}");
}
assert!(
paid.contains("a.cancellable"),
"the button only while monetize says the order can be cancelled"
);
assert!(
!paid.contains("'Pay with Stripe'"),
"a page opened after the checkout resumes the payment"
);
assert!(!paid.contains("monetize.ignalina.dk"));
}
/// ★ **Holger's buy pages run the shared crate's scripts** (A16, owner 2026-10-23): every word
/// filled in, Gunnar's screens and doors, Holger's session and its own "who is here" — the
/// checks holger-ui made of its copies before the copies went.
#[test]
fn holgers_buy_pages_run_the_shared_scripts_with_holgers_words() {
let scripts = buy_scripts();
let (js, welcome) = (&scripts.signup, &scripts.welcome);
assert!(
!js.contains("@@") && !welcome.contains("@@"),
"every word filled in"
);
assert_eq!(js, &monetize_site_buy::assets::buy_js(&holger_pages()));
for words in [
"'Create your account at {host}'",
"'Pay {amount}'",
"'Upgrade'",
"'Buy a plan'",
"'Grow your account'",
"'New private instance'",
"'Grow your plan'",
"'Account on '",
"'your plan now'",
"'for one '",
"'’s size now'",
"' (you)'",
"'Free account'",
"'Processing …'",
"'Taking you to pay …'",
"'New private Holger'",
"'Nothing is sold by this site.'",
] {
assert!(js.contains(words), "Gunnar's words {words}");
}
for door in [
"'/auth/signup/ladder'",
"'/auth/signup/name?u='",
"'/auth/signup/start'",
"'/auth/private/ladder'",
"'/auth/private/start'",
"'/auth/private/change'",
"'/auth/private/status?company='",
"'/auth/private/sign-in-again'",
"'/api/account/subscription/bundles'",
"'/api/account/subscription/buy-bundle'",
"post('/auth/private/renew', { company })",
"post('/auth/private/cancel', { company })",
"post('/auth/private/terminate', { company, confirm: typed.value.trim() })",
] {
assert!(js.contains(door), "the relay door {door}");
}
assert!(js.contains("const SITE = { bearer: '', status: '/-/signup', statusShape: 'links', offer: '/auth/buy/offer' };") && js.contains("h['Authorization'] = 'Bearer ' + t"), "who is here, by the console's cookie (A34-38)");
assert!(
js.contains("const SIGNUP_PRIVATE_RENEW = '/signup?plan=private&renew=';")
&& js.contains("plan: 'holger/account', next: '/' }")
&& js.contains("const CARD_CHECK_KEY = 'holger_card_check';")
);
assert!(
js.contains("reveal($('f-email'), (m === 'private' || m === 'account') && !signedIn)"),
"no e-mail field when signed in"
);
assert!(js.contains("if (step && step.id !== FREE_ACCOUNT.id) return BUTTONS.account.replace('{amount}', amountOf(step.month));"), "Pay <amount> for a paid plan");
assert!(
js.contains(
"'Payment received. When ' + company + ' is ready we’ll send a sign-in link to '"
),
"the private watch"
);
assert!(
!js.contains("monetize.ignalina.dk") && !js.contains("/shop/") && !js.contains("/kop"),
"no page of the shop's"
);
assert!(
welcome.contains("'/auth/signup/status?session='")
&& welcome.contains("'/auth/signup/sign-in-again'")
&& welcome.contains("'/auth/signup/rename'")
);
assert!(
welcome.contains("'Your account is ready'")
&& welcome.contains("'Register your security key now'")
&& welcome.contains("'Or open the link we sent to your e-mail.'")
&& welcome.contains("const CONTACT = 'holger@holger.rs';")
&& welcome.contains("const SESSION_KEY = 'holger_signup_session';")
);
}
#[test]
fn a_site_sells_only_where_its_bearer_is_fed_and_the_bearer_is_never_printed() {
assert!(
shop_feed_of(&feed(&[("fleet", "https://monetize.mock.invalid")])).is_none(),
"no bearer: this front sells nothing itself"
);
let f = shop_feed_of(&feed(&[
(FEED_MONETIZE_BEARER, "s3cret-bearer"),
(FEED_CONSOLE_PRODUCT, "holger"),
(FEED_FLEET, "https://monetize.mock.invalid/"),
(FEED_PASSKEY_RP, "Holger.Mock.Invalid"),
]))
.unwrap()
.unwrap();
assert_eq!(
(f.door.url.as_str(), f.product.as_str(), f.origin.as_str()),
(
"https://monetize.mock.invalid",
"holger",
"https://holger.mock.invalid"
)
);
assert!(!format!("{f:?}").contains("s3cret"), "{f:?}");
let half = shop_feed_of(&feed(&[
(FEED_MONETIZE_BEARER, "b"),
(FEED_FLEET, "https://monetize.mock.invalid"),
]))
.unwrap()
.unwrap_err();
assert!(half.contains(FEED_CONSOLE_PRODUCT), "{half}");
}
#[test]
fn the_primarys_word_names_the_person_and_nothing_else() {
assert_eq!(who_of(&serde_json::json!({"who": "visitor"})), Who::Visitor);
assert_eq!(
who_of(&serde_json::json!({"who": "person", "account": "x14", "tenant": "x14"})),
Who::Person(Buyer {
account: "x14".into(),
tenant: "x14".into(),
..Buyer::default()
})
);
assert_eq!(
who_of(&serde_json::json!({"who": "person", "account": ""})),
Who::Visitor
);
assert_eq!(unhex("7b7d"), Some(b"{}".to_vec()));
assert_eq!(unhex("7"), None);
// ★ A16: an instance is managed on the sign-up page, as Gunnar's is (Renew, Stop renewing,
// Terminate over the shared doors); `/private?renew=` goes on there.
let p = HolgerSite::new("https://holger.mock.invalid", "127.0.0.1:1", None).pages();
assert_eq!(
(p.account.as_str(), p.private.as_str(), p.manage.as_str()),
(
"/signup?plan=account",
"/signup?plan=private",
"/signup?plan=private&renew="
)
);
}
/// ★ The person is the primary's word, asked with THEIR credential; the passkey question
/// with the console's.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn the_site_asks_the_primary_with_the_persons_own_credential() {
use axum::routing::{get, post};
let primary = axum::Router::new()
.route("/-/buy/me", get(|h: HeaderMap| async move {
match h.get("authorization").and_then(|v| v.to_str().ok()) {
Some("Bearer person-token") => axum::Json(serde_json::json!({"who": "person", "account": "x14", "tenant": "x14", "bound": true, "caps": {"disk_bytes": 5}})),
_ => axum::Json(serde_json::json!({"who": "visitor"})),
}
}))
.route("/-/buy/ticket", post(|h: HeaderMap, axum::Json(b): axum::Json<serde_json::Value>| async move {
let ok = h.get("authorization").and_then(|v| v.to_str().ok()) == Some("Bearer person-token");
if ok && b["purpose"] == "order" && b["caps"]["disk_bytes"] == 15 {
(StatusCode::OK, axum::Json(serde_json::json!({"ticket": "7b7d"})))
} else {
(StatusCode::FORBIDDEN, axum::Json(serde_json::json!({"error": "more room for this repository is bought by an admin of it"})))
}
}))
.route("/-/auth/passkey/session", post(|h: HeaderMap| async move {
match h.get("authorization").and_then(|v| v.to_str().ok()) {
Some("Bearer fresh-token") => (StatusCode::OK, axum::Json(serde_json::json!({"subject": "passkey:aa", "stepped_up": true}))),
Some("Bearer person-token") => (StatusCode::OK, axum::Json(serde_json::json!({"subject": "passkey:aa", "stepped_up": false}))),
_ => (StatusCode::UNAUTHORIZED, axum::Json(serde_json::json!({"error": "no session"}))),
}
}))
.route("/-/buy/account", get(|h: HeaderMap| async move {
let console = h.get("authorization").and_then(|v| v.to_str().ok()) == Some("Bearer console-token");
axum::Json(serde_json::json!({"account": if console { "no_passkey" } else { "none" }}))
}));
let l = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let at = l.local_addr().unwrap();
tokio::spawn(async move { axum::serve(l, primary).await });
let site = HolgerSite::new(
"https://holger.mock.invalid",
&at.to_string(),
Some("console-token".into()),
);
let mut h = HeaderMap::new();
assert_eq!(
site.who(&h).await.unwrap(),
Who::Visitor,
"no credential: a visitor, the primary not even asked"
);
h.insert("authorization", "Bearer person-token".parse().unwrap());
let Who::Person(buyer) = site.who(&h).await.unwrap() else {
panic!("a person")
};
assert_eq!(buyer.account, "x14");
assert_eq!(
site.held_caps(&h, &buyer).await.unwrap(),
BTreeMap::from([("disk_bytes".to_string(), 5u64)])
);
let caps = BTreeMap::from([("disk_bytes".to_string(), 15u64)]);
assert_eq!(
site.ticket(&h, &buyer, "order", &caps).await.unwrap(),
b"{}".to_vec()
);
let refused = site
.ticket(&h, &buyer, "order", &BTreeMap::new())
.await
.unwrap_err();
assert_eq!(refused.status, StatusCode::FORBIDDEN);
assert_eq!(
site.signup_account("x14").await.unwrap(),
Account::NoPasskey
);
// ★ A16: ending an instance needs a fresh, user-verified login — the primary's word.
assert!(
site.stepped_up(&h).await.is_err(),
"a session older than the window logs in again"
);
let mut fresh = HeaderMap::new();
fresh.insert("authorization", "Bearer fresh-token".parse().unwrap());
assert_eq!(site.stepped_up(&fresh).await, Ok(()));
assert!(site.stepped_up(&HeaderMap::new()).await.is_err());
let down = HolgerSite::new("https://holger.mock.invalid", "127.0.0.1:9", None);
assert_eq!(down.who(&h).await.unwrap_err().words, HOLGER_NOT_REACHED);
}
}