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

nordisk/holger

server/lib/src/identity_host.rs

//! **Holger as an identity host** (IDENTITY-HOST, owner GO 2026-10-27; lane OX-IDH step 3).
//!
//! The identity flow (sign-up, login, invite, redeem, sessions, step-up, logout, deactivate)
//! is edda's `nornir-identity` core, `Identity<H: IdentityHost>`. The ONE product-specific part
//! is the host: where this box sits ([`Site`]), where its people are kept ([`UserStore`]), what
//! roles they hold, where an event is written down, and how a link is mailed. This module is
//! Holger's host and nothing more: no ceremony, no session rule, no invite rule is decided here.
//!
//! * **site**: a sold private Holger (its fleet instance is a company name) and an open build
//!   (it sells nothing) are [`Site::Private`], invite only; a product site the ONE shop sells
//!   sign-up on (`holger-rs`, `test2-holger-rs`) is [`Site::Root`]. Data, never a code path.
//! * **store**: [`HolgerStore`], one file on the data root (`holger-passkeys/identity.json`,
//!   beside the archives, so the twin follows it like every other file there), written whole
//!   and atomically under one lock.
//! * **roles**: the declarative `AuthConfig` policy, exactly as every door asks it.
//! * **audit**: Holger's audit chain (`AuditLog`).
//! * **mailer**: monetize's one shared sender where the box was fed a relay ([`HolgerMailer`]);
//!   an airgapped box refuses by name (`nornir_identity::NoMail`).

use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};

use nornir_identity::{
    Credential, Event, IdentityHost, Invite, Mail, MailKind, Mailer, Revocations, Roles, Session,
    StoreError, User, UserState, UserStore,
};
use serde::{Deserialize, Serialize};

/// Where a box sits (edda's), named here for holger-server's boot.
pub use nornir_identity::Site;

use crate::audit::{AuditAction, AuditEvent, AuditLog};
use crate::auth::{AuthConfig, AuthIdentity};
use crate::lock::LockOrRecover as _;

/// The directory under the data root the store lives in.
pub const STORE_DIR: &str = "holger-passkeys";
/// The store's one file.
pub const STORE_FILE: &str = "identity.json";
/// The most sessions kept; past it the ones that end soonest go first.
pub const MAX_SESSIONS: usize = 4096;

/// **Where a box with fleet instance `instance` sits** (pure). A sold private Holger's instance
/// is its company (`room::sold_company`), invite only; a product site's label (`holger-rs`)
/// sells sign-up where the build sells at all (`metered`); an open build sells nothing.
pub fn site_for(instance: &str, metered: bool) -> Site {
    let instance = instance.trim();
    if !metered || instance.is_empty() || sold_company(instance) {
        Site::Private
    } else {
        Site::Root
    }
}

/// `room::sold_company`'s rule (a company name: 1 to 16 of `[a-z0-9]`, a letter first), named
/// here because `room` keeps it behind `metered`.
fn sold_company(instance: &str) -> bool {
    let b = instance.as_bytes();
    (1..=16).contains(&b.len())
        && b[0].is_ascii_lowercase()
        && b.iter()
            .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit())
}

/// **This box's site**, from its manual fleet enrolment (`fleet/fleet.json`, the `enrol`
/// CLI's), else [`Site::Private`]. Never from the passkey host (RA5-1): a shop-laid box's host
/// is its estate's DNS name, which is never a company name, so reading it as a site opened
/// sign-up on every sold private Holger. A shop-laid box's site is what the ONE box fed
/// ([`site_from_feed`], `Holger::estate_site` at the start, [`follow_site`] after it).
pub fn site_of(data_root: Option<&Path>, auth: &AuthConfig) -> Site {
    let _ = auth;
    #[cfg(feature = "metered")]
    {
        let enrolled = data_root
            .and_then(|root| {
                crate::holger_fleet::config::FleetConfig::load(root)
                    .ok()
                    .flatten()
            })
            .map(|c| c.instance.trim().to_owned())
            .unwrap_or_default();
        site_for(&enrolled, true)
    }
    #[cfg(not(feature = "metered"))]
    {
        let _ = data_root;
        site_for("", false)
    }
}

/// The feed key the ONE box names the estate's declared kind with (monetize-shop-pairing
/// `contract::FEED_VARIANT`, in every box's FIRST feed: the record's variant).
pub const VARIANT_FEED: &str = "estate.variant";
/// The variant an operator site the ONE box laid itself is recorded as (holger-monetize-guest's
/// `site_variant`: `holger-twin-one`); a sold estate is fed its `sold_variant` instead.
pub const SITE_VARIANT: &str = "holger-twin-one";
/// The feed keys the estate the boxes pull as is named under (plain, or the front's offer).
const INSTANCE_FEEDS: [&str; 2] = ["fleet_instance", "offer.front.fleet_instance"];

/// Pure: **the site the ONE box's feed declares** (RA5-1). `estate.variant` is the estate's
/// declared kind: [`Site::Root`] only for an operator site's variant ([`SITE_VARIANT`]) on a
/// build that sells (`metered`), [`Site::Private`] for every other variant. A fed
/// `fleet_instance` that is a company name is a sold estate's, private whatever else is fed;
/// with no variant, the instance decides as [`site_for`] does. `None`: the feed declares
/// nothing (yet). Fails closed: any private signal wins.
pub fn site_from_feed(
    feed: &std::collections::BTreeMap<String, String>,
    metered: bool,
) -> Option<Site> {
    let variant = feed
        .get(VARIANT_FEED)
        .map(|v| v.trim())
        .filter(|v| !v.is_empty());
    let instance = INSTANCE_FEEDS
        .iter()
        .find_map(|k| feed.get(*k))
        .map(|v| v.trim())
        .filter(|v| !v.is_empty());
    match (variant, instance) {
        (None, None) => None,
        (_, Some(i)) if sold_company(i) => Some(Site::Private),
        (Some(v), _) => Some(if metered && v == SITE_VARIANT {
            Site::Root
        } else {
            Site::Private
        }),
        (None, Some(i)) => Some(site_for(i, metered)),
    }
}

/// **Follow the site the feed declares, live** (RA5-1): `core`'s site becomes what the feed
/// declares the moment a feed declares one (a late `fleet_instance`, a refeed); a feed that
/// declares nothing leaves it as it is.
pub fn follow_site(feeds: &dyn crate::late::Feeds, core: SharedIdentity) {
    crate::late::on_change(
        feeds,
        "identity: the estate's site",
        |f| site_from_feed(f, cfg!(feature = "metered")),
        move |site: &Option<Site>| {
            if let Some(site) = *site {
                if core.host().site() != site {
                    log::warn!("identity: the feed declares this box {site:?}; sign-up follows it");
                }
                core.host().set_site(site);
            }
        },
    );
}

// ─── The store ──────────────────────────────────────────────────────────────

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
struct UserRow {
    id: String,
    display: String,
    #[serde(default)]
    email: Option<String>,
    #[serde(default)]
    groups: Vec<String>,
    state: String,
    created: i64,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
struct CredentialRow {
    /// base64url of the credential id.
    id: String,
    user: String,
    /// base64url of the SEC1 uncompressed key.
    public_key: String,
    sign_count: u32,
    created: i64,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
struct SessionRow {
    id: String,
    user: String,
    auth_time: i64,
    expires: i64,
    #[serde(default)]
    stepped_up_at: Option<i64>,
}

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
struct Tables {
    #[serde(default)]
    users: Vec<UserRow>,
    #[serde(default)]
    credentials: Vec<CredentialRow>,
    #[serde(default)]
    sessions: Vec<SessionRow>,
    #[serde(default)]
    invites: Vec<Invite>,
    #[serde(default)]
    revocations: Revocations,
}

fn b64(bytes: &[u8]) -> String {
    use base64::Engine as _;
    base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(bytes)
}

fn unb64(text: &str) -> Result<Vec<u8>, StoreError> {
    use base64::Engine as _;
    base64::engine::general_purpose::URL_SAFE_NO_PAD
        .decode(text)
        .map_err(|e| StoreError(format!("the identity store holds a bad key: {e}")))
}

fn state_word(s: UserState) -> &'static str {
    match s {
        UserState::Pending => "pending",
        UserState::Active => "active",
        UserState::Deactivated => "deactivated",
    }
}

fn state_of(word: &str) -> UserState {
    match word {
        "active" => UserState::Active,
        "pending" => UserState::Pending,
        // Anything else is no login: a damaged word never lets a person in.
        _ => UserState::Deactivated,
    }
}

impl From<&User> for UserRow {
    fn from(u: &User) -> Self {
        UserRow {
            id: u.id.clone(),
            display: u.display.clone(),
            email: u.email.clone(),
            groups: u.groups.clone(),
            state: state_word(u.state).to_owned(),
            created: u.created,
        }
    }
}

impl From<&UserRow> for User {
    fn from(r: &UserRow) -> Self {
        User {
            id: r.id.clone(),
            display: r.display.clone(),
            email: r.email.clone(),
            groups: r.groups.clone(),
            state: state_of(&r.state),
            created: r.created,
        }
    }
}

impl From<&Session> for SessionRow {
    fn from(s: &Session) -> Self {
        SessionRow {
            id: s.id.clone(),
            user: s.user.clone(),
            auth_time: s.auth_time,
            expires: s.expires,
            stepped_up_at: s.stepped_up_at,
        }
    }
}

impl From<&SessionRow> for Session {
    fn from(r: &SessionRow) -> Self {
        Session {
            id: r.id.clone(),
            user: r.user.clone(),
            auth_time: r.auth_time,
            expires: r.expires,
            stepped_up_at: r.stepped_up_at,
        }
    }
}

fn credential_of(r: &CredentialRow) -> Result<Credential, StoreError> {
    Ok(Credential {
        id: unb64(&r.id)?,
        user: r.user.clone(),
        public_key: unb64(&r.public_key)?,
        sign_count: r.sign_count,
        created: r.created,
    })
}

/// **Holger's people, on the data root**: users, passkeys, sessions (by hash), invites (by
/// hash) and the revocation floors, in one file written whole (temporary file, sync, rename,
/// directory sync) under one lock, so a crash leaves the old file or the new one.
///
/// **The file is the truth** (RA5-3): another writer replaces it under a running process (the
/// pulled Recharge, the twin's follow), so every read and every change first looks at the file
/// (one `stat`) and reads it again when it is not the one this store read or wrote last. A
/// change is made over what the file holds now, never over a stale copy; a file that does not
/// read is refused by name and never written over.
pub struct HolgerStore {
    path: PathBuf,
    tables: Mutex<Held>,
}

/// The tables as last read or written, and which file they were.
struct Held {
    tables: Tables,
    seen: Option<Stamp>,
}

/// Which file the store's path names now: its inode, size and modification time.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct Stamp {
    ino: u64,
    len: u64,
    modified: Option<std::time::SystemTime>,
}

fn stamp_of(path: &Path) -> std::io::Result<Option<Stamp>> {
    match std::fs::metadata(path) {
        Ok(m) => {
            #[cfg(unix)]
            let ino = std::os::unix::fs::MetadataExt::ino(&m);
            #[cfg(not(unix))]
            let ino = 0;
            Ok(Some(Stamp {
                ino,
                len: m.len(),
                modified: m.modified().ok(),
            }))
        }
        Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
        Err(e) => Err(e),
    }
}

/// The tables the file at `path` holds (none: no file), and its stamp.
fn load(path: &Path) -> Result<(Tables, Option<Stamp>), String> {
    let seen = stamp_of(path)
        .map_err(|e| format!("the identity store {} cannot be read: {e}", path.display()))?;
    let tables = match std::fs::read(path) {
        Ok(bytes) => serde_json::from_slice(&bytes)
            .map_err(|e| format!("the identity store {} does not read: {e}", path.display()))?,
        Err(e) if e.kind() == std::io::ErrorKind::NotFound => Tables::default(),
        Err(e) => {
            return Err(format!(
                "the identity store {} cannot be read: {e}",
                path.display()
            ))
        }
    };
    Ok((tables, seen))
}

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

impl HolgerStore {
    /// The store under `data_root` (made on first use). A file that does not read is refused by
    /// name: a damaged store is never read as "nobody is here", which would reopen the first
    /// admin's invite.
    pub fn open(data_root: &Path) -> Result<Self, String> {
        let dir = data_root.join(STORE_DIR);
        std::fs::create_dir_all(&dir)
            .map_err(|e| format!("the identity store {} cannot be made: {e}", dir.display()))?;
        let path = dir.join(STORE_FILE);
        let (tables, seen) = load(&path)?;
        Ok(HolgerStore {
            path,
            tables: Mutex::new(Held { tables, seen }),
        })
    }

    /// The held tables made the file's again when another writer replaced it since.
    fn fresh(&self, held: &mut Held) -> Result<(), StoreError> {
        let now = stamp_of(&self.path).map_err(|e| {
            StoreError(format!(
                "the identity store {} cannot be read: {e}",
                self.path.display()
            ))
        })?;
        if now != held.seen {
            let (tables, seen) = load(&self.path).map_err(StoreError)?;
            log::warn!("identity: the store {} was written by another writer (a recharge, the twin's follow): read again", self.path.display());
            *held = Held { tables, seen };
        }
        Ok(())
    }

    fn save(&self, t: &Tables) -> Result<(), StoreError> {
        let fail = |e: std::io::Error| {
            StoreError(format!(
                "the identity store {} was not written: {e}",
                self.path.display()
            ))
        };
        let bytes = serde_json::to_vec(t)
            .map_err(|e| StoreError(format!("the identity store does not serialize: {e}")))?;
        let tmp = self.path.with_extension("json.tmp");
        {
            use std::io::Write as _;
            let mut f = std::fs::File::create(&tmp).map_err(fail)?;
            f.write_all(&bytes).map_err(fail)?;
            f.sync_all().map_err(fail)?;
        }
        std::fs::rename(&tmp, &self.path).map_err(fail)?;
        if let Some(dir) = self.path.parent() {
            if let Ok(d) = std::fs::File::open(dir) {
                let _ = d.sync_all();
            }
        }
        Ok(())
    }

    /// Change the tables and write them, all under the lock; a write that fails leaves the
    /// tables as they were.
    fn change<R>(&self, f: impl FnOnce(&mut Tables) -> R) -> Result<R, StoreError> {
        let mut held = self.tables.lock_or_recover();
        self.fresh(&mut held)?;
        let mut next = held.tables.clone();
        let r = f(&mut next);
        self.save(&next)?;
        let seen = stamp_of(&self.path).map_err(|e| {
            StoreError(format!(
                "the identity store {} cannot be read: {e}",
                self.path.display()
            ))
        })?;
        *held = Held { tables: next, seen };
        Ok(r)
    }

    fn read<R>(&self, f: impl FnOnce(&Tables) -> R) -> Result<R, StoreError> {
        let mut held = self.tables.lock_or_recover();
        self.fresh(&mut held)?;
        Ok(f(&held.tables))
    }
}

/// **The people the store under `data_root` holds**, read without opening it (nothing is made);
/// none when there is no store or it does not read.
pub fn people(data_root: &Path) -> Vec<User> {
    std::fs::read(data_root.join(STORE_DIR).join(STORE_FILE))
        .ok()
        .and_then(|b| serde_json::from_slice::<Tables>(&b).ok())
        .map(|t| t.users.iter().map(User::from).collect())
        .unwrap_or_default()
}

impl UserStore for HolgerStore {
    fn user(&self, id: &str) -> Result<Option<User>, StoreError> {
        self.read(|t| t.users.iter().find(|u| u.id == id).map(User::from))
    }

    fn user_by_email(&self, email: &str) -> Result<Option<User>, StoreError> {
        self.read(|t| {
            t.users
                .iter()
                .find(|u| u.email.as_deref() == Some(email))
                .map(User::from)
        })
    }

    fn users(&self) -> Result<Vec<User>, StoreError> {
        self.read(|t| t.users.iter().map(User::from).collect())
    }

    fn put_user(&self, user: &User) -> Result<(), StoreError> {
        self.change(|t| {
            t.users.retain(|u| u.id != user.id);
            t.users.push(UserRow::from(user));
        })
    }

    fn credential(&self, id: &[u8]) -> Result<Option<Credential>, StoreError> {
        let key = b64(id);
        self.read(|t| {
            t.credentials
                .iter()
                .find(|c| c.id == key)
                .map(credential_of)
        })?
        .transpose()
    }

    fn credentials_of(&self, user: &str) -> Result<Vec<Credential>, StoreError> {
        self.read(|t| {
            t.credentials
                .iter()
                .filter(|c| c.user == user)
                .map(credential_of)
                .collect()
        })?
    }

    fn put_credential(&self, c: &Credential) -> Result<(), StoreError> {
        let row = CredentialRow {
            id: b64(&c.id),
            user: c.user.clone(),
            public_key: b64(&c.public_key),
            sign_count: c.sign_count,
            created: c.created,
        };
        self.change(|t| {
            t.credentials.retain(|r| r.id != row.id);
            t.credentials.push(row);
        })
    }

    fn session(&self, id: &str) -> Result<Option<Session>, StoreError> {
        self.read(|t| t.sessions.iter().find(|s| s.id == id).map(Session::from))
    }

    fn put_session(&self, session: &Session) -> Result<(), StoreError> {
        self.change(|t| {
            t.sessions.retain(|s| s.id != session.id);
            t.sessions.push(SessionRow::from(session));
            if t.sessions.len() > MAX_SESSIONS {
                t.sessions.sort_by_key(|s| std::cmp::Reverse(s.expires));
                t.sessions.truncate(MAX_SESSIONS);
            }
        })
    }

    fn delete_session(&self, id: &str) -> Result<(), StoreError> {
        self.change(|t| t.sessions.retain(|s| s.id != id))
    }

    fn invites(&self) -> Result<Vec<Invite>, StoreError> {
        self.read(|t| t.invites.clone())
    }

    fn update_invites(&self, change: &mut dyn FnMut(&mut Vec<Invite>)) -> Result<(), StoreError> {
        self.change(|t| change(&mut t.invites))
    }

    fn revocations(&self) -> Result<Revocations, StoreError> {
        self.read(|t| t.revocations.clone())
    }

    fn update_revocations(
        &self,
        change: &mut dyn FnMut(&mut Revocations),
    ) -> Result<(), StoreError> {
        self.change(|t| change(&mut t.revocations))
    }
}

// ─── The mailer ─────────────────────────────────────────────────────────────

/// **How Holger mails a link**: the login page of this box (`<origin>/login?invite=<secret>`,
/// the page's one way in for an invite and a first-login link alike), through the shared
/// sender it was fed.
pub struct HolgerMailer {
    origin: String,
}

/// The link a mail carries.
pub fn link(origin: &str, secret: &str) -> String {
    format!("{}/login?invite={secret}", origin.trim_end_matches('/'))
}

/// The subject and body of a mail (Holger's words).
pub fn words(kind: MailKind, link: &str) -> (&'static str, String) {
    match kind {
        MailKind::FirstLogin => ("Your Holger account: register your security key", format!("Open this link to register the security key you will sign in to Holger with:\n\n{link}\n\nThe link works once.\n")),
        MailKind::Invite => ("You are invited to a Holger", format!("You were invited to a Holger. Open this link to register your security key:\n\n{link}\n\nThe link works once.\n")),
    }
}

impl HolgerMailer {
    /// The mailer of the box reached at `origin`: it mails through the postman the process holds
    /// at the time of each mail (`sign_in_again::postman`, the fed relay once the pull channel
    /// starts), and refuses by name while there is none (an airgapped or open box).
    pub fn of(origin: &str) -> Self {
        HolgerMailer {
            origin: origin.to_owned(),
        }
    }
}

impl Mailer for HolgerMailer {
    fn send(&self, mail: &Mail) -> Result<(), String> {
        let (subject, body) = words(mail.kind, &link(&self.origin, &mail.secret));
        #[cfg(feature = "metered")]
        if let Some(postman) = crate::sign_in_again::postman() {
            postman.ready()?;
            return postman.post(&crate::holger_fleet::sign_in::SignInMail {
                to: mail.to.clone(),
                subject: subject.to_owned(),
                body,
            });
        }
        let _ = (subject, body);
        Err(nornir_identity::NO_MAIL.to_owned())
    }
}

// ─── Serving: the one core of this process ─────────────────────────────────

/// The identity core of a Holger.
pub type HolgerIdentity = nornir_identity::Identity<HolgerHost>;
/// It, shared by the gRPC door (`IdentityService`) and the auth seam.
pub type SharedIdentity = Arc<HolgerIdentity>;

/// The relying party of `auth`'s passkey method (`rp_id`, `rp_origin`), if it has one.
pub fn party_of(auth: &AuthConfig) -> Option<(String, String)> {
    auth.methods.iter().find_map(|m| match m {
        crate::auth::AuthMethodConfig::Passkey {
            rp_id, rp_origin, ..
        } => Some((
            rp_id.trim().to_owned(),
            rp_origin.trim().trim_end_matches('/').to_owned(),
        )),
        _ => None,
    })
}

// WWW-CANON: the site's origins live in `crate::auth` (every build has them; the account and
// room links of a build without `auth-passkey` name them too).
pub use crate::auth::{origins_of, site_origin};

/// The core's settings for the relying party `rp_id` at `origin` (and its `www.`, [`origins_of`]).
pub fn config_of(rp_id: &str, origin: &str) -> Result<nornir_identity::Config, String> {
    if rp_id
        .trim()
        .trim_matches(|c| c == '[' || c == ']')
        .parse::<std::net::IpAddr>()
        .is_ok()
    {
        return Err(format!(
            "the relying party {rp_id:?} is an IP literal; a passkey is scoped to a name"
        ));
    }
    let party = format!("{rp_id}={}", origins_of(rp_id, origin).join(","))
        .parse::<nornir_identity::RelyingParty>()?;
    Ok(nornir_identity::Config {
        parties: vec![party],
        ..nornir_identity::Config::default()
    })
}

/// **This box's identity core**, over the store on `data_root`, where `auth` has a passkey
/// method (else `None`: the IdentityService is not served, as the passkey door is not).
pub fn open(
    data_root: &Path,
    auth: &AuthConfig,
    audit: Arc<dyn AuditLog>,
) -> Result<Option<SharedIdentity>, String> {
    open_at(data_root, auth, audit, None)
}

/// [`open`] at the site the ONE box fed (`fed`, from the feed: [`site_from_feed`]); `None`: the
/// box's own [`site_of`].
pub fn open_at(
    data_root: &Path,
    auth: &AuthConfig,
    audit: Arc<dyn AuditLog>,
    fed: Option<Site>,
) -> Result<Option<SharedIdentity>, String> {
    let Some((rp_id, origin)) = party_of(auth) else {
        return Ok(None);
    };
    let config = config_of(&rp_id, &origin)?;
    let host = HolgerHost::new(
        fed.unwrap_or_else(|| site_of(Some(data_root), auth)),
        HolgerStore::open(data_root)?,
        auth.clone(),
        audit,
        Box::new(HolgerMailer::of(&origin)),
    );
    Ok(Some(Arc::new(nornir_identity::Identity::new(host, config))))
}

/// The key a core is filed under: the policy's passkey session key file, else its relying
/// party (one data root, one key file, one core).
fn live_key(auth: &AuthConfig) -> Option<String> {
    auth.methods.iter().find_map(|m| match m {
        crate::auth::AuthMethodConfig::Passkey {
            session_key_file: Some(f),
            ..
        } if !f.trim().is_empty() => Some(f.trim().to_owned()),
        crate::auth::AuthMethodConfig::Passkey { rp_id, .. } => Some(rp_id.trim().to_owned()),
        _ => None,
    })
}

/// The cores this process serves (held for its life), by the key file or relying party their
/// policy names, so the one
/// `auth::validate_request` every door calls finds the core whose session a token is.
fn live_cores() -> &'static std::sync::RwLock<Vec<(String, SharedIdentity)>> {
    static LIVE: std::sync::OnceLock<std::sync::RwLock<Vec<(String, SharedIdentity)>>> =
        std::sync::OnceLock::new();
    LIVE.get_or_init(Default::default)
}

/// Make `core` the one [`session_identity`] asks for tokens under `auth`'s relying party.
pub fn register_live(auth: &AuthConfig, core: &SharedIdentity) {
    use crate::lock::RwLockOrRecover as _;
    let Some(key) = live_key(auth) else { return };
    let mut live = live_cores().write_or_recover();
    live.retain(|(k, _)| k != &key);
    live.push((key, Arc::clone(core)));
}

/// The core this process serves under `auth`'s passkey method, if any.
pub fn live(auth: &AuthConfig) -> Option<SharedIdentity> {
    use crate::lock::RwLockOrRecover as _;
    let key = live_key(auth)?;
    live_cores()
        .read_or_recover()
        .iter()
        .find(|(k, _)| *k == key)
        .map(|(_, c)| Arc::clone(c))
}

/// **The identity a core session token acts as now**: its person with the groups they hold
/// now, or `None` (no such session, ended, expired, under a floor, the person not active, or
/// no core serves this policy here).
pub fn session_identity(auth: &AuthConfig, token: &str) -> Option<AuthIdentity> {
    let core = live(auth)?;
    let view = core.session(token).ok()?;
    let user = core.host().store().user(&view.user_id).ok()??;
    Some(identity_of(&user))
}

/// Is `bearer` a core session token (the shape, not its truth)?
pub fn is_session_token(bearer: &str) -> bool {
    bearer.starts_with(nornir_identity::TOKEN_PREFIX)
}

// ─── The host ───────────────────────────────────────────────────────────────

/// The clock a host tells time by (Unix milliseconds).
pub type Clock = Arc<dyn Fn() -> i64 + Send + Sync>;

/// **Holger's identity host.**
pub struct HolgerHost {
    /// Live: the feed may declare it after the start ([`follow_site`]).
    site: std::sync::RwLock<Site>,
    store: HolgerStore,
    auth: AuthConfig,
    audit: Arc<dyn AuditLog>,
    mailer: Box<dyn Mailer>,
    clock: Option<Clock>,
}

impl std::fmt::Debug for HolgerHost {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("HolgerHost")
            .field("site", &self.site())
            .field("store", &self.store)
            .finish()
    }
}

impl HolgerHost {
    /// The host over `store`, the policy `auth`, the audit chain and a mailer
    /// (`nornir_identity::NoMail` on an airgapped box).
    pub fn new(
        site: Site,
        store: HolgerStore,
        auth: AuthConfig,
        audit: Arc<dyn AuditLog>,
        mailer: Box<dyn Mailer>,
    ) -> Self {
        HolgerHost {
            site: std::sync::RwLock::new(site),
            store,
            auth,
            audit,
            mailer,
            clock: None,
        }
    }

    /// Where this box sits from now on (the feed declared it: [`follow_site`]).
    pub fn set_site(&self, site: Site) {
        use crate::lock::RwLockOrRecover as _;
        *self.site.write_or_recover() = site;
    }

    /// The same host telling time by `clock` (a test's).
    pub fn with_clock(mut self, clock: Clock) -> Self {
        self.clock = Some(clock);
        self
    }

    /// The policy the roles are read from.
    pub fn auth(&self) -> &AuthConfig {
        &self.auth
    }
}

/// The identity a [`User`] acts as under the policy (the shape every door resolves).
pub fn identity_of(user: &User) -> AuthIdentity {
    AuthIdentity {
        subject: user.id.clone(),
        method: "passkey".to_owned(),
        groups: user.groups.clone(),
        display: Some(user.display.clone()),
    }
}

/// What an event is called on the audit chain, who did it, its status and one line of detail.
fn audit_line(event: &Event) -> (String, &'static str, u16, String) {
    match event {
        Event::SignedUp { user } => (user.clone(), "signed-up", 200, String::new()),
        Event::SignUpRefused { code } => {
            ("anonymous".into(), "sign-up-refused", 403, code.to_string())
        }
        Event::InviteMinted { invite, by, mailed } => (
            by.clone(),
            "invite-minted",
            200,
            format!("invite {invite}, mailed {mailed}"),
        ),
        Event::InviteRedeemed { invite, user } => (
            user.clone(),
            "invite-redeemed",
            200,
            format!("invite {invite}"),
        ),
        Event::PasskeyRegistered { user, credential } => (
            user.clone(),
            "passkey-registered",
            200,
            format!("credential {credential}"),
        ),
        Event::LoggedIn {
            user,
            session,
            user_verified,
        } => (
            user.clone(),
            "logged-in",
            200,
            format!("session {session}, user verified {user_verified}"),
        ),
        // nornir-identity 0.2.1 names the person the passkey belonged to: their audit has it.
        Event::LoginRefused { code, user } => (
            user.clone().unwrap_or_else(|| "anonymous".into()),
            "login-refused",
            401,
            code.to_string(),
        ),
        Event::SteppedUp { user } => (user.clone(), "stepped-up", 200, String::new()),
        Event::LoggedOut { user, everywhere } => (
            user.clone(),
            "logged-out",
            200,
            format!("everywhere {everywhere}"),
        ),
        Event::Deactivated { user, by } => (by.clone(), "deactivated", 200, format!("user {user}")),
        Event::InviteRevoked { invite, by } => (
            by.clone(),
            "invite-revoked",
            200,
            format!("invite {invite}"),
        ),
        Event::SessionsEnded { user, by } => {
            (by.clone(), "sessions-ended", 200, format!("user {user}"))
        }
        Event::EveryoneLoggedOut { by } => (by.clone(), "everyone-logged-out", 200, String::new()),
    }
}

impl IdentityHost for HolgerHost {
    fn site(&self) -> Site {
        use crate::lock::RwLockOrRecover as _;
        *self.site.read_or_recover()
    }

    fn store(&self) -> &dyn UserStore {
        &self.store
    }

    fn roles(&self, user: &User) -> Roles {
        let role = self.auth.role_for_identity(&identity_of(user));
        Roles {
            admin: role.can_admin(),
            names: vec![crate::passkey::role_word(role).to_owned()],
        }
    }

    fn audit(&self, event: Event) {
        let (who, what, status, detail) = audit_line(&event);
        let ev = AuditEvent::new(
            who,
            AuditAction::Other,
            "",
            format!("identity/{what}"),
            "",
            status,
            0,
        )
        .with_detail(detail);
        if let Err(e) = self.audit.record(ev) {
            log::warn!("audit record failed: {e}");
        }
    }

    fn mailer(&self) -> &dyn Mailer {
        self.mailer.as_ref()
    }

    fn now_ms(&self) -> i64 {
        match &self.clock {
            Some(c) => c(),
            None => crate::clock::now_unix_ms(),
        }
    }
}

#[cfg(test)]
pub(crate) mod tests {
    use super::*;
    use crate::audit::MemoryAuditLog;
    use nornir_identity::conformance::Harness;
    use nornir_identity::test_host::Kit;
    use std::collections::HashMap;

    /// The groups the shipped estate's policy makes an admin of, and a reader of.
    pub const ADMINS: &str = "holger-admins";
    pub const READERS: &str = "holger-readers";

    pub fn policy() -> AuthConfig {
        AuthConfig {
            group_roles: HashMap::from([
                (ADMINS.to_string(), crate::auth::Role::Admin),
                (READERS.to_string(), crate::auth::Role::Reader),
            ]),
            ..Default::default()
        }
    }

    /// **Holger's conformance harness**: a fresh store on a fresh data root, the kit's mailbox
    /// and clock, the shipped policy's groups.
    pub struct HolgerHarness;

    impl Harness for HolgerHarness {
        type Host = HolgerHost;
        fn host(&self, site: Site, kit: &Kit) -> HolgerHost {
            let root = tempfile::tempdir().unwrap().keep();
            let clock = kit.clock.clone();
            HolgerHost::new(
                site,
                HolgerStore::open(&root).unwrap(),
                policy(),
                Arc::new(MemoryAuditLog::new()),
                Box::new(kit.mail.clone()),
            )
            .with_clock(Arc::new(move || clock.now_ms()))
        }
        fn admin_groups(&self) -> Vec<String> {
            vec![ADMINS.to_owned()]
        }
        fn member_groups(&self) -> Vec<String> {
            vec![READERS.to_owned()]
        }
    }

    /// The core's ONE conformance suite, against Holger's host.
    mod conformance {
        nornir_identity::conformance_suite!(super::HolgerHarness);
    }

    /// ★ The site is data: a sold private Holger and an open build are invite only, the
    /// shop's product site sells sign-up.
    #[test]
    fn a_sold_private_holger_and_an_open_build_are_invite_only_and_the_product_site_sells_sign_up()
    {
        assert_eq!(
            site_for("acme", true),
            Site::Private,
            "a sold private Holger's instance is its company"
        );
        assert_eq!(
            site_for("holger-rs", true),
            Site::Root,
            "the product site sells sign-up"
        );
        assert_eq!(site_for("test2-holger-rs", true), Site::Root);
        assert_eq!(
            site_for("holger-rs", false),
            Site::Private,
            "an open build sells nothing"
        );
        assert_eq!(
            site_for("", true),
            Site::Private,
            "a box that does not know where it is sells nothing"
        );
    }

    /// ★ RA5-1: the site is what the ONE box FED: the estate's declared variant (in every box's
    /// first feed), a company instance always private, nothing fed nothing declared.
    #[test]
    fn the_fed_variant_and_instance_declare_the_site_and_private_wins() {
        let f = |kv: &[(&str, &str)]| {
            kv.iter()
                .map(|(k, v)| (k.to_string(), v.to_string()))
                .collect::<std::collections::BTreeMap<_, _>>()
        };
        assert_eq!(
            site_from_feed(&f(&[]), true),
            None,
            "nothing fed, nothing declared"
        );
        assert_eq!(
            site_from_feed(&f(&[(VARIANT_FEED, "holger-twin-monetize")]), true),
            Some(Site::Private),
            "a sold estate's variant"
        );
        assert_eq!(
            site_from_feed(&f(&[(VARIANT_FEED, SITE_VARIANT)]), true),
            Some(Site::Root),
            "an operator site's variant"
        );
        assert_eq!(
            site_from_feed(&f(&[(VARIANT_FEED, SITE_VARIANT)]), false),
            Some(Site::Private),
            "an open build sells nothing"
        );
        assert_eq!(
            site_from_feed(
                &f(&[(VARIANT_FEED, SITE_VARIANT), ("fleet_instance", "acme")]),
                true
            ),
            Some(Site::Private),
            "a company instance is private whatever else is fed"
        );
        assert_eq!(
            site_from_feed(&f(&[("offer.front.fleet_instance", "acme")]), true),
            Some(Site::Private)
        );
        assert_eq!(
            site_from_feed(&f(&[("fleet_instance", "holger-rs")]), true),
            Some(Site::Root),
            "no variant: the instance decides"
        );
        assert_eq!(site_from_feed(&f(&[(VARIANT_FEED, "  ")]), true), None);
    }

    /// ★ RA5-1: the site follows the feed live: a late feed declaring the estate private closes
    /// a core that opened as a site, and a feed that declares nothing leaves it.
    #[test]
    fn the_core_follows_the_site_the_feed_declares() {
        struct Bus(Mutex<Vec<crate::late::Take>>);
        impl crate::late::Feeds for Bus {
            fn subscribe(&self, _: &'static str, take: crate::late::Take) {
                take(&Default::default());
                self.0.lock().unwrap().push(take);
            }
            fn now(&self) -> crate::late::Feed {
                Default::default()
            }
            fn door_role(&self) -> Option<Option<String>> {
                None
            }
        }
        let root = tempfile::tempdir().unwrap();
        let core = open_at(
            root.path(),
            &served_policy(),
            Arc::new(MemoryAuditLog::new()),
            Some(Site::Root),
        )
        .unwrap()
        .unwrap();
        let bus = Bus(Mutex::new(Vec::new()));
        follow_site(&bus, Arc::clone(&core));
        assert_eq!(core.host().site(), Site::Root, "nothing declared yet");
        let feed: crate::late::Feed =
            [(VARIANT_FEED.to_string(), "holger-twin-monetize".to_string())].into();
        for t in bus.0.lock().unwrap().iter() {
            t(&feed);
        }
        assert_eq!(
            core.host().site(),
            Site::Private,
            "the late feed declares a sold estate"
        );
    }

    /// ★ The store outlives a restart, and a damaged file is refused by name, never read as
    /// "nobody is here".
    #[test]
    fn the_store_survives_a_restart_and_a_damaged_file_is_refused() {
        let root = tempfile::tempdir().unwrap();
        let s = HolgerStore::open(root.path()).unwrap();
        let ada = User {
            id: "u-1".into(),
            display: "Ada".into(),
            email: Some("ada@mock.invalid".into()),
            groups: vec![ADMINS.into()],
            state: UserState::Active,
            created: 1,
        };
        s.put_user(&ada).unwrap();
        s.put_credential(&Credential {
            id: vec![1, 2, 3],
            user: "u-1".into(),
            public_key: vec![4; 65],
            sign_count: 7,
            created: 1,
        })
        .unwrap();
        s.update_revocations(&mut |r| r.revoke_subject("u-1", 5))
            .unwrap();
        let again = HolgerStore::open(root.path()).unwrap();
        assert_eq!(again.user("u-1").unwrap(), Some(ada));
        assert_eq!(again.credential(&[1, 2, 3]).unwrap().unwrap().sign_count, 7);
        assert!(!again.revocations().unwrap().admits("u-1", 5));
        std::fs::write(root.path().join(STORE_DIR).join(STORE_FILE), b"{ not json").unwrap();
        assert!(HolgerStore::open(root.path())
            .unwrap_err()
            .contains("does not read"));
    }

    /// ★ RA5-3: **a recharge that writes the store's file under a running process is read, and
    /// the next write keeps every person.** The pulled Recharge (and the twin's follow) write
    /// `holger-passkeys/identity.json` with no restart; the process's store must see it, never
    /// save its stale tables over it.
    #[test]
    fn a_recharge_written_under_the_store_is_read_and_a_later_write_keeps_every_person() {
        let root = tempfile::tempdir().unwrap();
        let person = |id: &str, name: &str| User {
            id: id.into(),
            display: name.into(),
            email: None,
            groups: vec![READERS.into()],
            state: UserState::Active,
            created: 1,
        };
        // The rebuilt main boots with an empty store, held.
        let s = HolgerStore::open(root.path()).unwrap();
        assert!(s.users().unwrap().is_empty());
        // The recharge writes the twin's people: renamed into place (a new file) ...
        let twin = HolgerStore::open(&tempfile::tempdir().unwrap().keep()).unwrap();
        twin.put_user(&person("u-ada", "Ada")).unwrap();
        twin.put_credential(&Credential {
            id: vec![9, 9],
            user: "u-ada".into(),
            public_key: vec![4; 65],
            sign_count: 3,
            created: 1,
        })
        .unwrap();
        twin.update_revocations(&mut |r| r.revoke_subject("u-old", 5))
            .unwrap();
        let file = root.path().join(STORE_DIR).join(STORE_FILE);
        let tmp = root.path().join("pulled.json");
        std::fs::copy(&twin.path, &tmp).unwrap();
        std::fs::rename(&tmp, &file).unwrap();
        assert_eq!(
            s.user("u-ada").unwrap().map(|u| u.display),
            Some("Ada".into()),
            "the store reads what the recharge wrote"
        );
        // ... and the first write after it (an invite, a sign-up, the shop's take-in) keeps them.
        s.put_user(&person("u-bo", "Bo")).unwrap();
        // A second writer rewrites the file in place (the same file, new bytes).
        let mut t: Tables = serde_json::from_slice(&std::fs::read(&file).unwrap()).unwrap();
        t.users.push(UserRow::from(&person("u-cy", "Cy")));
        std::fs::write(&file, serde_json::to_vec(&t).unwrap()).unwrap();
        s.put_session(&Session {
            id: "sess".into(),
            user: "u-bo".into(),
            auth_time: 1,
            expires: i64::MAX,
            stepped_up_at: None,
        })
        .unwrap();
        let again = HolgerStore::open(root.path()).unwrap();
        let mut ids: Vec<String> = again.users().unwrap().into_iter().map(|u| u.id).collect();
        ids.sort();
        assert_eq!(ids, vec!["u-ada", "u-bo", "u-cy"], "every person is kept");
        assert_eq!(
            again.credential(&[9, 9]).unwrap().map(|c| c.sign_count),
            Some(3),
            "the passkey is kept"
        );
        assert!(
            !again.revocations().unwrap().admits("u-old", 5),
            "the revocation floor is kept"
        );
        // A file the other writer left damaged is refused by name, never written over.
        std::fs::write(&file, b"{ half a file").unwrap();
        assert!(s
            .put_user(&person("u-dee", "Dee"))
            .unwrap_err()
            .0
            .contains("does not read"));
        assert_eq!(std::fs::read(&file).unwrap(), b"{ half a file");
    }

    /// ★ Roles are the policy's, as every door asks it.
    #[test]
    fn roles_are_the_policys() {
        let h = HolgerHarness.host(Site::Private, &Kit::default());
        let user = |g: &str| User {
            id: "u".into(),
            display: "x".into(),
            email: None,
            groups: vec![g.into()],
            state: UserState::Active,
            created: 0,
        };
        assert_eq!(
            h.roles(&user(ADMINS)),
            Roles {
                admin: true,
                names: vec!["admin".into()]
            }
        );
        assert_eq!(
            h.roles(&user(READERS)),
            Roles {
                admin: false,
                names: vec!["reader".into()]
            }
        );
    }

    /// ★ An identity event lands in Holger's audit chain, and no secret with it.
    #[test]
    fn an_event_is_written_to_holgers_audit_chain() {
        let audit = Arc::new(MemoryAuditLog::new());
        let root = tempfile::tempdir().unwrap();
        let h = HolgerHost::new(
            Site::Private,
            HolgerStore::open(root.path()).unwrap(),
            policy(),
            audit.clone(),
            Box::new(nornir_identity::NoMail),
        );
        h.audit(Event::InviteMinted {
            invite: "inv-hash".into(),
            by: "u-1".into(),
            mailed: false,
        });
        let ev = audit.events();
        assert_eq!(ev.len(), 1);
        assert_eq!(
            (ev[0].ident.as_str(), ev[0].artifact.as_str(), ev[0].status),
            ("u-1", "identity/invite-minted", 200)
        );
        assert_eq!(ev[0].detail, "invite inv-hash, mailed false");
    }

    /// ★ A refused passkey login is written to the person the passkey named (nornir-identity
    /// 0.2.1's `LoginRefused.user`); a passkey nobody here holds stays anonymous.
    #[test]
    fn a_refused_login_is_in_the_audit_of_the_person_the_passkey_named() {
        let audit = Arc::new(MemoryAuditLog::new());
        let root = tempfile::tempdir().unwrap();
        let h = HolgerHost::new(
            Site::Private,
            HolgerStore::open(root.path()).unwrap(),
            policy(),
            audit.clone(),
            Box::new(nornir_identity::NoMail),
        );
        h.audit(Event::LoginRefused {
            code: nornir_identity::Code::NotActive,
            user: Some("u-1".into()),
        });
        h.audit(Event::LoginRefused {
            code: nornir_identity::Code::Ceremony,
            user: None,
        });
        let ev = audit.events();
        assert_eq!(ev.len(), 2);
        assert_eq!(
            (ev[0].ident.as_str(), ev[0].artifact.as_str(), ev[0].status),
            ("u-1", "identity/login-refused", 401)
        );
        assert_eq!(ev[0].detail, nornir_identity::Code::NotActive.to_string());
        assert_eq!(
            (ev[1].ident.as_str(), ev[1].artifact.as_str(), ev[1].status),
            ("anonymous", "identity/login-refused", 401)
        );
    }

    /// ★ The link a mail carries is this box's login page, and an open build mails nothing.
    #[test]
    fn the_mail_carries_the_login_link_and_an_open_build_refuses_by_name() {
        assert_eq!(
            link("https://holger.mock.invalid/", "s3cret"),
            "https://holger.mock.invalid/login?invite=s3cret"
        );
        let (subject, body) = words(
            MailKind::Invite,
            &link("https://holger.mock.invalid", "s3cret"),
        );
        assert!(
            subject.contains("Holger")
                && body.contains("https://holger.mock.invalid/login?invite=s3cret")
        );
        #[cfg(not(feature = "metered"))]
        {
            let m = HolgerMailer::of("https://holger.mock.invalid");
            assert_eq!(
                m.send(&Mail {
                    to: "kim@mock.invalid".into(),
                    kind: MailKind::Invite,
                    secret: "s".into()
                }),
                Err(nornir_identity::NO_MAIL.to_owned())
            );
        }
    }

    // ── Serving ──────────────────────────────────────────────────────────────

    use nornir_identity::conformance::{redeem, ORIGIN, RP_ID};
    use nornir_identity::grpc::pb::identity_service_server::IdentityService as _;
    use nornir_identity::grpc::{pb, IdentityGrpc};
    use nornir_identity::test_host::TestHost;

    /// A policy with a passkey method for the suite's relying party.
    fn served_policy() -> AuthConfig {
        let mut auth = policy();
        auth.methods = vec![crate::auth::AuthMethodConfig::Passkey {
            rp_id: RP_ID.into(),
            rp_origin: ORIGIN.into(),
            rp_name: String::new(),
            session_key_file: None,
            bootstrap_until_first_admin: true,
        }];
        auth
    }

    /// ★ A box with a passkey method serves a core; a box without one serves none.
    #[test]
    fn a_box_with_a_passkey_method_serves_the_identity_core_and_one_without_serves_none() {
        let root = tempfile::tempdir().unwrap();
        assert!(
            open(root.path(), &policy(), Arc::new(MemoryAuditLog::new()))
                .unwrap()
                .is_none(),
            "no passkey method, no IdentityService"
        );
        let core = open(
            root.path(),
            &served_policy(),
            Arc::new(MemoryAuditLog::new()),
        )
        .unwrap()
        .expect("a passkey method serves the core");
        // The site is the data the box holds: an open build sells nothing, and a box that was
        // neither enrolled nor fed a site is invite only (RA5-1: never by its passkey host).
        assert_eq!(
            core.host().site(),
            Site::Private,
            "a box that does not know where it is sells nothing"
        );
        let fed = open_at(
            tempfile::tempdir().unwrap().path(),
            &served_policy(),
            Arc::new(MemoryAuditLog::new()),
            Some(Site::Root),
        )
        .unwrap()
        .unwrap();
        assert_eq!(fed.host().site(), Site::Root, "the site the ONE box fed");
        assert_eq!(core.config().parties.len(), 1);
        assert!(
            config_of("10.0.0.7", "https://10.0.0.7").is_err(),
            "an IP literal is no relying party"
        );
    }

    /// ★ A core session is a credential on every door (`auth::validate_request`): its person
    /// with the groups they hold, and refused the moment it ends.
    #[tokio::test]
    async fn a_core_session_is_a_credential_on_every_door_until_it_ends() {
        let root = tempfile::tempdir().unwrap();
        let auth = served_policy();
        let core = open(root.path(), &auth, Arc::new(MemoryAuditLog::new()))
            .unwrap()
            .unwrap();
        register_live(&auth, &core);
        let first = core
            .first_admin_invite(vec![ADMINS.into()], "installer")
            .unwrap();
        let (ada, _) = redeem(&core, &first.secret, "Ada", 1);
        let id = crate::auth::validate_request(&auth, Some(&ada.token), None)
            .await
            .unwrap()
            .expect("the session is a credential");
        assert_eq!(
            (id.subject.as_str(), id.method.as_str(), id.groups.clone()),
            (
                ada.session.user_id.as_str(),
                "passkey",
                vec![ADMINS.to_string()]
            )
        );
        assert!(auth.role_for_identity(&id).can_admin());
        core.logout(&ada.token, false).unwrap();
        assert!(
            crate::auth::validate_request(&auth, Some(&ada.token), None)
                .await
                .is_err(),
            "an ended session is refused like a forged one"
        );
        assert!(
            crate::auth::validate_request(&auth, Some("nis1.forged"), None)
                .await
                .is_err()
        );
    }

    /// One scripted walk over the IdentityService adapter, every reply with its ids, tokens and
    /// times taken out: what a console sees of a product.
    async fn wire_transcript<H: IdentityHost + 'static>(
        core: Arc<nornir_identity::Identity<H>>,
        kit: &Kit,
        admin_groups: Vec<String>,
        member_groups: Vec<String>,
    ) -> Vec<String> {
        let svc = IdentityGrpc::new(Arc::clone(&core));
        let said = |r: Result<String, tonic::Status>| match r {
            Ok(v) => format!("ok {v}").trim_end().to_owned(),
            Err(s) => format!(
                "refused {:?} {}",
                s.code(),
                nornir_identity::grpc::refusal_of(&s)
                    .map(|c| c.as_str())
                    .unwrap_or("-")
            ),
        };
        let info = |s: &pb::SessionInfo| {
            format!(
                "display={} roles={} admin={} stepped_up={}",
                s.display,
                s.roles.len(),
                s.admin,
                s.stepped_up
            )
        };
        let mut out = Vec::new();
        let r = svc
            .sign_up(tonic::Request::new(pb::SignUpRequest {
                email: "kim@mock.invalid".into(),
                display: "Kim".into(),
            }))
            .await;
        out.push(format!(
            "sign_up: {}",
            said(r.map(|_| format!("mails={}", kit.mail.sent().len())))
        ));
        let first = core
            .first_admin_invite(admin_groups.clone(), "installer")
            .unwrap();
        let (ada, mut ada_key) = redeem(&core, &first.secret, "Ada", 1);
        // RA5-2 (nornir-identity 0.2.2): an invite needs a fresh step-up.
        nornir_identity::conformance::step_up(&core, &ada.token, &mut ada_key)
            .expect("ada steps up");
        let r = svc
            .whoami(tonic::Request::new(pb::WhoamiRequest {
                session: ada.token.clone(),
            }))
            .await;
        out.push(format!("whoami: {}", said(r.map(|w| info(w.get_ref())))));
        let ask = |groups: Vec<String>, email: &str| pb::CreateInviteRequest {
            session: ada.token.clone(),
            display: String::new(),
            email: email.into(),
            groups,
            ttl_secs: 0,
            person: String::new(),
        };
        let r = svc
            .create_invite(tonic::Request::new(ask(admin_groups.clone(), "")))
            .await;
        out.push(format!("invite_admin: {}", said(r.map(|_| String::new()))));
        let r = svc
            .create_invite(tonic::Request::new(ask(
                member_groups.clone(),
                "bo@mock.invalid",
            )))
            .await;
        let bo_secret = r.as_ref().ok().map(|i| i.get_ref().secret.clone());
        out.push(format!(
            "invite: {}",
            said(r.map(|i| format!(
                "mailed={} refusal={}",
                i.get_ref().mailed,
                !i.get_ref().mail_refusal.is_empty()
            )))
        ));
        let r = svc
            .redeem_start(tonic::Request::new(pb::RedeemStartRequest {
                secret: "not-an-invite".into(),
                display: "Eve".into(),
                rp_id: RP_ID.into(),
                origin: ORIGIN.into(),
                client: String::new(),
            }))
            .await;
        out.push(format!(
            "redeem_unknown: {}",
            said(r.map(|_| String::new()))
        ));
        if let Some(secret) = bo_secret {
            let (bo, _) = redeem(&core, &secret, "Bo", 3);
            let r = svc
                .whoami(tonic::Request::new(pb::WhoamiRequest {
                    session: bo.token.clone(),
                }))
                .await;
            out.push(format!("bo_whoami: {}", said(r.map(|w| info(w.get_ref())))));
            let r = svc
                .create_invite(tonic::Request::new(pb::CreateInviteRequest {
                    session: bo.token.clone(),
                    ..Default::default()
                }))
                .await;
            out.push(format!("bo_invites: {}", said(r.map(|_| String::new()))));
            // Past the step-up window: the deactivation below is the unstepped one.
            kit.clock.advance(10 * 60 + 1);
            let r = svc
                .deactivate(tonic::Request::new(pb::DeactivateRequest {
                    session: ada.token.clone(),
                    user_id: bo.session.user_id.clone(),
                }))
                .await;
            out.push(format!(
                "deactivate_unstepped: {}",
                said(r.map(|_| String::new()))
            ));
            let r = svc
                .logout(tonic::Request::new(pb::LogoutRequest {
                    session: bo.token.clone(),
                    everywhere: true,
                }))
                .await;
            out.push(format!("bo_logout: {}", said(r.map(|_| String::new()))));
            let r = svc
                .whoami(tonic::Request::new(pb::WhoamiRequest { session: bo.token }))
                .await;
            out.push(format!(
                "bo_after_logout: {}",
                said(r.map(|w| info(w.get_ref())))
            ));
        }
        let r = svc
            .login_start(tonic::Request::new(pb::LoginStartRequest {
                rp_id: "evil.mock.invalid".into(),
                origin: "https://evil.mock.invalid".into(),
                client: String::new(),
            }))
            .await;
        out.push(format!(
            "login_elsewhere: {}",
            said(r.map(|_| String::new()))
        ));
        let r = svc
            .logout(tonic::Request::new(pb::LogoutRequest {
                session: ada.token.clone(),
                everywhere: false,
            }))
            .await;
        out.push(format!("logout: {}", said(r.map(|_| String::new()))));
        let r = svc
            .whoami(tonic::Request::new(pb::WhoamiRequest {
                session: ada.token,
            }))
            .await;
        out.push(format!(
            "after_logout: {}",
            said(r.map(|w| info(w.get_ref())))
        ));
        out
    }

    /// ★ **The diff test**: Holger's IdentityService answers the same scripted steps exactly
    /// as the reference host does, on a Root and on a Private site (Gunnar's runs the same walk
    /// against the same reference: two products, one set of replies), and the core's own
    /// transcript is the reference one (the conformance suite).
    #[tokio::test]
    async fn holgers_identity_service_answers_as_the_reference_host_does() {
        for site in [Site::Root, Site::Private] {
            let kit = Kit::default();
            let holger = Arc::new(nornir_identity::Identity::new(
                HolgerHarness.host(site, &kit),
                nornir_identity::conformance::config(),
            ));
            let ours =
                wire_transcript(holger, &kit, vec![ADMINS.into()], vec![READERS.into()]).await;
            let kit = Kit::default();
            let reference = Arc::new(nornir_identity::Identity::new(
                TestHost::new(site, &kit),
                nornir_identity::conformance::config(),
            ));
            let theirs = wire_transcript(
                reference,
                &kit,
                vec![nornir_identity::test_host::TEST_ADMINS.into()],
                vec!["readers".into()],
            )
            .await;
            assert_eq!(
                ours, theirs,
                "{site:?}: Holger's replies differ from the reference's"
            );
            assert!(ours.len() >= 10, "{ours:?}");
        }
    }
}