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

nordisk/holger

holger-front/src/lib.rs

//! **holger.rs's front page** — the whole public door, as one pure function.
//!
//! holger-server is a hand-rolled `hyper` dispatcher with no router, no
//! template engine and no static-asset tree; until now `GET /` answered `404
//! Unknown repository`. So this crate is not a framework and does not try to
//! be one: it is [`route`], a function from a method and a path to a
//! [`Reply`], and mounting it in `server/lib/src/exposed/http.rs` is four
//! lines next to the `/-/search` arm.
//!
//! That shape is the point. Everything the page *is* — the bytes, the JSON,
//! the cache headers, the refusals — is decided here, where it can be tested
//! in a crate that builds in seconds, rather than inside a request handler
//! that needs the whole server to compile.
//!
//! ## The two doors
//!
//! | path | what it answers |
//! |---|---|
//! | `GET /` | the page (one self-contained HTML file) |
//! | `GET /holger.webp` | the picture on its left half |
//! | `GET /-/front` | who this server is, and whether it has a browser login |
//! | `GET /-/releases` | the catalogue: the public repositories, and the latest version of each package |
//!
//! ## ★ The one rule about version numbers
//!
//! **This crate never decides which version is newest.** [`latest_by`] groups
//! and folds, but the comparison is a function the caller passes in, and the
//! caller is `server/lib`, which passes `retention::cmp_version` — the
//! comparator holger already uses to decide which artifact a retention sweep
//! must not delete.
//!
//! That is not indirection for its own sake. A front page that ranked versions
//! with its own comparator would be a SECOND source of truth for "the latest
//! release", and the first time somebody published `1.10.0` beside `1.9.0` the
//! page and `holger retention` would name different artifacts as the newest —
//! with no way to tell which of them was wrong.

use serde::{Deserialize, Serialize};
use std::cmp::Ordering;
use std::collections::BTreeMap;

use holger_errcode as ec;

/// The page, as served. One file: holger-server has nowhere to serve a second
/// one from.
pub const PAGE: &str = include_str!("../assets/front.html");

/// **The release key**: the key id (hex sha256 of the SubjectPublicKeyInfo DER) of the one
/// key Holger's releases are signed with, the owner's YubiKey key (PIV slot 9c, ECDSA
/// P-256, generated and attested on the device). Written here and nowhere else in this
/// crate; the page shows it through [`RELEASE_SIGNING_SLOT`].
pub const RELEASE_KEY_ID: &str = "d24d3785e8281bcd37e8f2c40a3a85e8152d3ce2200a5c929a1a5ca636e8249e";
/// `slsa/` in holger: the key, its YubiKey attestation and how to verify a release.
pub const HOLGER_SLSA_URL: &str = "https://gunnar.rs/nordisk/holger/tree/slsa";
/// `slsa/` in gunnar: the same key, which signs that product's releases too.
pub const GUNNAR_SLSA_URL: &str = "https://gunnar.rs/nordisk/gunnar/tree/slsa";
/// The one slot in [`PAGE`] that [`page`] fills with [`release_signing_html`].
pub const RELEASE_SIGNING_SLOT: &str = "<!--release-signing-->";

/// The release-signing note, from the constants above (no data from outside).
pub fn release_signing_html() -> String {
    format!(
        "<section class=\"signing\" aria-labelledby=\"signing-heading\">\n\
         <h2 id=\"signing-heading\">Release signing</h2>\n\
         <p>Holger's releases are signed with an ECDSA P-256 key made on the owner's YubiKey, which \
         attests on the device that the key was generated there and cannot leave it. Key id \
         <code>{RELEASE_KEY_ID}</code>. The key, its attestation and how to verify a release: \
         <a href=\"{HOLGER_SLSA_URL}\">slsa/ in holger</a> and \
         <a href=\"{GUNNAR_SLSA_URL}\">slsa/ in gunnar</a> (the same key).</p>\n\
         </section>"
    )
}

/// The page as the door serves it: [`PAGE`] with [`RELEASE_SIGNING_SLOT`] filled.
pub fn page() -> &'static str {
    static SERVED: std::sync::OnceLock<String> = std::sync::OnceLock::new();
    SERVED.get_or_init(|| PAGE.replace(RELEASE_SIGNING_SLOT, &release_signing_html()))
}

/// The picture on the left half.
///
/// Produced from the repository's OWN `.nornir/assets/holger-znippy-logo.png`
/// by `holger-ops logo` — the same file the readme shows, converted once, in
/// Rust, to lossless WebP. Compiled in rather than read from disk, because a
/// front page whose picture depends on a deploy having copied a file is a
/// front page with a half-drawn state nobody tests.
pub const PICTURE: &[u8] = include_bytes!("../assets/holger.webp");

/// Where the picture is served, named once here and once in the page's
/// `--backdrop-image`. A test asserts they agree.
pub const PICTURE_PATH: &str = "/holger.webp";

/// **The makers' marks, compiled in for the same reason the picture is** — and
/// served from THIS origin rather than hotlinked. An avatar fetched off a third
/// party's CDN would need a hole in this page's policy and would hand every
/// visitor's address to that third party for two files under 4 kB. Copied from
/// gunnar-ui, so the estate's two fronts credit their makers identically.
pub const VETRA_MARK: &[u8] = include_bytes!("../assets/vetra.svg");
/// Where the Vetra wordmark is served. A WORDMARK, so the page sizes it by
/// height only (96x18 from its own 1380x260 viewBox); forcing it square would
/// squash five letters.
pub const VETRA_PATH: &str = "/vetra.svg";
pub const IGNALINA_MARK: &[u8] = include_bytes!("../assets/ignalina.png");
/// Where the Ignalina mark is served — a square avatar, 18x18.
pub const IGNALINA_PATH: &str = "/ignalina.png";
pub const FRONT_PATH: &str = "/-/front";
pub const RELEASES_PATH: &str = "/-/releases";

// ─────────────────────────────────────────────────────────────────────────────
// The three columns
// ─────────────────────────────────────────────────────────────────────────────

/// **One row of the front page's table, and there is exactly one more field
/// than there used to be.**
///
/// Size, content type, checksum and namespace all exist on
/// [`holger_traits::ArtifactEntry`](https://git.gunnar.rs/nordisk/holger) and
/// none of them is here. The page a stranger reads first answers one question —
/// what is in this server and how new is it — and every further column is noise
/// around the answer. The console has the rest.
///
/// ★ **`published_unix_ms` is the exception, and it is here because the second
/// half of that question could not be answered without it.** "How new is it"
/// was answered by showing the latest VERSION of each package, which is a
/// different question wearing the same words: a repository whose newest
/// publication was a patch to an old branch reads as untouched, and a package
/// that has not moved in a year reads the same as one that moved this morning.
/// Nothing in the artifact model carried a time, so no time was invented — the
/// right call, and this is the fix for it rather than a workaround.
///
/// It is `Option`, and `None` means the backend serving that repository does
/// not know (see `ArtifactEntry`'s field docs for which backends can answer and
/// why). A row with no time is NOT ranked as old: it is left out of the
/// recency list entirely, because sorting an unknown is how a fabricated zero
/// gets into a page.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Release {
    /// The holger repository the artifact lives in (`crates-mirror`, `bundles`).
    pub repository: String,
    /// The artifact's name. A namespaced coordinate (maven `groupId`, npm
    /// scope) is rendered `namespace/name` by [`artifact_name`] — one string,
    /// because the column is one column.
    pub artifact: String,
    /// The version, verbatim. Never parsed, never normalised, never
    /// re-rendered: whatever was published is what is shown.
    pub version: String,
    /// When it was published, Unix milliseconds UTC. `None` = the serving
    /// backend does not know, which is a normal answer and not a zero.
    ///
    /// `skip_serializing_if` so a row with no time carries no key at all: a
    /// `null` and a missing key read the same to the page's `if (row.published_unix_ms)`,
    /// but the missing key cannot be mistaken for a value by anything else
    /// reading this document. `default` so a document written before this field
    /// existed still parses.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub published_unix_ms: Option<i64>,
}

/// **One public repository, as a stranger may see it.**
///
/// ★ Why this exists beside [`Release`]: a repository with nothing in it
/// produced NO rows, so a freshly installed holger — or one whose store is
/// still empty, which is what holger.rs is today — answered `/-/releases` with
/// `[]` and the front page said "Nothing published yet." and named nothing at
/// all. That is a true sentence about the artifacts and a useless page about
/// the server: the repositories are configured, they are public, and they are
/// the first thing a visitor needs in order to point a package manager
/// anywhere.
///
/// So the roster is listed whether or not anything has been published into it.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Repository {
    /// The repository's name, which is also its first path segment on this
    /// server.
    pub name: String,
    /// The format it speaks — `cargo`, `maven`, `npm`, `generic`. Verbatim from
    /// the server; never mapped to a prettier word here, because the word the
    /// server uses is the word the operator configured.
    pub format: String,
    /// How many distinct packages the listing found in it. `0` is an answer and
    /// is printed as one — an empty repository is a normal state.
    pub packages: usize,
}

/// **What `/-/releases` answers: the roster and the rows, in one document.**
///
/// One fetch, because it is one question — *what is in this server* — and two
/// fetches would be two chances for the page to render half an answer.
///
/// It is an object and not an array, and that is a wire change with a reason:
/// the array could only ever carry artifacts, so a server with repositories and
/// no artifacts had no way to say so.
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
pub struct Catalogue {
    /// Every repository a stranger may list. Already ordered by the caller.
    pub repositories: Vec<Repository>,
    /// The newest version of each package, already ranked and ordered by the
    /// caller. See the module note on why this crate ranks nothing.
    pub releases: Vec<Release>,
    /// ★ **The most recently changed packages, newest first.**
    ///
    /// A different question from [`releases`](Self::releases), and it needed a
    /// different list rather than a re-sort of that one: "the latest version of
    /// every package, in name order" and "what moved here lately" are two
    /// answers, and a visitor wants both — one to point a package manager at,
    /// one to see whether the server is alive.
    ///
    /// Only rows whose backend could answer a publication time are in it, so it
    /// is legitimately EMPTY on a server whose repositories are all
    /// znippy-backed. Empty means "nothing here can tell me when", and the page
    /// draws no section at all rather than a section full of blanks — which is
    /// the same choice the roster makes for a server with no artifacts.
    ///
    /// `default` so a document written before this field existed still parses.
    #[serde(default)]
    pub recent: Vec<Release>,
}

/// How a namespaced coordinate becomes the single string in the middle column.
///
/// `Some("org.apache.arrow") + "arrow-vector"` -> `org.apache.arrow/arrow-vector`;
/// `None + "serde"` -> `serde`. An empty namespace is the same as none — some
/// backends hand back `Some("")` and a leading `/` in the column would be a
/// visible bug for an invisible cause.
pub fn artifact_name(namespace: Option<&str>, name: &str) -> String {
    match namespace {
        Some(ns) if !ns.is_empty() => format!("{ns}/{name}"),
        _ => name.to_string(),
    }
}

/// Fold every `(repository, artifact, version)` down to the newest version of
/// each `(repository, artifact)`, using `newer` to compare.
///
/// `newer(a, b)` must answer the ordering of `a` against `b` as VERSIONS. Pass
/// `retention::cmp_version`; do not pass `str::cmp`, which puts `1.9.0` after
/// `1.10.0` and is the whole reason this is a parameter.
///
/// The result is sorted by repository, then artifact — a stable order, so the
/// page does not reshuffle between two loads of an unchanged server.
pub fn latest_by<F>(rows: impl IntoIterator<Item = Release>, newer: F) -> Vec<Release>
where
    F: Fn(&str, &str) -> Ordering,
{
    // A BTreeMap, not a HashMap: the key order IS the output order, so the
    // sort is free and cannot be forgotten.
    //
    // The value carries the chosen row's publication time along with its
    // version, because the two belong to the SAME artifact: keeping the version
    // of one row and the time of another would print a date that never applied
    // to the version beside it.
    let mut best: BTreeMap<(String, String), (String, Option<i64>)> = BTreeMap::new();
    for r in rows {
        let key = (r.repository, r.artifact);
        match best.get(&key) {
            // `Greater` only — a tie keeps the FIRST one seen, so a repository
            // that somehow lists one version twice does not flap between loads.
            Some((have, _)) if newer(&r.version, have) != Ordering::Greater => {}
            _ => {
                best.insert(key, (r.version, r.published_unix_ms));
            }
        }
    }
    best.into_iter()
        .map(
            |((repository, artifact), (version, published_unix_ms))| Release {
                repository,
                artifact,
                version,
                published_unix_ms,
            },
        )
        .collect()
}

/// **The most recently changed packages, newest first — one row per package.**
///
/// This is the other half of [`latest_by`]. That one answers "the newest
/// version of each package, in a stable name order"; this one answers "what
/// moved here lately", and until [`Release::published_unix_ms`] existed it
/// could not be answered at all — the page showed the latest VERSION of each
/// package and called it recency, which it is not.
///
/// Three rules, and each of them is a decision:
///
/// 1. **A row with no time is not in the list.** It is not ranked as old and it
///    is not ranked as new: a backend that cannot answer has said nothing, and
///    putting its packages at one end of a "recent" list would be printing a
///    fabricated zero with extra steps. A server whose repositories are all
///    znippy-backed therefore gets an empty list, and the page draws nothing —
///    which is the truthful page for that server.
/// 2. **One row per `(repository, artifact)`**, keeping the package's most
///    recent publication. The question is about packages, not versions; ten
///    versions of one crate published this morning are one thing that changed,
///    and without the fold they would fill the whole list.
/// 3. **Ties break on `(repository, artifact)`**, so two packages published in
///    the same millisecond keep a fixed order and the page does not reshuffle
///    between two loads of an unchanged server. (This is also why the field is
///    milliseconds and not seconds: same-second publications are ordinary.)
///
/// `limit == 0` means no cap.
pub fn most_recently_changed(
    rows: impl IntoIterator<Item = Release>,
    limit: usize,
) -> Vec<Release> {
    let mut best: BTreeMap<(String, String), Release> = BTreeMap::new();
    for r in rows {
        // Rule 1: an unknown time is not a date. Skipped before it can be sorted.
        let Some(ms) = r.published_unix_ms else {
            continue;
        };
        let key = (r.repository.clone(), r.artifact.clone());
        match best.get(&key) {
            Some(have) if have.published_unix_ms.unwrap_or(i64::MIN) >= ms => {}
            _ => {
                best.insert(key, r);
            }
        }
    }

    let mut out: Vec<Release> = best.into_values().collect();
    // Newest first, then the stable name order the BTreeMap already had.
    out.sort_by(|a, b| {
        b.published_unix_ms
            .cmp(&a.published_unix_ms)
            .then_with(|| a.repository.cmp(&b.repository))
            .then_with(|| a.artifact.cmp(&b.artifact))
    });
    if limit != 0 && out.len() > limit {
        out.truncate(limit);
    }
    out
}

// ─────────────────────────────────────────────────────────────────────────────
// Who this server is
// ─────────────────────────────────────────────────────────────────────────────

/// ★ **WHICH OF THE THREE STATES the door is in — the third one is new.**
///
/// There were two: no login at all ([`Front::login`] is `None`), and a login
/// somewhere else on this origin (a link the button follows). Neither of them
/// is what holger.rs needs, because on holger.rs the console IS this origin:
/// the link sent a visitor to `/login`, a second page with the same button on
/// it, and the reader pressed the same thing twice to reach one ceremony.
///
/// So there is a third: the ceremony runs **on this page**. The page holds the
/// WebAuthn call itself — `navigator.credentials.get` against the paths the
/// server names — and there is no navigation until it has succeeded.
///
/// It is a field and not a separate type because the airgapped appliance
/// serves this same crate with `/auth/*` absent entirely. The appliance
/// answers [`Door::Link`] or `None` and the ceremony code on the page is never
/// reached; nothing about the ceremony is compiled into the appliance's
/// choices, it is markup and script that a server has to opt into by naming
/// three paths.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Door {
    /// The button is a link: press it and the browser goes to `start`.
    ///
    /// The default, and it is the default on purpose: a `/-/front` document
    /// written before this field existed deserialises as a link, which is what
    /// it was.
    #[default]
    Link,
    /// The ceremony is HERE. `start`, `finish` and `next` are all rooted paths
    /// on this origin and the page never leaves until `finish` said yes.
    Here,
}

/// The door a browser can actually walk through, if there is one.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Login {
    /// What the button says. **The server's word, not the page's.**
    pub label: String,
    /// Where the button goes. A same-site rooted path; the page refuses
    /// anything else, so a misconfigured server cannot turn the button into an
    /// open redirect.
    ///
    /// For [`Door::Here`] this is the ceremony's START path, fetched rather
    /// than navigated to.
    pub start: String,
    /// Which of the two doors this is. Defaulted, so an older document still
    /// reads.
    #[serde(default)]
    pub door: Door,
    /// [`Door::Here`] only: where the signed assertion is POSTed. `None` for a
    /// link, and the page treats a `Here` with no `finish` as a link, because
    /// a ceremony with nowhere to finish is not a ceremony.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub finish: Option<String>,
    /// [`Door::Here`] only: where the browser goes once the session cookie is
    /// set. A same-site rooted path like the others; `None` means `/`.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub next: Option<String>,
    /// [`Door::Here`] only: where the FIRST key is enrolled, when this server
    /// offers enrolment on this page at all.
    ///
    /// ★ A ceremony with no way to enrol a key is one page short of useful.
    /// MEASURED on holger.rs 2026-09-20: the button worked, called the door,
    /// and the door answered "no passkey is registered yet" — true, and a dead
    /// end, because nothing was enrolled and the only place to enrol was a
    /// second page again.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub enrol: Option<Enrolment>,
    /// [`Door::Here`] only: what the button says to a tab that already holds a session
    /// ([`SIGNED_IN_LABEL`], HOL-LOGIN-LOOP). The page asks holger's session door once and, signed
    /// in, the button opens `next` with no second ceremony. `None`: the page never asks.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub signed_in: Option<String>,
    /// The line beside [`Login::signed_in`], before the person's name ([`SIGNED_IN_AS`]).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub signed_in_as: Option<String>,
}

/// **Where a passkey is ENROLLED, as three rooted paths.**
///
/// Separate from the login ceremony because they are separate acts with
/// separate authority: logging in proves you already hold a key, enrolling
/// mints one, and the second is gated by a break-glass admin token or a
/// single-use invite. The page's panel is collapsed and only ever appears when
/// a server hands over all three.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Enrolment {
    /// `GET`, with `?user=` and optionally `?invite=`, plus an
    /// `Authorization: Bearer <admin token>` when there is no invite.
    pub start: String,
    /// `POST` — the attestation.
    pub finish: String,
    /// `GET` — reports `bootstrap_spent`. ★ Once the first admin key exists the
    /// break-glass token is spent, so the panel that names it is a lie to
    /// everyone who reads it, and the page takes the panel away on this signal.
    pub status: String,
}

/// What the button says when the door is the console.
///
/// Here rather than in the server for the reason [`Front::no_login_refusal`] is
/// here: the page prints it verbatim, so it exists in one place and a test can
/// hold the page to it.
///
/// "Log in" (A11 fix 1, 2026-10-23): Gunnar's word for the half of the split row beside
/// "Sign up account"; it opens the sign-in page.
pub const CONSOLE_LABEL: &str = "Log in";

/// ★ **What the button says when the ceremony is on this page, and it is the
/// estate's word.**
///
/// Byte-for-byte gunnar's button
/// (`gunnar-ui/crates/gunnar-ui-server/src/auth/login.html`, "Log in" since LOGIN-1): the
/// two fronts in one estate name the same act the same way, beside "Sign up account".
/// "Open the console" describes a navigation, and there is no navigation any more.
pub const PASSKEY_LABEL: &str = "Log in";

/// ★ **What the button says to a person who is already signed in** (HOL-LOGIN-LOOP, holger.rs
/// 2026-10-30, "login → login again"): the front page offered "Log in" to a signed-in tab, so the
/// person signed in again, landed on the console, came back and was offered "Log in" again. Here
/// the press IS a navigation (to `next`), so the words say so; and the line beside it names who.
pub const SIGNED_IN_LABEL: &str = "Open the console";
/// The line the page shows beside [`SIGNED_IN_LABEL`], before the person's name.
pub const SIGNED_IN_AS: &str = "Signed in as ";

/// ★ **holger's passkey sign-in, as the console carries it on this origin** (HOLDIRECT, owner
/// 2026-10-28): the start the front page's press asks (`POST`, a JSON body), and the finish
/// the assertion goes to. The console's server turns the finish's answer into its session
/// cookie, so the front page's press signs in exactly as `/login`'s does.
pub const PASSKEY_LOGIN_START: &str = "/-/auth/passkey/login/start";
/// The finish of [`PASSKEY_LOGIN_START`]'s ceremony.
pub const PASSKEY_LOGIN_FINISH: &str = "/-/auth/passkey/login/finish";
/// **Where a sign-in from the front page lands**: the console's home (holger-ui's
/// `CONSOLE_HOME`), never `/`, which is this public page again.
pub const CONSOLE_HOME: &str = "/overview";

/// ★ **The console door, or `None` — and `None` is the answer for anything the
/// PAGE would refuse to follow.**
///
/// The page's button follows only a same-site rooted path (see the `btn.onclick`
/// guard in `assets/front.html`), which is the open-redirect defence and is not
/// negotiable. So an operator who points this at `https://console.example.com`
/// would get a button that is enabled, labelled, and does **nothing at all** on
/// click — strictly worse than the honest dead button
/// [`Front::no_login_refusal`] explains, because there is no sentence anywhere
/// saying why.
///
/// This constructor therefore applies the page's own rule **server-side**, and
/// answers `None` for every value the page would silently drop. The two rules
/// are asserted equal by
/// [`the_rust_rule_and_the_pages_rule_refuse_the_same_values`] — one rule, two
/// languages, held together the way this crate holds its sentences together.
///
/// Note what this means for a deployment, because it is a real constraint and
/// not a detail: **the console must be reachable at a path on THIS server's
/// origin.** A console on its own host has no rooted path from here, and the
/// honest answer for it is the one the page already gives.
pub fn console_login(path: &str) -> Option<Login> {
    if !same_site_rooted_path(path) {
        return None;
    }
    Some(Login {
        label: CONSOLE_LABEL.to_string(),
        start: path.to_string(),
        door: Door::Link,
        finish: None,
        next: None,
        enrol: None,
        signed_in: None,
        signed_in_as: None,
    })
}

/// ★ **The third state: the passkey ceremony, on this page.**
///
/// `start` and `finish` are the server's own ceremony doors and `next` is where
/// a successful login lands. All three go through `same_site_rooted_path`,
/// the same rule [`console_login`] applies, and **any one of them failing it
/// answers `None`** — not a partly-wired ceremony. A page that fetched a
/// ceremony from one origin and posted the assertion to another is not a door
/// with a bug in it, it is a different thing entirely.
///
/// The label is [`PASSKEY_LABEL`] and the server does not get to choose it: the
/// button's words and the button's behaviour are one decision, and a server
/// that could say "Open the console" over a ceremony that never navigates would
/// be lying to the reader in the one place the reader is looking.
pub fn passkey_login_here(start: &str, finish: &str, next: &str) -> Option<Login> {
    if !same_site_rooted_path(start)
        || !same_site_rooted_path(finish)
        || !same_site_rooted_path(next)
    {
        return None;
    }
    Some(Login {
        label: PASSKEY_LABEL.to_string(),
        start: start.to_string(),
        door: Door::Here,
        finish: Some(finish.to_string()),
        next: Some(next.to_string()),
        enrol: None,
        signed_in: Some(SIGNED_IN_LABEL.to_string()),
        signed_in_as: Some(SIGNED_IN_AS.to_string()),
    })
}

/// ★ **The front page's Log in on a server whose console is on this origin** (HOLDIRECT, owner
/// 2026-10-28: "Log in" went to `/login`, a second page with a second "Log in").
///
/// `console` is the console's path (`HOLGER_CONSOLE_PATH`), `passkeys` whether this server
/// runs the passkey sign-in. With both, the press runs holger's ceremony HERE
/// ([`passkey_login_here`] at [`PASSKEY_LOGIN_START`] / [`PASSKEY_LOGIN_FINISH`]) and lands on
/// [`CONSOLE_HOME`]; `/login` stays for a direct link and for enrolment. Without the passkey
/// sign-in the button stays the link it was, and a path the page would refuse is no door.
pub fn console_sign_in(console: &str, passkeys: bool) -> Option<Login> {
    let link = console_login(console)?;
    if !passkeys {
        return Some(link);
    }
    passkey_login_here(PASSKEY_LOGIN_START, PASSKEY_LOGIN_FINISH, CONSOLE_HOME).or(Some(link))
}

/// ★ **Enrolment, on the same page — or `None`, which is a complete answer.**
///
/// Applies the one rule to all three paths and refuses the whole panel if any
/// of them fails it, for the reason [`passkey_login_here`] refuses a half-wired
/// ceremony: an enrolment that started here and posted its attestation
/// elsewhere is not a door with a bug in it.
///
/// `None` is what the appliance gets and what any server without `/auth/*`
/// gets: no field on the wire, no panel on the page, no form that cannot be
/// served. It is also what a [`Door::Link`] login gets — a server whose login
/// lives on another page enrols on that page too.
pub fn with_enrolment(login: Login, start: &str, finish: &str, status: &str) -> Login {
    if login.door != Door::Here
        || !same_site_rooted_path(start)
        || !same_site_rooted_path(finish)
        || !same_site_rooted_path(status)
    {
        return login;
    }
    Login {
        enrol: Some(Enrolment {
            start: start.to_string(),
            finish: finish.to_string(),
            status: status.to_string(),
        }),
        ..login
    }
}

/// The page's `btn.onclick` rule, in Rust.
///
/// One leading slash, no authority, and no character the URL parser strips
/// before it decides what the value even is: `//host` and `/\host` are off-site
/// in every engine, and a value carrying a space, a tab, a newline or a NUL is
/// one whose meaning is settled after those are removed.
fn same_site_rooted_path(path: &str) -> bool {
    !path.is_empty()
        && path.starts_with('/')
        && !path.starts_with("//")
        && !path.starts_with("/\\")
        // The page's class is `[\x00-\x20\x7f]` — written in `assets/front.html`
        // as literal control bytes, which is why that file reads as binary.
        // `is_ascii_whitespace` is NOT this set (it omits NUL, the other C0
        // controls and DEL), so the range is spelled out rather than borrowed.
        && !path.chars().any(|c| (c as u32) <= 0x20 || c as u32 == 0x7f)
}

/// What `/-/front` answers: everything on the page that is a fact about THIS
/// server rather than about holger.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Front {
    /// The base URL a package manager is pointed at. `None` when the server
    /// was not told its own public name — printed by the server or not at all,
    /// because a page that guessed it from `location.origin` would print the
    /// proxy's address on any deployment behind one.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub base_url: Option<String>,
    /// ★ **`None` is an ANSWER, and it is the honest one today.**
    ///
    /// holger-server authenticates with mTLS, OIDC and bearer tokens — doors
    /// for `cargo`, `pip`, `docker` and the native console. There is no
    /// browser session, so there is no button, and the page says
    /// [`ec::FRONT_NO_BROWSER_LOGIN`] by name rather than offering one.
    ///
    /// It is serialised even when null (no `skip_serializing_if`) precisely so
    /// the page can tell a server with no login from a server too old to have
    /// the field.
    pub login: Option<Login>,
}

impl Front {
    /// A server that has told us nothing but its own address.
    pub fn anonymous(base_url: Option<String>) -> Self {
        Front {
            base_url,
            login: None,
        }
    }

    /// The refusal the page prints when [`Front::login`] is `None`, by name
    /// and with its code. Kept here rather than only in the page's JavaScript
    /// so the words exist in one place and a test can hold them to it.
    pub fn no_login_refusal() -> String {
        format!(
            "{}: this server offers no browser login. Its doors are mTLS, OIDC and bearer — \
             for cargo, pip, docker and the console, not for a tab.",
            ec::FRONT_NO_BROWSER_LOGIN.code
        )
    }
}

/// ★ **The three sentences the PAGE prints on its own.**
///
/// They are raised in JavaScript, where no Rust compiler can see them and the
/// frozen registry cannot be consulted — which is exactly how a page ends up
/// showing a code nobody allocated, or the same failure worded two ways in two
/// places. So each one is built HERE, from the registry row, and the tests
/// assert the page carries the string this produces. Change the sentence here
/// and the test tells you the page has drifted; change it in the page and the
/// test tells you the same thing.
///
/// This is also what makes the rows honest to the wiring guard: a code marked
/// `wired` must be referenced from the file it claims, and these are the
/// references.
pub fn page_refusals() -> [(&'static ec::ErrCode, String); 3] {
    [
        (&ec::FRONT_NO_BROWSER_LOGIN, Front::no_login_refusal()),
        (
            &ec::FRONT_READS_GATED,
            format!(
                "{}: this server gates reads behind a credential, so its front page cannot name \
                 itself. Configure `require_auth_for_reads: false`, or read the console instead.",
                ec::FRONT_READS_GATED.code
            ),
        ),
        (
            &ec::FRONT_DOOR_ABSENT,
            format!(
                "{}: this build of holger-server has no `/-/front` door. The page is newer than \
                 the server behind it.",
                ec::FRONT_DOOR_ABSENT.code
            ),
        ),
    ]
}

// ─────────────────────────────────────────────────────────────────────────────
// The door
// ─────────────────────────────────────────────────────────────────────────────

/// One answer: a status, a content type, headers that matter, and the bytes.
#[derive(Debug, Clone, PartialEq)]
pub struct Reply {
    pub status: u16,
    pub content_type: &'static str,
    /// `Cache-Control`. Named rather than implied: the page and the JSON must
    /// NOT be cached (a stale release table is a lie about what is published)
    /// and the picture must be, because it is 300 kB that never changes
    /// without a redeploy.
    pub cache_control: &'static str,
    pub body: Vec<u8>,
}

impl Reply {
    fn html(body: &str) -> Reply {
        Reply {
            status: 200,
            content_type: "text/html; charset=utf-8",
            cache_control: "no-store",
            body: body.as_bytes().to_vec(),
        }
    }
    fn json(body: String) -> Reply {
        Reply {
            status: 200,
            content_type: "application/json",
            cache_control: "no-store",
            body: body.into_bytes(),
        }
    }
    fn webp(bytes: &'static [u8]) -> Reply {
        Reply {
            status: 200,
            content_type: "image/webp",
            // A year, and immutable: the file's content is compiled into the
            // binary, so it cannot change without a new binary, and a new
            // binary is a new deploy.
            cache_control: "public, max-age=31536000, immutable",
            body: bytes.to_vec(),
        }
    }
    /// A mark, cached like the picture: compiled into the binary, so it cannot
    /// change without a new deploy.
    fn mark(bytes: &'static [u8], content_type: &'static str) -> Reply {
        Reply {
            status: 200,
            content_type,
            cache_control: "public, max-age=31536000, immutable",
            body: bytes.to_vec(),
        }
    }
    fn refused(status: u16, code: &ec::ErrCode, detail: &str) -> Reply {
        Reply {
            status,
            content_type: "application/json",
            cache_control: "no-store",
            body: serde_json::json!({ "code": code.code, "error": detail })
                .to_string()
                .into_bytes(),
        }
    }
}

/// What the server must hand the door to answer it.
///
/// It is a struct and not four arguments so that adding a fact to the front
/// page is a field here and a line in the caller, never a new function.
pub struct Doors<'a> {
    pub front: &'a Front,
    /// The roster and the rows. Already the latest, already ordered. The door
    /// does not rank — see the module note on why.
    pub catalogue: &'a Catalogue,
}

/// ★ **The whole public surface, as a pure function.**
///
/// `None` means "not mine" — the caller falls through to its own routing, so
/// mounting this cannot shadow a repository. Every path it DOES own is under
/// `/` or the already-reserved `/-/` namespace, which can never route into a
/// repository.
pub fn route(method: &str, path: &str, doors: &Doors<'_>) -> Option<Reply> {
    let owned = matches!(
        path,
        "/" | PICTURE_PATH | VETRA_PATH | IGNALINA_PATH | FRONT_PATH | RELEASES_PATH
    );
    if !owned {
        return None;
    }

    // ★ Read-only, and the refusal is by name. A `POST /` that fell through to
    // the repository router would be a write attempt against a repository
    // called "" — a 404 that looks like a typo instead of a method that is not
    // allowed here.
    if method != "GET" && method != "HEAD" {
        return Some(Reply::refused(
            405,
            &ec::FRONT_METHOD_NOT_ALLOWED,
            "the front door answers GET and HEAD only",
        ));
    }

    let mut reply = match path {
        "/" => Reply::html(page()),
        PICTURE_PATH => Reply::webp(PICTURE),
        VETRA_PATH => Reply::mark(VETRA_MARK, "image/svg+xml"),
        IGNALINA_PATH => Reply::mark(IGNALINA_MARK, "image/png"),
        FRONT_PATH => {
            Reply::json(serde_json::to_string(doors.front).unwrap_or_else(|_| "{}".to_string()))
        }
        RELEASES_PATH => Reply::json(
            serde_json::to_string(doors.catalogue)
                .unwrap_or_else(|_| r#"{"repositories":[],"releases":[]}"#.to_string()),
        ),
        _ => unreachable!("`owned` above is the same list"),
    };
    // HEAD is the same answer without the bytes. Written here rather than left
    // to the caller, because a HEAD that returned the body would be the kind
    // of thing nothing notices until a health check downloads the picture
    // every thirty seconds.
    if method == "HEAD" {
        reply.body.clear();
    }
    Some(reply)
}

/// The refusal for a server whose reads are gated, so `/-/front` cannot be
/// read at all. Produced by the CALLER, which is the half that knows about
/// `require_auth_for_reads`; it lives here so the code and the words are in
/// the same place as the page that prints them.
pub fn reads_gated() -> Reply {
    Reply::refused(
        403,
        &ec::FRONT_READS_GATED,
        "this server gates reads behind a credential, so its front page cannot name itself",
    )
}

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

    /// A comparator that is WRONG on purpose — plain string order, which puts
    /// `1.9.0` above `1.10.0`. Used to prove the fold asks the comparator it
    /// was given and holds no opinion of its own.
    fn lexicographic(a: &str, b: &str) -> Ordering {
        a.cmp(b)
    }

    /// A toy numeric comparator standing in for `retention::cmp_version`, so
    /// the grouping can be tested without depending on `server/lib`.
    fn numeric(a: &str, b: &str) -> Ordering {
        let parts = |v: &str| {
            v.split('.')
                .map(|p| p.parse::<u64>().unwrap_or(0))
                .collect::<Vec<_>>()
        };
        parts(a).cmp(&parts(b))
    }

    fn rel(repo: &str, art: &str, ver: &str) -> Release {
        Release {
            repository: repo.into(),
            artifact: art.into(),
            version: ver.into(),
            // The default fixture is a row whose backend could not answer a
            // time — the majority case on a znippy-backed server — so every
            // test that does not care about recency exercises the unknown.
            published_unix_ms: None,
        }
    }

    /// The same row, published at `ms`.
    fn rel_at(repo: &str, art: &str, ver: &str, ms: i64) -> Release {
        Release {
            published_unix_ms: Some(ms),
            ..rel(repo, art, ver)
        }
    }

    /// ★ **"Most recently changed" is newest first, one row per package, and an
    /// unknown time is not a date.**
    ///
    /// This is the test the field was added for. Before it, the page showed the
    /// latest VERSION of each package and there was no timestamp anywhere in
    /// the artifact model to sort by.
    #[test]
    fn the_recency_list_is_newest_first_one_row_per_package() {
        let rows = vec![
            rel_at("bundles", "site-a", "1", 1_000),
            rel_at("bundles", "site-a", "2", 3_000), // same package, newer
            rel_at("crates", "serde", "1.0.0", 2_000),
            rel("crates", "quiet", "9.9.9"), // no time: not in the list at all
        ];
        let out = most_recently_changed(rows, 0);
        assert_eq!(
            out.iter().map(|r| r.artifact.as_str()).collect::<Vec<_>>(),
            vec!["site-a", "serde"],
            "newest first, and the package folded to its most recent publication"
        );
        assert_eq!(
            out[0].version, "2",
            "the fold kept the VERSION that belongs to that time"
        );
        assert!(
            out.iter().all(|r| r.artifact != "quiet"),
            "a row whose backend could not answer a time must not be ranked — not as old, \
             not as new; an unknown is not a date"
        );
    }

    /// A tie is broken by name, so the page does not reshuffle between two
    /// loads of a server that has not changed.
    #[test]
    fn a_tie_in_the_recency_list_keeps_a_fixed_order() {
        let rows = vec![
            rel_at("z-repo", "b", "1", 5_000),
            rel_at("a-repo", "b", "1", 5_000),
            rel_at("a-repo", "a", "1", 5_000),
        ];
        let first = most_recently_changed(rows.clone(), 0);
        let again = most_recently_changed(rows.into_iter().rev().collect::<Vec<_>>(), 0);
        assert_eq!(
            first, again,
            "the order must not depend on the order the rows arrived in"
        );
        assert_eq!(
            first
                .iter()
                .map(|r| format!("{}/{}", r.repository, r.artifact))
                .collect::<Vec<_>>(),
            vec!["a-repo/a", "a-repo/b", "z-repo/b"]
        );
    }

    /// A server whose every backend answers "unknown" gets an EMPTY list, not a
    /// list of packages dated 1970.
    #[test]
    fn a_server_that_knows_no_times_has_no_recency_list() {
        let rows = vec![
            rel("crates", "serde", "1.0.0"),
            rel("bundles", "site-a", "3"),
        ];
        assert!(most_recently_changed(rows, 0).is_empty());
    }

    /// The cap is applied AFTER the ranking, so a limit of one names the single
    /// most recent package and not whichever one was listed first.
    #[test]
    fn the_recency_cap_keeps_the_newest_and_not_the_first_seen() {
        let rows = vec![
            rel_at("crates", "old", "1", 1_000),
            rel_at("crates", "new", "1", 9_000),
        ];
        let out = most_recently_changed(rows, 1);
        assert_eq!(out.len(), 1);
        assert_eq!(out[0].artifact, "new");
    }

    /// `latest_by` keeps the time that belongs to the version it chose. Keeping
    /// one row's version beside another row's date would print a date that
    /// never applied to it.
    #[test]
    fn the_version_fold_carries_the_time_of_the_version_it_kept() {
        let rows = vec![
            rel_at("crates", "serde", "1.9.0", 1_000),
            rel_at("crates", "serde", "1.10.0", 7_000),
        ];
        let out = latest_by(rows, numeric);
        assert_eq!(out.len(), 1);
        assert_eq!(out[0].version, "1.10.0");
        assert_eq!(out[0].published_unix_ms, Some(7_000));
    }

    /// A `/-/releases` document written before either field existed still
    /// parses, and reads as "no times known" rather than failing the page.
    #[test]
    fn a_catalogue_from_before_these_fields_still_parses() {
        let old =
            r#"{"repositories":[],"releases":[{"repository":"r","artifact":"a","version":"1"}]}"#;
        let c: Catalogue = serde_json::from_str(old).expect("an older document must still parse");
        assert_eq!(c.releases[0].published_unix_ms, None);
        assert!(c.recent.is_empty());
    }

    /// A row with no time carries no key, so nothing downstream can read a
    /// `null` as a value.
    #[test]
    fn an_unknown_time_is_absent_from_the_json_and_not_a_null() {
        let doc = serde_json::to_string(&rel("r", "a", "1")).unwrap();
        assert!(!doc.contains("published_unix_ms"), "{doc}");
        let doc = serde_json::to_string(&rel_at("r", "a", "1", 42)).unwrap();
        assert!(doc.contains("\"published_unix_ms\":42"), "{doc}");
    }

    // ── The console door ─────────────────────────────────────────────────────

    /// A path the page will follow becomes a button, labelled in the server's
    /// own word.
    #[test]
    fn a_rooted_path_becomes_the_console_button() {
        let login = console_login("/console").expect("a rooted path is a door");
        assert_eq!(login.start, "/console");
        assert_eq!(login.label, CONSOLE_LABEL);
    }

    /// ★ **The Rust rule and the PAGE's rule refuse exactly the same values.**
    ///
    /// This is the test the whole constructor exists for. The page's
    /// `btn.onclick` drops a value it will not follow **silently** — no
    /// sentence, no code, just a button that does nothing — so a server that
    /// answered with one would produce the one failure state the front page's
    /// design is otherwise free of. Rather than trust two prose descriptions of
    /// one rule, the page's own class is read out of [`PAGE`] and applied here.
    ///
    /// It is spelled `\x00-\x20` plus `\x7f`, and since 2026-09-20 it is
    /// written in the HTML as JS ESCAPES and not as literal control bytes.
    /// That is not a style preference, it is the fix for a page that hung:
    /// the literal NUL made `assets/front.html` read as binary to `file(1)`,
    /// and in a browser the HTML tokenizer's script-data state replaces a
    /// U+0000 with U+FFFD, so the engine parsed `/[\u{fffd}-\u{20}\u{7f}]/`,
    /// refused it as "range out of order in character class", and threw away
    /// the WHOLE inline script — the `/-/front` fetch with it. The page then
    /// sat on its literal `reading the server …` with the button still
    /// `disabled` for ever. [`the_page_carries_no_raw_control_bytes`] is the
    /// guard that keeps a literal from coming back.
    #[test]
    fn the_rust_rule_and_the_pages_rule_refuse_the_same_values() {
        // The page still carries the guard this mirrors. If the button's
        // handler is rewritten, this test must be re-read, not re-blessed.
        assert!(
            PAGE.contains("if (!start.startsWith('/') || start.startsWith('//') || start.startsWith('/\\\\')) return false;"),
            "the page's same-site guard has moved; console_login may no longer mirror it"
        );
        assert!(
            PAGE.contains(r"if (/[\x00-\x20\x7f]/.test(start)) return false;"),
            "the page's control-character class has changed; same_site_rooted_path is now a second opinion"
        );

        for refused in [
            "",                            // the page returns early on empty
            "https://console.example.com", // an off-site URL — the tempting mistake
            "console",                     // a relative path is not rooted
            "//evil.example.com",          // protocol-relative: off-site everywhere
            "/\\evil.example.com",         // backslash authority: off-site everywhere
            "/console\u{0}",               // NUL
            "/console\u{9}",               // tab
            "/console\u{a}",               // newline
            "/con sole",                   // space
            "/console\u{7f}",              // DEL — the one the first draft missed
        ] {
            assert!(
                console_login(refused).is_none(),
                "{refused:?} must not become a button: the page would drop it in silence"
            );
        }
    }

    /// An unconfigured console leaves the page exactly as it is today — the
    /// named refusal, not a button to nowhere. A self-hoster who runs no
    /// console must not be offered one.
    #[test]
    fn no_console_configured_is_still_the_named_refusal() {
        let f = Front {
            base_url: None,
            login: None,
        };
        let body = serde_json::to_string(&f).unwrap();
        assert!(body.contains("\"login\":null"), "{body}");
        assert!(Front::no_login_refusal().starts_with(ec::FRONT_NO_BROWSER_LOGIN.code));
    }

    /// ★ **The fold has no opinion about versions.** The same input with two
    /// comparators gives two different answers — which is the proof that the
    /// ranking comes from the caller and not from this crate.
    #[test]
    fn the_comparator_decides_and_this_crate_does_not() {
        let rows = vec![
            rel("crates", "serde", "1.9.0"),
            rel("crates", "serde", "1.10.0"),
        ];
        assert_eq!(latest_by(rows.clone(), numeric)[0].version, "1.10.0");
        assert_eq!(latest_by(rows, lexicographic)[0].version, "1.9.0");
    }

    /// One row per `(repository, artifact)`, and the same artifact name in two
    /// repositories is two rows — a mirror and a local store legitimately hold
    /// different versions of `serde`, and collapsing them would hide it.
    #[test]
    fn one_row_per_repository_and_artifact() {
        let out = latest_by(
            vec![
                rel("crates", "serde", "1.0.1"),
                rel("crates", "serde", "1.0.9"),
                rel("mirror", "serde", "1.0.4"),
                rel("crates", "tokio", "1.2.0"),
            ],
            numeric,
        );
        assert_eq!(out.len(), 3);
        assert_eq!(
            out.iter()
                .map(|r| (
                    r.repository.as_str(),
                    r.artifact.as_str(),
                    r.version.as_str()
                ))
                .collect::<Vec<_>>(),
            vec![
                ("crates", "serde", "1.0.9"),
                ("crates", "tokio", "1.2.0"),
                ("mirror", "serde", "1.0.4")
            ]
        );
    }

    /// The order is stable and does not depend on the order rows arrived in.
    /// A page that reshuffled between two loads of an unchanged server would
    /// read as a server that is changing.
    #[test]
    fn the_order_does_not_depend_on_the_input_order() {
        let a = vec![rel("b", "y", "1"), rel("a", "z", "1"), rel("a", "y", "1")];
        let mut b = a.clone();
        b.reverse();
        assert_eq!(latest_by(a, numeric), latest_by(b, numeric));
    }

    #[test]
    fn a_namespace_joins_the_name_with_one_slash_and_an_empty_one_does_not() {
        assert_eq!(
            artifact_name(Some("org.apache.arrow"), "arrow-vector"),
            "org.apache.arrow/arrow-vector"
        );
        assert_eq!(artifact_name(Some("@scope"), "pkg"), "@scope/pkg");
        assert_eq!(artifact_name(None, "serde"), "serde");
        assert_eq!(
            artifact_name(Some(""), "serde"),
            "serde",
            "an empty namespace put a slash on the front"
        );
    }

    // ── the door ─────────────────────────────────────────────────────────────

    fn doors() -> (Front, Catalogue) {
        (
            Front::anonymous(Some("https://holger.rs".into())),
            Catalogue {
                repositories: vec![Repository {
                    name: "bundles".into(),
                    format: "generic".into(),
                    packages: 1,
                }],
                releases: vec![rel("bundles", "site-a", "3")],
                // The fixture server knows no publication times.
                recent: Vec::new(),
            },
        )
    }

    #[test]
    fn the_page_is_served_at_the_root_and_is_not_cached() {
        let (f, r) = doors();
        let d = Doors {
            front: &f,
            catalogue: &r,
        };
        let reply = route("GET", "/", &d).expect("the root is this door's");
        assert_eq!(reply.status, 200);
        assert_eq!(reply.content_type, "text/html; charset=utf-8");
        assert_eq!(
            reply.cache_control, "no-store",
            "the page must not be cached"
        );
        assert_eq!(reply.body, page().as_bytes());
    }

    /// The picture is cached hard, because it cannot change without a new
    /// binary — and the JSON is not, because it changes whenever anybody
    /// publishes.
    #[test]
    fn the_picture_is_cached_forever_and_the_json_never() {
        let (f, r) = doors();
        let d = Doors {
            front: &f,
            catalogue: &r,
        };
        let pic = route("GET", PICTURE_PATH, &d).unwrap();
        assert_eq!(pic.content_type, "image/webp");
        assert!(
            pic.cache_control.contains("immutable"),
            "{}",
            pic.cache_control
        );
        for p in [FRONT_PATH, RELEASES_PATH] {
            assert_eq!(
                route("GET", p, &d).unwrap().cache_control,
                "no-store",
                "{p} was cacheable"
            );
        }
    }

    /// ★ **The picture the page names is the picture the door serves.** The
    /// path is written twice — once in CSS, once as a constant — and this is
    /// what keeps the two from drifting into a broken left half that nobody
    /// notices because the layout still works.
    #[test]
    fn the_page_asks_for_the_picture_this_door_serves() {
        assert!(
            PAGE.contains(&format!("url(\"{PICTURE_PATH}\")")),
            "the page's --backdrop-image does not name {PICTURE_PATH}"
        );
        assert!(
            PAGE.contains(RELEASES_PATH),
            "the page does not fetch {RELEASES_PATH}"
        );
        assert!(
            PAGE.contains(FRONT_PATH),
            "the page does not fetch {FRONT_PATH}"
        );
    }

    /// ★ HOL-LOGIN-LOOP (holger.rs 2026-10-30, "login → login again"): **a signed-in visitor is
    /// offered the console, never "Log in" again.** The page asks holger's session door once,
    /// with the tab's own cookie (the console server's HttpOnly session), and a 200 turns the
    /// button into "Open the console" with "Signed in as <name>". It never navigates by itself:
    /// the press does.
    #[test]
    fn a_signed_in_visitor_is_offered_the_console_and_never_log_in_again() {
        let entry = &PAGE[PAGE.find("(function entry()").expect("the entry block")..];
        let ask = entry
            .find("fetch('/-/auth/passkey/session', { method: 'POST', credentials: 'same-origin'")
            .expect("the entry block asks holger's session door with the tab's cookie");
        let answered = &entry[ask..ask
            + entry[ask..]
                .find("btn.onclick")
                .expect("the button's press after the ask")];
        // The words are the server's, as every label here: the page prints what `/-/front` hands it.
        assert!(
            answered.contains("front.login.signed_in")
                && answered.contains("front.login.signed_in_as"),
            "a session turns the button into the console's, in the server's words"
        );
        assert!(
            !answered.contains("location.href =") && !answered.contains("location.replace("),
            "the session's answer never navigates by itself"
        );
        let login = console_sign_in("/overview", true).expect("the ceremony here");
        assert_eq!(
            (login.signed_in.as_deref(), login.next.as_deref()),
            (Some(SIGNED_IN_LABEL), Some(CONSOLE_HOME))
        );
        let wire = serde_json::to_value(&login).unwrap();
        assert_eq!(
            (wire["signed_in"].as_str(), wire["signed_in_as"].as_str()),
            (Some("Open the console"), Some("Signed in as ")),
            "{wire}"
        );
        assert_eq!(
            console_login("/console").unwrap().signed_in,
            None,
            "a link door never asks"
        );
    }

    /// ★ **The page carries no raw control byte, because one of them silently
    /// deleted the whole inline script.**
    ///
    /// MEASURED 2026-09-20 on the live holger.rs: `assets/front.html` held a
    /// literal U+0000 inside the `btn.onclick` guard's regex. `curl` saw a
    /// perfectly good 23 115-byte document and `/-/front` answered 200; a
    /// browser did not. The HTML tokenizer's script-data state turns U+0000
    /// into U+FFFD before the JS engine ever sees it, so the engine was handed
    /// `/[\u{fffd}-\u{20}\u{7f}]/`, whose range runs backwards. That is an
    /// early SyntaxError, which kills the ENTIRE `<script>` element — the
    /// `/-/front` fetch, the button's label, the button's `disabled = false`,
    /// all of it — and leaves the page showing its own literal placeholder
    /// `reading the server …` next to a grey button that cannot be pressed.
    ///
    /// The class is therefore spelled with escapes now, and this test is the
    /// reason a literal cannot come back. `\t`, `\n` and `\r` are the three
    /// bytes a text document is allowed to contain.
    #[test]
    fn the_page_carries_no_raw_control_bytes() {
        for (i, b) in PAGE.bytes().enumerate() {
            let allowed = matches!(b, b'\t' | b'\n' | b'\r');
            assert!(
                allowed || !(b.is_ascii_control() || b == 0x7f),
                "assets/front.html carries a raw control byte {b:#04x} at offset {i}; \
                 a browser replaces U+0000 with U+FFFD in script data and throws the whole \
                 inline script away. Write it as a JS escape."
            );
        }
    }

    /// The picture really is a WebP, and really is the one that was converted
    /// — not a PNG somebody renamed.
    #[test]
    fn the_compiled_in_picture_is_a_webp() {
        traits::test_support::assert_not_lfs_pointer("holger-front/assets/holger.webp", PICTURE);
        traits::test_support::assert_not_lfs_pointer(
            "holger-front/assets/ignalina.png",
            IGNALINA_MARK,
        );
        assert!(
            PICTURE.len() > 12,
            "the picture is empty — run `holger-ops logo`"
        );
        assert_eq!(&PICTURE[0..4], b"RIFF", "not a RIFF container");
        assert_eq!(&PICTURE[8..12], b"WEBP", "not a WebP");
    }

    /// ★ **Nothing but this door's own four paths is claimed.** A door that
    /// answered `/crates-mirror` would shadow a repository, and it would do it
    /// silently — the repository would simply stop existing.
    #[test]
    fn a_repository_path_is_never_this_doors() {
        let (f, r) = doors();
        let d = Doors {
            front: &f,
            catalogue: &r,
        };
        for p in [
            "/crates-mirror",
            "/v2/alpine/manifests/latest",
            "/-/search",
            "/healthz",
            "/bundles/x",
            "",
        ] {
            assert!(
                route("GET", p, &d).is_none(),
                "{p} was claimed by the front door"
            );
        }
    }

    /// A write against the front door is refused BY NAME with a code, not
    /// dropped into the repository router where it becomes a confusing 404.
    #[test]
    fn a_write_is_refused_by_name_with_its_code() {
        let (f, r) = doors();
        let d = Doors {
            front: &f,
            catalogue: &r,
        };
        for m in ["POST", "PUT", "DELETE", "PATCH"] {
            let reply = route(m, "/", &d).unwrap();
            assert_eq!(reply.status, 405, "{m}");
            let body = String::from_utf8(reply.body).unwrap();
            assert!(
                body.contains(ec::FRONT_METHOD_NOT_ALLOWED.code),
                "{m}: {body}"
            );
        }
    }

    #[test]
    fn head_answers_the_same_thing_without_the_bytes() {
        let (f, r) = doors();
        let d = Doors {
            front: &f,
            catalogue: &r,
        };
        for p in ["/", PICTURE_PATH, FRONT_PATH, RELEASES_PATH] {
            let get = route("GET", p, &d).unwrap();
            let head = route("HEAD", p, &d).unwrap();
            assert_eq!(head.status, get.status);
            assert_eq!(head.content_type, get.content_type);
            assert!(head.body.is_empty(), "HEAD {p} carried a body");
        }
    }

    /// The three columns go over the wire under the names the page reads, and
    /// there is no fourth.
    #[test]
    fn the_wire_carries_exactly_three_columns() {
        let (f, r) = doors();
        let d = Doors {
            front: &f,
            catalogue: &r,
        };
        let body = String::from_utf8(route("GET", RELEASES_PATH, &d).unwrap().body).unwrap();
        let doc: serde_json::Value = serde_json::from_str(&body).unwrap();
        let rows: Vec<serde_json::Map<String, serde_json::Value>> =
            serde_json::from_value(doc["releases"].clone()).unwrap();
        assert_eq!(rows.len(), 1);
        // A SET, not a sequence: `serde_json::Map` is a BTreeMap without the
        // `preserve_order` feature, so the wire order is alphabetical and
        // asserting declaration order here would only be asserting serde's
        // build configuration. The COLUMN order is the page's, and it is
        // checked where it lives — in `the_page_is_holgers`, against the
        // `<th>` headings a reader actually sees.
        let mut keys: Vec<&str> = rows[0].keys().map(|k| k.as_str()).collect();
        keys.sort_unstable();
        assert_eq!(
            keys,
            vec!["artifact", "repository", "version"],
            "the row grew or lost a column"
        );
    }

    /// ★ **`login: null` is sent, not omitted.** The page must be able to tell
    /// a server that HAS no browser login from a server too old to have the
    /// field — they need different sentences, and a skipped field makes them
    /// the same byte sequence.
    #[test]
    fn a_server_with_no_login_says_so_rather_than_saying_nothing() {
        let f = Front::anonymous(None);
        let body = serde_json::to_string(&f).unwrap();
        assert!(body.contains("\"login\":null"), "{body}");
        assert!(
            !body.contains("base_url"),
            "an absent base URL should not be sent at all: {body}"
        );
    }

    /// The refusal the page prints carries the registry's code, and the page
    /// and the Rust say the same words — one sentence, two places, held
    /// together by this.
    #[test]
    fn the_no_login_refusal_is_the_same_sentence_in_the_page_and_in_the_code() {
        let r = Front::no_login_refusal();
        assert!(r.starts_with(ec::FRONT_NO_BROWSER_LOGIN.code), "{r}");
        assert!(
            PAGE.contains(&r),
            "the page's sentence has drifted from Front::no_login_refusal():\n{r}"
        );
    }

    /// Every code this door and its page can show is a row in the FROZEN
    /// registry. A refusal with a number nobody allocated is a refusal nobody
    /// can look up.
    #[test]
    fn every_code_the_page_prints_is_in_the_registry() {
        for code in [
            ec::FRONT_METHOD_NOT_ALLOWED,
            ec::FRONT_NO_BROWSER_LOGIN,
            ec::FRONT_READS_GATED,
            ec::FRONT_DOOR_ABSENT,
        ] {
            assert_eq!(code.subsystem, "front");
            assert!(
                holger_errcode::ALL.iter().any(|c| c.code == code.code),
                "{} is not in ALL",
                code.code
            );
        }
    }

    /// ★ **Every sentence the page prints is the sentence this crate builds.**
    /// The page raises three refusals in JavaScript, where nothing checks
    /// them; this is the check. A word changed on either side fails here,
    /// naming which.
    #[test]
    fn the_pages_refusals_are_the_ones_this_crate_words() {
        for (code, sentence) in page_refusals() {
            assert!(sentence.starts_with(code.code), "{sentence}");
            assert!(
                PAGE.contains(&sentence),
                "the page has drifted from the wording of {}:\n  expected: {sentence}",
                code.code
            );
        }
    }

    /// ★ **No gunnar sentence survived the copy.** The page's shape is
    /// gunnar's login page and its words must be holger's; a page that says
    /// gunnar's sentences under holger's logo is worse than one that says
    /// nothing. This is the test that catches a paste.
    ///
    /// `vetra` and `ignalina` were on this list until 2026-09-17 and are
    /// DELIBERATELY off it now. They were never gunnar's words — they are the
    /// two companies that make both products, and the credit line is now
    /// gunnar's block verbatim BY INSTRUCTION, marks and all, so the estate's
    /// two fronts credit their makers identically instead of one crediting
    /// companies and the other listing two people and a repository URL. What
    /// this test exists to catch is a gunnar SENTENCE arriving under holger's
    /// logo; a shared maker is not that, and keeping them here would make the
    /// guard fail on the one paste that was asked for.
    ///
    /// `passkey` and `webauthn` were on this list until 2026-09-20 and are off
    /// it now for a reason that is not a relaxation: they were banned because
    /// holger-server HAD no browser session, so either word on this page could
    /// only have arrived by paste. The console's ceremony now runs ON this page
    /// ([`Door::Here`]), so "passkey" is holger's own word for holger's own
    /// door. `gunnar`, `badger` and `git server` stay: those describe a
    /// different product and can still only arrive by paste.
    #[test]
    fn the_page_says_nothing_about_gunnar() {
        let lower = PAGE.to_lowercase();
        for word in ["gunnar", "badger", "git server"] {
            assert!(!lower.contains(word), "the page still says `{word}`");
        }
    }

    /// ★ **The front page offers the sign-up as a BUTTON** (lane SU1; A11 fix 1, owner
    /// 2026-10-23: "no signup button at the front page"). Gunnar's first page is a split row,
    /// "Sign up account | Log in", two equal buttons (gunnar-ui `auth/login.html` + the metered
    /// `login-offer.*`); Holger's front page carries the same row. "Sign up account" is a link
    /// drawn as a button, to the console's `/signup`, shipped hidden and revealed only when
    /// holger's sign-up door (`/-/signup`, the ONE shop's links for this site) offers an
    /// account or a private Holger. The front door claims neither path.
    #[test]
    fn the_front_page_splits_sign_up_and_log_in_and_shows_sign_up_only_when_the_server_offers_one()
    {
        let row = PAGE
            .find("<div class=\"entry-buttons\">")
            .expect("the split row");
        // ★ LOGIN-1 + ALICA-2: the front page is where sign-up is reached, and "Sign up account"
        // opens the ACCOUNT's sign-up (`?plan=account`): /signup alone preselects nothing.
        let signup = PAGE.find("<a class=\"entry-button\" id=\"signup-link\" href=\"/signup?plan=account\" hidden>Sign up account</a>").expect("the sign-up half, a link drawn as a button, shipped hidden, to the account's sign-up");
        let login = PAGE
            .find("<button id=\"enter\" class=\"primary\" disabled>")
            .expect("the log-in half");
        assert!(
            row < signup && signup < login,
            "Sign up account first, Log in beside it, in one row"
        );
        assert!(
            PAGE.contains(".entry-buttons { display: flex; gap: .6rem; }"),
            "the row lays its halves side by side"
        );
        assert!(
            PAGE.contains(".entry-buttons > * { flex: 1 1 0; min-width: 0; }"),
            "the two halves are equal width"
        );
        assert!(
            PAGE.contains("a.entry-button[hidden] { display: none; }"),
            "a hidden half is not drawn"
        );
        assert!(
            !PAGE.contains("id=\"signup-offer\""),
            "the small text link is gone: the sign-up is the button"
        );
        assert!(
            PAGE.contains("fetch('/-/signup'"),
            "the page asks holger's sign-up door"
        );
        assert!(
            PAGE.contains("links.account_url || links.private_offered"),
            "shown only when something is sold"
        );
        assert!(
            PAGE.contains("document.getElementById('signup-link').hidden = false"),
            "the sign-up half is revealed by the door's answer"
        );
        let (f, rel) = doors();
        let d = Doors {
            front: &f,
            catalogue: &rel,
        };
        assert!(
            route("GET", "/signup", &d).is_none() && route("GET", "/-/signup", &d).is_none(),
            "neither is the front door's"
        );
    }

    /// ★ **The console door's half says "Log in"**, Gunnar's word for the same button
    /// (A11 fix 1): the button beside "Sign up account" opens the sign-in page.
    #[test]
    fn the_console_door_says_log_in() {
        assert_eq!(CONSOLE_LABEL, "Log in");
        assert_eq!(console_login("/login").unwrap().label, "Log in");
    }

    /// ★ **The credit names the makers and serves their marks from HERE.** The
    /// page carried the two developers' personal names and the old forge's repo link
    /// until 2026-09-17; it now carries gunnar's block. Both marks are compiled
    /// in and routed by this door, because a credit whose images 404 is worse
    /// than a credit in plain text.
    #[test]
    fn the_credit_names_both_makers_and_this_door_serves_their_marks() {
        assert!(
            PAGE.contains("Vetra AB"),
            "the credit does not name Vetra AB"
        );
        assert!(
            PAGE.contains("Ignalina ApS"),
            "the credit does not name Ignalina ApS"
        );
        // The old people-and-repo credit always carried the repo link, so the next
        // assertion is the guard against its return.
        assert!(
            !PAGE.contains(concat!("code", "berg", ".org/nordisk/holger")),
            "the old repo link is still here"
        );
        for path in [VETRA_PATH, IGNALINA_PATH] {
            assert!(PAGE.contains(path), "the page does not reference {path}");
            let (f, rel) = doors();
            let d = Doors {
                front: &f,
                catalogue: &rel,
            };
            let r = route("GET", path, &d).unwrap_or_else(|| panic!("{path} is not served"));
            assert_eq!(r.status, 200, "{path} answered {}", r.status);
            assert!(!r.body.is_empty(), "{path} served an empty body");
        }
    }

    /// ★ **No price, no currency, no amount.** The console's discipline,
    /// carried over: holger's front page states what is published, and every
    /// number on it came from the server.
    #[test]
    fn the_page_names_no_price_and_no_currency() {
        for token in [
            "€",
            "$",
            "£",
            "kr",
            "SEK",
            "EUR",
            "USD",
            "/month",
            "per month",
            "free tier",
            "pricing",
        ] {
            assert!(!PAGE.contains(token), "the page carries `{token}`");
        }
    }

    /// The page says what holger is, in the repository's own words, and names
    /// the product it is a front for.
    #[test]
    fn the_page_is_holgers() {
        assert!(
            PAGE.contains("<h1>Holger</h1>"),
            "the page does not name the product"
        );
        assert!(
            PAGE.contains("Immutable artifact repository"),
            "the tagline is not the readme's"
        );
        assert!(PAGE.contains("Latest releases"), "the list is not named");
        // ★ The three columns, in the order a reader sees them. The JSON is
        // alphabetical and says nothing about this; the markup is where the
        // order lives, so the markup is where it is checked.
        let mut at = 0usize;
        for col in ["Repository", "Artifact", "Version"] {
            let head = format!(">{col}</th>");
            let found = PAGE
                .find(&head)
                .unwrap_or_else(|| panic!("the `{col}` column heading is missing"));
            assert!(found > at, "the columns are out of order at `{col}`");
            at = found;
        }
        // …and the script fills them in that same order, so the heading and
        // the cell under it are the same field.
        let script = PAGE.split("<script>").nth(1).unwrap_or("");
        assert!(
            script.contains("[['repository', ''], ['artifact', ''], ['version', 'version']]"),
            "the script no longer fills the columns in the order the headings promise"
        );
    }

    /// The picture is on the LEFT: `.art` is the first child of `<body>` and
    /// takes the first half of a row. A `flex-direction: row-reverse` or a
    /// reordered body would move it, and it is one line either way — so it is
    /// asserted rather than trusted.
    #[test]
    fn the_picture_is_on_the_left() {
        let body = PAGE.split("<body>").nth(1).expect("the page has a body");
        let art = body
            .find("class=\"art\"")
            .expect("the page has the picture half");
        let side = body
            .find("class=\"side\"")
            .expect("the page has the column");
        assert!(art < side, "the picture is not the first half of the page");
        assert!(
            !PAGE.contains("row-reverse"),
            "something reversed the row and put the picture on the right"
        );
        assert!(
            PAGE.contains(".art {\n    flex: 0 0 50%;"),
            "the picture no longer owns a half"
        );
    }

    /// ★ **The page computes no version ordering.** The one rule of this
    /// crate, checked against the page's own script: no sort, no compare, no
    /// localeCompare. The server ranks; the page prints.
    /// ★ **The page has a "Recently changed" section, it reads `doc.recent`,
    /// and it still ranks nothing.**
    ///
    /// The markup and the script are checked together, because a section whose
    /// script never fills it is an empty box and a script that fills a section
    /// that is not there is dead code — and either one would let the page ship
    /// looking like it answers recency when it does not.
    #[test]
    fn the_page_shows_what_changed_recently() {
        assert!(
            PAGE.contains("Recently changed"),
            "the recency list is not named"
        );
        assert!(
            PAGE.contains(r#"id="recent-body""#),
            "the recency table has no body to fill"
        );
        assert!(
            PAGE.contains(">Published</th>"),
            "the recency table has no time column"
        );
        let script = PAGE.split("<script>").nth(1).unwrap_or("");
        assert!(
            script.contains("doc.recent"),
            "the page ignores the ranked recency rows"
        );
        assert!(
            script.contains("stamp(row.published_unix_ms)"),
            "the time column is not filled from the row's own time"
        );
        // The page RANKS nothing: `most_recently_changed` on the server decided
        // the order, and this is checked by `the_page_does_not_rank_anything`
        // below over the whole script, this section included.
    }

    /// A zero or a missing time prints as NOTHING, never as 1970.
    ///
    /// The server already leaves unknowns out of `recent`, so this is the
    /// page's own second refusal: it reads a document off a socket, and a peer
    /// that turned an unknown into a value must not get a date printed for it.
    #[test]
    fn the_page_refuses_to_print_an_epoch_zero_as_a_date() {
        let script = PAGE.split("<script>").nth(1).unwrap_or("");
        assert!(
            script.contains("if (!isFinite(n) || n <= 0) return ''"),
            "the page's stamp() would render a zero as 1970-01-01"
        );
    }

    #[test]
    fn the_page_does_not_rank_anything() {
        let script = PAGE.split("<script>").nth(1).unwrap_or("");
        for banned in [".sort(", "localeCompare", "parseFloat", "parseInt"] {
            assert!(
                !script.contains(banned),
                "the page's script carries `{banned}` — it is deciding something"
            );
        }
    }

    /// Nothing is fetched from a third party. This server runs airgapped by
    /// design; a font or a script from a CDN would be a front page that is
    /// blank in exactly the deployment holger exists for.
    #[test]
    fn the_page_fetches_nothing_from_outside() {
        for scheme in [
            "http://",
            "//cdn",
            "googleapis",
            "cdnjs",
            "jsdelivr",
            "unpkg",
        ] {
            assert!(!PAGE.contains(scheme), "the page reaches out to `{scheme}`");
        }
        // The one external link is the forge the source is published from, and
        // it is a link a reader clicks — not something the page loads.
        assert_eq!(
            PAGE.matches("https://").count(),
            1,
            "the page names more than one external URL"
        );
    }

    // ── the third state ──────────────────────────────────────────────────────

    /// ★★ **THE BUTTON SAYS "Log in with passkey" AND THE CEREMONY IS HERE.**
    ///
    /// The owner's words, twice: the same label as gunnar's, and no second page
    /// that shows the same thing again. Both halves are asserted, because
    /// either one alone is the bug that was shipped — a label over a
    /// navigation, or a ceremony behind a button that says "Open the console".
    #[test]
    fn the_ceremony_door_is_labelled_and_wired_to_this_page() {
        let login = passkey_login_here(
            "/auth/passkey/login/start",
            "/auth/passkey/login/finish",
            "/overview",
        )
        .expect("three rooted paths are a ceremony");
        assert_eq!(login.label, PASSKEY_LABEL);
        assert_eq!(login.label, "Log in");
        assert_eq!(login.door, Door::Here);
        assert_eq!(login.finish.as_deref(), Some("/auth/passkey/login/finish"));
        assert_eq!(login.next.as_deref(), Some("/overview"));

        // …and it goes over the wire naming its own state, because the page
        // branches on that word and nothing else.
        let body = serde_json::to_string(&Front {
            base_url: None,
            login: Some(login),
        })
        .unwrap();
        assert!(body.contains(r#""door":"here""#), "{body}");
    }

    /// ★ **HOLDIRECT (owner 2026-10-28): a console on this origin with holger's passkey
    /// sign-in is a ceremony on the front page**, holger's own doors, landing on the console's
    /// home; no passkey sign-in keeps the link; a path the page would refuse is no door.
    #[test]
    fn the_console_on_this_origin_signs_in_on_the_front_page() {
        let here =
            console_sign_in("/login", true).expect("the console's path and the passkey sign-in");
        assert_eq!(here.door, Door::Here);
        assert_eq!(here.label, "Log in");
        assert_eq!(here.start, "/-/auth/passkey/login/start");
        assert_eq!(here.finish.as_deref(), Some("/-/auth/passkey/login/finish"));
        assert_eq!(here.next.as_deref(), Some("/overview"));
        assert!(here.enrol.is_none(), "enrolment stays on /login");
        let link = console_sign_in("/login", false).expect("a console with no passkey sign-in");
        assert_eq!(link, console_login("/login").unwrap());
        assert!(console_sign_in("https://console.example.com", true).is_none());
    }

    /// ★ **The press asks holger's start as holger answers it**: a `POST` with a JSON body,
    /// asked as the person reaches for the button and used once (RA2-6), inside the press's
    /// gesture; a refusal by the device is said in the page's own words.
    #[test]
    fn the_pages_ceremony_asks_holgers_start_with_a_post() {
        let script = PAGE.split("<script>").nth(1).unwrap_or("");
        assert!(script.contains("fetch(start, { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' }, body: '{}' })"), "the start is holger's POST");
        assert!(
            script.contains("for (const ev of ['pointerenter', 'focus', 'touchstart'])"),
            "the start is asked as the person reaches for Log in"
        );
        assert!(
            script.contains("navigator.userActivation.isActive"),
            "a press whose gesture is gone asks again"
        );
        assert!(
            script.contains(NO_PASSKEY_HERE),
            "the page's words for a refused ceremony"
        );
    }

    /// What the front page says when no passkey answered (the console's `noPasskeyHere`).
    const NO_PASSKEY_HERE: &str = "No passkey for this site on this device. Use the phone, computer or password manager where you made your passkey.";

    /// The link door is unchanged and still says so on the wire. The appliance
    /// and any self-hoster with a console elsewhere on the origin keep exactly
    /// the page they had.
    #[test]
    fn a_link_door_still_says_link_and_carries_no_ceremony() {
        let login = console_login("/login?next=/overview").unwrap();
        assert_eq!(login.door, Door::Link);
        assert_eq!(login.label, CONSOLE_LABEL);
        assert!(login.finish.is_none() && login.next.is_none());
        let body = serde_json::to_string(&login).unwrap();
        assert!(body.contains(r#""door":"link""#), "{body}");
        assert!(
            !body.contains("finish"),
            "a link door sent a ceremony field: {body}"
        );
    }

    /// A `/-/front` document written before `door` existed still reads, and
    /// reads as the link it was. The field defaults rather than failing, which
    /// is the whole reason it is a field on a struct and not a tag on an enum.
    #[test]
    fn a_document_from_before_this_field_is_still_a_link() {
        let old = r#"{"label":"Open the console","start":"/login"}"#;
        let login: Login = serde_json::from_str(old).unwrap();
        assert_eq!(login.door, Door::Link);
    }

    /// ★ **All three paths go through one rule, and one bad path is no door.**
    ///
    /// A half-wired ceremony — a start on this origin and a finish somewhere
    /// else — is not a door with a bug in it. It is the assertion being posted
    /// to a stranger.
    #[test]
    fn one_off_site_path_refuses_the_whole_ceremony() {
        let good = (
            "/auth/passkey/login/start",
            "/auth/passkey/login/finish",
            "/overview",
        );
        assert!(passkey_login_here(good.0, good.1, good.2).is_some());
        for bad in [
            "https://evil.example.com/finish",
            "//evil.example.com",
            "/\\evil",
            "",
            "relative",
        ] {
            assert!(
                passkey_login_here(bad, good.1, good.2).is_none(),
                "start {bad:?}"
            );
            assert!(
                passkey_login_here(good.0, bad, good.2).is_none(),
                "finish {bad:?}"
            );
            assert!(
                passkey_login_here(good.0, good.1, bad).is_none(),
                "next {bad:?}"
            );
        }
    }

    /// ★ **The page holds the ceremony itself.** The three calls a WebAuthn
    /// login is made of are in the document, so the button cannot be a
    /// navigation dressed up as one.
    #[test]
    fn the_page_carries_the_ceremony_and_not_a_second_page() {
        let script = PAGE.split("<script>").nth(1).unwrap_or("");
        assert!(
            script.contains("navigator.credentials.get"),
            "the page does not call the authenticator — the button is still a link"
        );
        assert!(
            script.contains("allowCredentials"),
            "the page does not decode the challenge list"
        );
        assert!(
            script.contains("clientDataJSON"),
            "the page does not send the assertion"
        );
        // The LABEL is deliberately not in the page. It is the server's word —
        // `/-/front` names it and the page prints what it is handed — which is
        // why the button could say "Open the console" over a ceremony if the
        // two halves were decided in two places. They are decided in one:
        // `PASSKEY_LABEL` is set by `passkey_login_here` and by nothing else,
        // and `the_ceremony_door_is_labelled_and_wired_to_this_page` is where
        // the words are held.
        assert!(
            !PAGE.contains("Open the console"),
            "the page hard-codes a label; the server names the button"
        );
    }

    /// ★★ **THE FIRST KEY CAN BE ENROLLED ON THIS PAGE.**
    ///
    /// MEASURED on holger.rs 2026-09-20: the ceremony shipped, the owner
    /// pressed the button, and the door answered "no passkey is registered
    /// yet". True, and a dead end. The panel is the other half.
    #[test]
    fn the_enrolment_panel_is_offered_when_the_server_serves_it() {
        let login = passkey_login_here("/a/start", "/a/finish", "/overview").unwrap();
        assert!(login.enrol.is_none(), "enrolment is opt-in, never implied");
        let with = with_enrolment(login, "/r/start", "/r/finish", "/auth/status");
        let e = with.enrol.as_ref().expect("three rooted paths are a panel");
        assert_eq!(e.start, "/r/start");
        assert_eq!(e.finish, "/r/finish");
        assert_eq!(e.status, "/auth/status");
        let body = serde_json::to_string(&with).unwrap();
        assert!(body.contains(r#""enrol""#), "{body}");
    }

    /// ★ **A LINK door never grows an enrolment form, and neither does a
    /// half-configured one.** The appliance serves this same page with no
    /// `/auth/*` at all; a panel there would be a form nothing can answer.
    #[test]
    fn a_link_door_and_a_bad_path_both_refuse_the_panel() {
        let link = console_login("/login").unwrap();
        let still = with_enrolment(link, "/r/start", "/r/finish", "/auth/status");
        assert!(
            still.enrol.is_none(),
            "a link door was given an enrolment panel"
        );
        assert!(
            !serde_json::to_string(&still).unwrap().contains("enrol"),
            "a link door sent an enrolment field"
        );

        let here = passkey_login_here("/a/start", "/a/finish", "/overview").unwrap();
        for bad in [
            "https://evil.example.com/r",
            "//evil",
            "/\\evil",
            "",
            "relative",
        ] {
            assert!(
                with_enrolment(here.clone(), bad, "/r/finish", "/s")
                    .enrol
                    .is_none(),
                "start {bad:?}"
            );
            assert!(
                with_enrolment(here.clone(), "/r/start", bad, "/s")
                    .enrol
                    .is_none(),
                "finish {bad:?}"
            );
            assert!(
                with_enrolment(here.clone(), "/r/start", "/r/finish", bad)
                    .enrol
                    .is_none(),
                "status {bad:?}"
            );
        }
    }

    /// The page carries the enrolment half of WebAuthn, the panel ships
    /// collapsed AND hidden, and the invite is taken out of the URL.
    #[test]
    fn the_page_carries_the_enrolment_and_hides_it_until_the_server_offers_it() {
        let script = PAGE.split("<script>").nth(1).unwrap_or("");
        assert!(
            script.contains("navigator.credentials.create"),
            "the page cannot mint a key"
        );
        assert!(
            script.contains("attestationObject"),
            "the page does not send the attestation"
        );
        assert!(
            script.contains("bootstrap_spent"),
            "the page never retires the break-glass door"
        );
        assert!(
            script.contains("history.replaceState"),
            "the invite is left in the address bar"
        );
        assert!(
            PAGE.contains(r#"<details class="reg" id="reg" hidden>"#),
            "the enrolment panel does not ship hidden, so a server without /auth/* would draw it"
        );
        assert!(
            !PAGE.contains("<details class=\"reg\" id=\"reg\" open"),
            "the enrolment panel ships open; it is the rarer act and belongs collapsed"
        );
    }

    // ── the roster ───────────────────────────────────────────────────────────

    /// ★ **A server with repositories and nothing published still has a
    /// catalogue.** The state holger.rs is in: an empty array said "nothing
    /// here" about a server that has repositories a stranger can use.
    #[test]
    fn an_empty_store_still_names_its_repositories() {
        let f = Front::anonymous(None);
        let c = Catalogue {
            repositories: vec![Repository {
                name: "crates-mirror".into(),
                format: "cargo".into(),
                packages: 0,
            }],
            releases: vec![],
            recent: Vec::new(),
        };
        let d = Doors {
            front: &f,
            catalogue: &c,
        };
        let body = String::from_utf8(route("GET", RELEASES_PATH, &d).unwrap().body).unwrap();
        let doc: serde_json::Value = serde_json::from_str(&body).unwrap();
        assert_eq!(doc["repositories"].as_array().unwrap().len(), 1);
        assert_eq!(doc["repositories"][0]["name"], "crates-mirror");
        assert_eq!(doc["repositories"][0]["format"], "cargo");
        assert_eq!(doc["repositories"][0]["packages"], 0);
        assert!(doc["releases"].as_array().unwrap().is_empty());
    }

    /// The page reads the two lists by the names the wire uses, and names the
    /// roster for a reader.
    #[test]
    fn the_page_reads_both_halves_of_the_catalogue() {
        let script = PAGE.split("<script>").nth(1).unwrap_or("");
        assert!(
            script.contains("doc.repositories"),
            "the page ignores the roster"
        );
        assert!(script.contains("doc.releases"), "the page ignores the rows");
        assert!(
            PAGE.contains("Public repositories"),
            "the roster has no heading"
        );
    }

    /// No `innerHTML` anywhere: a repository name, an artifact name and a
    /// version are operator-supplied text arriving over a socket.
    #[test]
    fn nothing_on_the_page_turns_text_into_markup() {
        assert!(
            !PAGE.contains("innerHTML"),
            "the page writes markup from data"
        );
        assert!(
            !PAGE.contains("document.write"),
            "the page uses document.write"
        );
    }
}