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/passkey.rs

//! **The passkey door** — Holger's HTTP face of edda's identity core (IDENTITY-HOST).
//!
//! Sign-up, login, the passkey ceremony, invites, sessions and step-up are ONE implementation,
//! `nornir_identity::Identity` over Holger's host ([`crate::identity_host`]). This module is
//! the door a browser and the CLI speak (`/-/auth/passkey/*` on the HTTP gateway): it reads
//! their JSON, asks the core, and answers in the shape the console reads. What stays here is
//! Holger's own and only that:
//!
//! * **the policy's words**: a role or a duty becomes the group the declarative `AuthConfig`
//!   maps to it ([`group_for_role`], [`group_for_duty`], [`first_admin_groups`]), and the names
//!   a machine acts under are no person's ([`is_reserved_name`]);
//! * **where a person comes from**: the shop's fed first-login invite (its SHA-256, taken in
//!   once, [`PasskeyDoor::seed_first_admin`]), a paid sign-up's repository (`signup`), the
//!   break-glass bearer that enrols the FIRST admin and then retires, the first-key door, the
//!   resend sign-in link; each lands as an invite in the core's store and is redeemed by the
//!   core;
//! * **the sold record**: a redeemed sign-up binds its repository to the person, the shop's
//!   buyer owns what the order made.
//!
//! The ceremony, the session token (opaque, kept as a hash), the floors, the single-use rule and
//! the step-up window are the core's. The JSON shapes are the ones the console has always read
//! (`publicKey` options in, the browser's response out), so the page changed nothing.

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

use base64::Engine as _;
use nornir_identity::{
    Assertion, ClientKey, Code, Event, IdentityHost as _, Invite, InviteAsk, Refused, Registration,
    Relay, UserState,
};
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};

use crate::auth::{AuthConfig, AuthIdentity};
use crate::identity_host::{identity_of, SharedIdentity};
use crate::lock::LockOrRecover as _;

const B64: base64::engine::general_purpose::GeneralPurpose =
    base64::engine::general_purpose::URL_SAFE_NO_PAD;

/// A refusal: the HTTP status and the words.
pub type Refusal = (u16, String);

fn refuse(status: u16, msg: impl Into<String>) -> Refusal {
    (status, msg.into())
}

/// Seconds since the epoch by the ESTATE's clock (`crate::clock`).
fn now_secs() -> i64 {
    i64::try_from(crate::clock::now_unix_secs()).unwrap_or(i64::MAX)
}

fn str_field<'a>(v: &'a Value, key: &str) -> Option<&'a str> {
    v.get(key)
        .and_then(Value::as_str)
        .map(str::trim)
        .filter(|s| !s.is_empty())
}

fn b64_field(v: &Value, key: &str) -> Result<Vec<u8>, Refusal> {
    let raw = v.get(key).and_then(Value::as_str).unwrap_or_default();
    B64.decode(raw)
        .map_err(|_| refuse(400, format!("`{key}` is not base64url")))
}

fn groups_field(v: &Value) -> Vec<String> {
    v.get("groups")
        .and_then(Value::as_array)
        .map(|a| {
            a.iter()
                .filter_map(Value::as_str)
                .map(str::trim)
                .filter(|s| !s.is_empty())
                .map(str::to_owned)
                .collect()
        })
        .unwrap_or_default()
}

/// How long a ceremony stays open, as the browser is told (the core's own bound decides).
const CEREMONY_TIMEOUT_MS: i64 = 300_000;

/// The step-up window and its machine word (edda `nornir-identity-session`).
pub use nornir_identity_session::step_up::{STEP_UP_CODE, STEP_UP_WINDOW_SECS};

/// The words of the refusal a session that is not stepped up earns; a console recognises it by
/// its [`STEP_UP_CODE`], never by these words.
pub const STEP_UP_REFUSAL: &str =
    "log in again with your key first: this needs a user-verified session from the last 10 minutes";

/// **The body of a passkey door's refusal**: `{"error": words}`, and `"code": "stepup"` when the
/// refusal is [`STEP_UP_REFUSAL`].
pub fn refusal_json(words: &str) -> Value {
    if words == STEP_UP_REFUSAL {
        json!({ "error": words, "code": STEP_UP_CODE })
    } else {
        json!({ "error": words })
    }
}

/// A core refusal in the door's words and status.
fn said(r: Refused) -> Refusal {
    let status = match r.code {
        Code::SignUpClosed | Code::InviteWouldBeAdmin | Code::NotAdmin => 403,
        Code::InviteNotOpen | Code::NoSession => 401,
        // A deactivated person's key logs in no more: the plain 401 a stranger's gets.
        Code::NotActive => 401,
        // A ceremony that is gone (spent, expired, never opened) is not there.
        Code::Ceremony if r.detail.contains("was not opened here") => 404,
        Code::Bootstrapped => return refuse(409, FIRST_KEY_ENROLLED),
        Code::NeedsStepUp => return refuse(403, STEP_UP_REFUSAL),
        Code::Busy => 429,
        Code::MailRefused => 503,
        Code::Store => 500,
        _ => 400,
    };
    let words = match r.code {
        Code::InviteNotOpen => "this invite is not valid any more; ask for a new one".to_owned(),
        Code::InviteWouldBeAdmin => "an invite cannot grant admin: it is a link that travels out of band. The first admin is enrolled once; every other person is invited".to_owned(),
        Code::NotAdmin => "this action requires an admin identity".to_owned(),
        _ => r.detail,
    };
    (status, words)
}

// ─── The policy's words ──────────────────────────────────────────────────────

/// The role words a console shows for holger's roles.
pub(crate) fn role_word(role: crate::auth::Role) -> &'static str {
    match role {
        crate::auth::Role::Denied => "denied",
        crate::auth::Role::Reader => "reader",
        crate::auth::Role::Writer => "writer",
        crate::auth::Role::Admin => "admin",
    }
}

/// **The group an invite or an enrolment by ROLE puts a person in**: the group this server's
/// `group_roles` maps to exactly that role (the first by name). The console's words are
/// accepted beside holger's (`viewer` = Reader, `operator` = Writer).
pub fn group_for_role(auth: &AuthConfig, role: &str) -> Result<String, Refusal> {
    let want = match role.trim().to_ascii_lowercase().as_str() {
        "reader" | "viewer" => crate::auth::Role::Reader,
        "writer" | "operator" => crate::auth::Role::Writer,
        "admin" => crate::auth::Role::Admin,
        other => {
            return Err(refuse(
                400,
                format!("`{other}` is not a role (reader, writer or admin)"),
            ))
        }
    };
    let mut groups: Vec<&String> = auth
        .group_roles
        .iter()
        .filter(|(_, r)| **r == want)
        .map(|(g, _)| g)
        .collect();
    groups.sort();
    groups.first().map(|g| (*g).clone()).ok_or_else(|| {
        refuse(
            409,
            format!(
                "this server's `group_roles` names no group with the role {want:?}, so nobody can be given it by role; add one (for example \"holger-{}s\": {want:?}) to the auth block",
                format!("{want:?}").to_ascii_lowercase()
            ),
        )
    })
}

/// **The group an invite by DUTY puts a person in** (lane W7): a group `group_duties` grants
/// that duty through, never an admin group, the one with the fewest duties first.
pub fn group_for_duty(auth: &AuthConfig, word: &str) -> Result<String, Refusal> {
    let duty = crate::auth::duty_from_word(word)
        .ok_or_else(|| refuse(400, format!("`{word}` is not a duty (importer, security_approver, auditor, developer or repository_admin)")))?;
    let admin = |g: &str| {
        auth.role_for_identity(&groups_identity(&[g.to_owned()]))
            .can_admin()
    };
    let mut fits: Vec<(usize, &String)> = auth
        .group_duties
        .iter()
        .filter(|(g, d)| {
            d.contains(&duty) && (duty == crate::auth::Duty::RepositoryAdmin || !admin(g))
        })
        .map(|(g, d)| (d.len(), g))
        .collect();
    fits.sort();
    fits.first().map(|(_, g)| (*g).clone()).ok_or_else(|| {
        refuse(
            409,
            format!(
                "this server's `group_duties` names no group with the duty {}, so nobody can be given it by an invite; add one (for example \"holger-{}s\": [{}]) to the auth block",
                duty.name(),
                duty.name().to_ascii_lowercase(),
                duty.name()
            ),
        )
    })
}

fn groups_identity(groups: &[String]) -> AuthIdentity {
    AuthIdentity {
        subject: String::new(),
        method: "passkey".to_owned(),
        groups: groups.to_vec(),
        display: None,
    }
}

/// **The names a MACHINE holds here, which no person may take**: every static-token subject
/// the policy declares, the console's own names, `anonymous`, and anything shaped like a
/// subject.
pub fn is_reserved_name(auth: &AuthConfig, name: &str) -> bool {
    let n = name.trim().to_ascii_lowercase();
    if n.is_empty()
        || n.starts_with("passkey:")
        || (n.starts_with("u-") && n.len() == 18)
        || ["anonymous", "holger-ui", "admin-token"].contains(&n.as_str())
    {
        return true;
    }
    auth.methods.iter().any(|m| matches!(m, crate::auth::AuthMethodConfig::StaticToken { subject, .. } if subject.trim().eq_ignore_ascii_case(&n)))
        || auth.roles.keys().any(|k| k.trim().eq_ignore_ascii_case(&n))
}

fn refuse_reserved(auth: &AuthConfig, name: &str) -> Result<(), Refusal> {
    if is_reserved_name(auth, name) {
        return Err(refuse(400, format!("{name:?} is a name a machine of this server acts under (or one shaped like an identity), so no person may take it; choose another")));
    }
    Ok(())
}

/// **An estate owner's invitee is of the owner's estate** (lane P1): an invite minted by one of
/// the estate's owners puts its invitee in [`crate::auth::ESTATE_OWNERS`] too.
pub fn with_owners_of(body: &Value, inviter: Option<&AuthIdentity>) -> Value {
    let owners = crate::auth::ESTATE_OWNERS;
    if !inviter.is_some_and(|i| i.groups.iter().any(|g| g == owners)) {
        return body.clone();
    }
    let mut b = body.clone();
    let mut groups = groups_field(&b);
    if !groups.iter().any(|g| g == owners) {
        groups.push(owners.to_string());
    }
    b["groups"] = json!(groups);
    b
}

/// **The groups the shop's buyer is enrolled in** (lane P1, W7's duties): the admin group, the
/// auditors' when that holds no Auditor, and the estate's owners; RepositoryAdmin,
/// SecurityApprover and Auditor, never Importer.
pub fn first_admin_groups(auth: &AuthConfig) -> Result<Vec<String>, Refusal> {
    use crate::auth::Duty;
    if !auth.duties_enabled() {
        return Err(refuse(409, "this server grants no duties, so the shop's first admin cannot be given RepositoryAdmin, SecurityApprover and Auditor"));
    }
    let duties = |groups: &[String]| auth.duties_for_identity(&groups_identity(groups));
    let mut groups = vec![group_for_role(auth, "admin")?];
    if !duties(&groups).contains(&Duty::Auditor) {
        groups.push(group_for_duty(auth, "auditor")?);
    }
    groups.push(crate::auth::ESTATE_OWNERS.to_string());
    let held = duties(&groups);
    let missing: Vec<&str> = [Duty::RepositoryAdmin, Duty::SecurityApprover, Duty::Auditor]
        .into_iter()
        .filter(|d| !held.contains(d))
        .map(|d| d.name())
        .collect();
    if !missing.is_empty() {
        return Err(refuse(
            409,
            format!(
                "the shop's first admin would not hold {} under this server's duties ({})",
                missing.join(", "),
                groups.join(", ")
            ),
        ));
    }
    if held.contains(&Duty::Importer) {
        return Err(refuse(409, format!("the shop's first admin would hold Importer under this server's duties ({}); an admin judges uploads and never makes them", groups.join(", "))));
    }
    Ok(groups)
}

// ─── Where a person comes from ───────────────────────────────────────────────

/// The shop's fed first-login invite, as the installer fed it (lane P1): the SHA-256 of the
/// secret the buyer's mail carries, how long it lives and the name it is pinned to.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FirstAdminInvite {
    pub sha256_hex: String,
    pub ttl_secs: i64,
    pub display: String,
}

/// The one answer to a sign-up that cannot be redeemed.
pub const SIGNUP_REFUSED: &str =
    "this sign-up cannot be redeemed here (it is already someone's, or it was not made here)";
/// Who a fed first-login invite was "minted" by.
pub const FIRST_ADMIN_BY: &str = "the shop's first-login invite";
/// Who a first-key invite was minted by (`holger.doors.v1.FirstKeyService`).
pub const FIRST_KEY_BY: &str = "the first-key door";

/// The file a plain start writes its first sign-in link into, under the data root (lane
/// HOL-FIELD-TEST P3): `<rp_origin>/login?invite=<secret>`, 0600.
pub const FIRST_SIGN_IN_FILE: &str = "holger-passkeys/first-sign-in.url";
/// The one answer once the first admin key exists.
pub const FIRST_KEY_ENROLLED: &str = "this server already has an admin key: the first-key door is shut; an admin enrols further keys with their own session";
/// Who a resend sign-in link was minted by (A16, monetize's signed order).
pub const SIGN_IN_AGAIN_BY: &str = "monetize:sign_in_again";
/// Who a paid sign-up's invite is minted by: `signup:<repository>`.
const SIGNUP_BY: &str = "signup:";
/// The longest a fed first-login invite may live.
const FIRST_ADMIN_TTL_MAX: i64 = 30 * 24 * 3600;
/// The record that the fed invite was taken in, once ([`crate::auth::FIRST_ADMIN_MARKER_REL`]).
const FIRST_ADMIN_FILE: &str = "first-admin.json";

/// What [`FIRST_ADMIN_FILE`] says.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
struct FirstAdminSeeded {
    sha256: String,
    at: i64,
    /// `false`: the box already had an admin, so the fed invite was not kept.
    seeded: bool,
    #[serde(default)]
    display: String,
    #[serde(default)]
    ttl_secs: i64,
    /// The buyer, once registered.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    used_by: Option<String>,
}

fn sha256_hex(secret: &str) -> String {
    use sha2::Digest as _;
    hex::encode(sha2::Sha256::digest(secret.trim().as_bytes()))
}

/// What one open enrolment is for, by its ceremony (the core holds the ceremony; this is the
/// door's own note of where the person comes from).
#[derive(Debug, Clone, Default)]
struct Note {
    /// The repository a paid sign-up binds to the person.
    bind: Option<String>,
    /// The shop's buyer (a fed first-login invite or a resend link).
    buyer: bool,
    /// An admin enrolled a key for somebody else: no session for the admin's browser.
    for_another: bool,
    /// The invite the ceremony redeems (its hash), for the first-key rule at the finish.
    invite: Option<String>,
    label: String,
    /// When the ceremony opened (Unix seconds): a note lives as long as its ceremony may be
    /// answered, and no longer.
    opened: i64,
}

/// Open notes kept at most. Past it a new enrolment is refused by name (RA5-15); an open one
/// is never dropped to make room.
const MAX_NOTES: usize = 1024;
/// Refused when [`MAX_NOTES`] enrolments are open at once.
pub const ENROLMENTS_FULL: &str =
    "too many enrolments are open on this server right now; try again in a few minutes";
/// Refused when a finish names a ceremony whose note this door does not hold.
pub const ENROLMENT_UNKNOWN: &str = "this enrolment is not open here any more (it expired, or the server restarted): start it again";

/// **The passkey door of one box**: its identity core, its data root, its relying party.
pub struct PasskeyDoor {
    core: SharedIdentity,
    data_root: PathBuf,
    rp_id: String,
    /// The origins a ceremony may come from (WWW-CANON): the site's canonical one first
    /// (`https://www.<rp_id>`), then the kept `rp_origin` ([`crate::identity_host::origins_of`]).
    origins: Vec<String>,
    rp_name: String,
    bootstrap_until_first_admin: bool,
    notes: Mutex<HashMap<String, Note>>,
    marker_lock: Mutex<()>,
    gate: OnceLock<Arc<crate::auth::ReadGate>>,
}

/// The door, shared by the HTTP gateway, the gRPC doors and the auth seam.
pub type SharedPasskeyDoor = Arc<PasskeyDoor>;

/// The notes whose ceremony can no longer be answered, dropped.
fn prune_notes(notes: &mut HashMap<String, Note>, now: i64) {
    notes.retain(|_, n| now - n.opened <= CEREMONY_TIMEOUT_MS / 1000 + 60);
}

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

fn live_doors() -> &'static Mutex<Vec<(String, SharedPasskeyDoor)>> {
    static LIVE: OnceLock<Mutex<Vec<(String, SharedPasskeyDoor)>>> = OnceLock::new();
    LIVE.get_or_init(Default::default)
}

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,
    })
}

/// Make `door` the one [`door_for`] answers under `auth`'s passkey method, and its core the
/// one every door's auth seam asks (`identity_host::session_identity`).
pub fn register_live(auth: &AuthConfig, door: &SharedPasskeyDoor) {
    crate::identity_host::register_live(auth, &door.core);
    let Some(key) = live_key(auth) else { return };
    let mut live = live_doors().lock_or_recover();
    live.retain(|(k, _)| k != &key);
    live.push((key, Arc::clone(door)));
}

/// The door this process serves under `auth`'s passkey method, if any.
pub fn door_for(auth: &AuthConfig) -> Option<SharedPasskeyDoor> {
    let key = live_key(auth)?;
    live_doors()
        .lock_or_recover()
        .iter()
        .find(|(k, _)| *k == key)
        .map(|(_, d)| Arc::clone(d))
}

impl PasskeyDoor {
    /// **The door of the box at `data_root`** under `auth` (its passkey method resolved), its
    /// events on `audit`. `None`: no passkey method, no door.
    pub fn open(
        data_root: &Path,
        auth: &AuthConfig,
        audit: Arc<dyn crate::audit::AuditLog>,
    ) -> Result<Option<SharedPasskeyDoor>, String> {
        Self::open_at(data_root, auth, audit, None)
    }

    /// [`Self::open`] at the site the ONE box fed (`identity_host::open_at`).
    pub fn open_at(
        data_root: &Path,
        auth: &AuthConfig,
        audit: Arc<dyn crate::audit::AuditLog>,
        site: Option<nornir_identity::Site>,
    ) -> Result<Option<SharedPasskeyDoor>, String> {
        let Some((rp_id, rp_origin, rp_name, bootstrap)) =
            auth.methods.iter().find_map(|m| match m {
                crate::auth::AuthMethodConfig::Passkey {
                    rp_id,
                    rp_origin,
                    rp_name,
                    bootstrap_until_first_admin,
                    ..
                } => Some((
                    rp_id.trim().to_owned(),
                    rp_origin.trim().trim_end_matches('/').to_owned(),
                    rp_name.trim().to_owned(),
                    *bootstrap_until_first_admin,
                )),
                _ => None,
            })
        else {
            return Ok(None);
        };
        let Some(core) = crate::identity_host::open_at(data_root, auth, audit, site)? else {
            return Ok(None);
        };
        Ok(Some(Arc::new(PasskeyDoor {
            core,
            data_root: data_root.to_path_buf(),
            rp_name: if rp_name.is_empty() {
                "holger".to_owned()
            } else {
                rp_name
            },
            origins: crate::identity_host::origins_of(&rp_id, &rp_origin),
            rp_id,
            bootstrap_until_first_admin: bootstrap,
            notes: Mutex::new(HashMap::new()),
            marker_lock: Mutex::new(()),
            gate: OnceLock::new(),
        })))
    }

    /// The core this door asks.
    pub fn core(&self) -> &SharedIdentity {
        &self.core
    }

    /// The directory the people live in (`<data_root>/holger-passkeys`), which the twin follows.
    pub fn root(&self) -> PathBuf {
        self.data_root.join(crate::identity_host::STORE_DIR)
    }

    /// The relying party's id.
    pub fn rp_id(&self) -> &str {
        &self.rp_id
    }

    /// **The read gate every door holds** (lane P1): attached once, at the start.
    pub fn attach_gate(&self, gate: Arc<crate::auth::ReadGate>) {
        let _ = self.gate.set(gate);
    }

    fn relay(&self, client: &str) -> Relay {
        let client = client.trim();
        Relay {
            rp_id: self.rp_id.clone(),
            origin: self.origin().to_owned(),
            client: (!client.is_empty()).then(|| ClientKey::of_peer(client)),
        }
    }

    /// **The site's origin** (WWW-CANON): `https://www.<rp_id>` for a site named by its bare
    /// name, else the kept `rp_origin`.
    pub fn origin(&self) -> &str {
        &self.origins[0]
    }

    /// **The origin a ceremony's answer is checked against**: the one the browser signed
    /// (`clientDataJSON.origin`) when it is one of this door's ([`Self::origins`]), else the
    /// site's own, so a foreign origin is refused by the core's check, by name.
    fn origin_of(&self, client_data_json: &[u8]) -> String {
        nornir_auth_webauthn::ceremony::client_data(client_data_json)
            .ok()
            .map(|d| d.origin)
            .filter(|o| self.origins.iter().any(|ours| ours == o))
            .unwrap_or_else(|| self.origin().to_owned())
    }

    /// The origins a ceremony may come from: the site's first.
    pub fn origins(&self) -> &[String] {
        &self.origins
    }

    /// The door's public facts (`GET /-/auth/passkey/config`).
    pub fn config_json(&self) -> Value {
        json!({ "enabled": true, "rp_id": self.rp_id, "rp_origin": self.origin(), "rp_origins": self.origins, "rp_name": self.rp_name })
    }

    /// **Does an active admin exist?** (the break-glass retirement and the first-key rule).
    /// **The first sign-in link of a plain start** (lane HOL-FIELD-TEST P3, Gunnar's first-login
    /// path): while no admin holds a passkey, a single-use first-key invite whose link is written
    /// to [`FIRST_SIGN_IN_FILE`] (0600, the box's user alone). The passkey it enrols is the admin:
    /// no bearer, no attestation step. An open first-key invite with its file kept is left as it
    /// is, so a restart never revokes a link already handed out. A box whose RON names an Admin
    /// subject gets none: that admin mints the invite. `Some(file)` when one was written.
    pub fn offer_first_sign_in(&self, auth: &AuthConfig) -> Result<Option<PathBuf>, Refusal> {
        // An admin that is not a person (a bootstrap bearer, an mTLS subject) mints the first-key
        // invite itself (`holger-cli first-key`); and a box with an admin passkey has its admin.
        if self.has_admin_passkey(auth) {
            // A link a crash left behind after its enrolment: single use on disk too.
            self.forget_first_sign_in();
            return Ok(None);
        }
        if auth.roles.values().any(|r| *r == crate::auth::Role::Admin) {
            return Ok(None);
        }
        let Some(origin) = auth.methods.iter().find_map(|m| match m {
            crate::auth::AuthMethodConfig::Passkey { rp_origin, .. } => {
                Some(rp_origin.trim().trim_end_matches('/').to_owned())
            }
            _ => None,
        }) else {
            return Ok(None);
        };
        let file = self.data_root.join(FIRST_SIGN_IN_FILE);
        let now = now_secs();
        let mut open = false;
        self.core
            .host()
            .store()
            .update_invites(&mut |all| {
                open = all
                    .iter()
                    .any(|i| i.created_by.starts_with(FIRST_KEY_BY) && i.open(now));
            })
            .map_err(|e| refuse(500, e.0))?;
        if open && file.exists() {
            return Ok(None);
        }
        let (secret, _) = self.mint_first_key_invite("", "holger start", auth)?;
        if let Some(dir) = file.parent() {
            std::fs::create_dir_all(dir).map_err(|e| refuse(500, e.to_string()))?;
        }
        crate::keyfile::write_private(
            &file,
            format!("{origin}/login?invite={secret}\n").as_bytes(),
        )
        .map_err(|e| refuse(500, format!("{}: {e}", file.display())))?;
        Ok(Some(file))
    }

    /// **The first sign-in link is single use on disk too** (HOL-DOCFIND f): once a first-key
    /// invite enrolled its passkey (or the box has its admin), [`FIRST_SIGN_IN_FILE`] goes, so
    /// no copy of the data root (a backup, a disk image) holds a credential-shaped file. A file
    /// that cannot be removed is emptied.
    fn forget_first_sign_in(&self) {
        let file = self.data_root.join(FIRST_SIGN_IN_FILE);
        match std::fs::remove_file(&file) {
            Ok(()) => log::warn!(
                "passkey: the first sign-in link was used; {} is removed",
                file.display()
            ),
            Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
            Err(e) => {
                let emptied = std::fs::write(&file, b"").is_ok();
                log::error!(
                    "passkey: the used first sign-in link {} could not be removed ({e}); {}",
                    file.display(),
                    if emptied {
                        "it was emptied"
                    } else {
                        "it could not be emptied either"
                    }
                );
            }
        }
    }

    pub fn has_admin_passkey(&self, _auth: &AuthConfig) -> bool {
        let host = self.core.host();
        host.store()
            .users()
            .unwrap_or_default()
            .iter()
            .any(|u| u.state == UserState::Active && host.roles(u).admin)
    }

    /// The person `subject` as this server knows them now; `None` when not active.
    pub fn person(&self, subject: &str) -> Option<AuthIdentity> {
        let user = self.core.host().store().user(subject).ok()??;
        (user.state == UserState::Active).then(|| identity_of(&user))
    }

    /// **The identity a core session acts as now**, or `None`.
    pub fn live_identity(&self, token: &str) -> Option<AuthIdentity> {
        let view = self.core.session(token).ok()?;
        self.person(&view.user_id)
    }

    // ── Login ────────────────────────────────────────────────────────────────

    /// `POST /-/auth/passkey/login/start` (`{"uv": "required"}` asks a user-verified touch).
    pub fn open_login(&self, body: &Value, client: &str) -> Result<Value, Refusal> {
        let c = self.core.login_start(&self.relay(client)).map_err(said)?;
        let uv = if str_field(body, "uv") == Some("required") || c.user_verification_required {
            "required"
        } else {
            "preferred"
        };
        Ok(json!({
            "ceremony_id": c.id,
            "publicKey": { "challenge": c.challenge, "rpId": c.rp_id, "timeout": CEREMONY_TIMEOUT_MS, "userVerification": uv, "allowCredentials": [] },
        }))
    }

    fn assertion(&self, body: &Value) -> Result<Assertion, Refusal> {
        let id = str_field(body, "id").ok_or_else(|| refuse(400, "`id` is required"))?;
        let response = body
            .get("response")
            .ok_or_else(|| refuse(400, "`response` is required"))?;
        let client_data_json = b64_field(response, "clientDataJSON")?;
        Ok(Assertion {
            ceremony_id: str_field(body, "ceremony_id")
                .unwrap_or_default()
                .to_owned(),
            credential_id: B64
                .decode(id)
                .map_err(|_| refuse(400, "`id` is not base64url"))?,
            origin: self.origin_of(&client_data_json),
            client_data_json,
            authenticator_data: b64_field(response, "authenticatorData")?,
            signature: b64_field(response, "signature")?,
            rp_id: self.rp_id.clone(),
        })
    }

    /// `POST /-/auth/passkey/login/finish`: the core's session.
    pub fn close_login(&self, body: &Value) -> Result<Value, Refusal> {
        str_field(body, "ceremony_id").ok_or_else(|| refuse(400, "`ceremony_id` is required"))?;
        let done = self
            .core
            .login_finish(self.assertion(body)?)
            .map_err(said)?;
        log::info!("passkey: {} logged in", done.session.display);
        Ok(json!({
            "token": done.token,
            "subject": done.session.user_id,
            "expires_at": done.session.expires_at,
            "display": done.session.display,
            // Rendering only, in holger's role names (the server decides on every request).
            "role": role_name(done.session.roles.first().map(String::as_str).unwrap_or_default()),
            "stepped_up": done.session.stepped_up,
        }))
    }

    // ── Step-up ──────────────────────────────────────────────────────────────

    /// `POST /-/auth/passkey/step-up/start`: a fresh challenge for the session's own passkeys.
    pub fn open_step_up(
        &self,
        identity: Option<&AuthIdentity>,
        bearer: Option<&str>,
        client: &str,
    ) -> Result<Value, Refusal> {
        let (token, person) = step_up_person(identity, bearer)?;
        let c = self
            .core
            .step_up_start(token, Some(&ClientKey::of_peer(client)))
            .map_err(said)?;
        let allow: Vec<Value> = self
            .core
            .host()
            .store()
            .credentials_of(person)
            .unwrap_or_default()
            .iter()
            .map(|c| json!({ "type": "public-key", "id": B64.encode(&c.id) }))
            .collect();
        Ok(json!({
            "ceremony_id": "step-up",
            "publicKey": { "challenge": c.challenge, "rpId": self.rp_id, "timeout": CEREMONY_TIMEOUT_MS, "userVerification": "required", "allowCredentials": allow },
        }))
    }

    /// `POST /-/auth/passkey/step-up/finish`: the session is stepped up in place.
    pub fn close_step_up(
        &self,
        body: &Value,
        identity: Option<&AuthIdentity>,
        bearer: Option<&str>,
    ) -> Result<Value, Refusal> {
        let (token, _) = step_up_person(identity, bearer)?;
        let mut a = self.assertion(body)?;
        a.ceremony_id = String::new();
        // A touch that is not the session's own key is a refusal of the person, not of the wire.
        let view = self.core.step_up_finish(token, a).map_err(|r| {
            if r.code == Code::Ceremony {
                (403, r.detail)
            } else {
                said(r)
            }
        })?;
        Ok(
            json!({ "signed_in": true, "subject": view.user_id, "display": view.display, "role": view.roles.first().cloned().unwrap_or_default(), "stepped_up": view.stepped_up, "expires_at": view.expires_at }),
        )
    }

    /// Is `bearer` a PERSON's core session that is stepped up? A machine credential is not a
    /// person, so it never is (the stricter cousin of [`PasskeyDoor::caller_stepped_up`]).
    pub fn person_stepped_up(&self, bearer: Option<&str>) -> bool {
        bearer
            .filter(|b| crate::identity_host::is_session_token(b))
            .and_then(|b| self.core.session(b).ok())
            .is_some_and(|v| v.stepped_up)
    }

    /// Is this caller stepped up? A core session must be; a machine credential is exempt.
    pub fn caller_stepped_up(&self, identity: &AuthIdentity, bearer: Option<&str>) -> bool {
        if !bearer.is_some_and(crate::identity_host::is_session_token) {
            return identity.method != "passkey";
        }
        bearer
            .and_then(|b| self.core.session(b).ok())
            .is_some_and(|v| v.stepped_up)
    }

    // ── Enrolment ────────────────────────────────────────────────────────────

    /// `POST /-/auth/passkey/register/start`: an invite (the core's, a first-key one, or the
    /// shop's fed one), a paid sign-up, or an admin's word (the break-glass bearer for the FIRST
    /// admin; an admin's session for everybody else, by invite) — redeemed by the core.
    pub fn open_enrolment(
        &self,
        body: &Value,
        identity: Option<&AuthIdentity>,
        bearer: Option<&str>,
        auth: &AuthConfig,
        client: &str,
    ) -> Result<Value, Refusal> {
        let label = str_field(body, "label").unwrap_or("passkey").to_owned();
        let display = str_field(body, "display").unwrap_or_default().to_owned();
        if !display.is_empty() {
            refuse_reserved(auth, &display)?;
        }
        let mut note = Note {
            label,
            opened: now_secs(),
            ..Note::default()
        };
        // The room is checked before anything is minted: a full door mints nothing.
        {
            let mut notes = self.notes.lock_or_recover();
            prune_notes(&mut notes, now_secs());
            if notes.len() >= MAX_NOTES {
                return Err(refuse(503, ENROLMENTS_FULL));
            }
        }
        let secret = match (str_field(body, "invite"), str_field(body, "signup")) {
            (Some(secret), _) => {
                self.take_in_fed(secret, auth)?;
                let invite = self.open_invite(secret).ok_or_else(|| {
                    refuse(401, "this invite is not valid any more; ask for a new one")
                })?;
                // THE INVITEE'S CHOICE OF NAME is theirs to choose, never to take.
                if invite.display.is_empty() && !display.is_empty() && self.name_taken(&display) {
                    return Err(refuse(409, format!("{display:?} is already somebody's name here, so an invite cannot be redeemed under it; choose another, or ask for an invite pinned to it if it is yours")));
                }
                if invite.created_by.starts_with(FIRST_KEY_BY) && self.has_admin_passkey(auth) {
                    self.withdraw_invite(&invite.id).ok();
                    return Err(refuse(409, FIRST_KEY_ENROLLED));
                }
                note.buyer =
                    invite.created_by == FIRST_ADMIN_BY || invite.created_by == SIGN_IN_AGAIN_BY;
                note.invite = Some(invite.id);
                secret.to_owned()
            }
            (None, Some(signup)) => {
                let repo = self
                    .unredeemed_signup(signup)
                    .ok_or_else(|| refuse(401, SIGNUP_REFUSED))?;
                if display.is_empty() {
                    return Err(refuse(
                        400,
                        "a sign-up lets you choose your name: `display` is required",
                    ));
                }
                if self.name_taken(&display) {
                    return Err(refuse(
                        409,
                        format!("{display:?} is already somebody's name here; choose another"),
                    ));
                }
                // No group: what this person has here is the repository the sign-up paid for.
                let (secret, invite) = self.seed(
                    &display,
                    Vec::new(),
                    &format!("{SIGNUP_BY}{repo}"),
                    nornir_identity_session::invites::INVITE_TTL_SECS,
                    None,
                )?;
                note.bind = Some(repo);
                note.invite = Some(invite.id);
                secret
            }
            (None, None) => {
                let id = identity
                    .ok_or_else(|| refuse(401, "enrolling a passkey requires authority"))?;
                if !auth.role_for_identity(id).can_admin() {
                    return Err(refuse(
                        403,
                        "enrolling a passkey requires an admin identity",
                    ));
                }
                if display.is_empty() {
                    return Err(refuse(400, "`display` is required"));
                }
                let mut groups = groups_field(body);
                if let Some(role) = str_field(body, "role") {
                    groups.push(group_for_role(auth, role)?);
                }
                if let Some(duty) = str_field(body, "duty") {
                    groups.push(group_for_duty(auth, duty)?);
                }
                let session = bearer.filter(|b| crate::identity_host::is_session_token(b));
                let secret = match session {
                    // An admin's session: an invite of the core's (never an admin), redeemed here.
                    Some(token) => {
                        let ask = InviteAsk {
                            display: display.clone(),
                            email: None,
                            groups,
                            ttl_secs: None,
                            person: None,
                        };
                        self.core.invite(token, ask).map_err(said)?.secret
                    }
                    // A machine credential (the break-glass bearer, an mTLS cert): the FIRST admin
                    // only, and it retires the moment one exists.
                    None => {
                        if self.has_admin_passkey(auth)
                            && (self.bootstrap_until_first_admin || id.method != "passkey")
                        {
                            return Err(refuse(403, "this server already has an admin passkey, so the bootstrap credential no longer enrols passkeys — sign in with an admin passkey, or use an invite"));
                        }
                        let invited = self
                            .core
                            .first_admin_invite(groups, &id.audit_name())
                            .map_err(said)?;
                        self.pin_display(&invited.invite_id, &display)?;
                        invited.secret
                    }
                };
                note.for_another = session.is_some();
                note.invite = Some(nornir_identity_session::invites::hash_secret(&secret));
                secret
            }
        };
        let c = self
            .core
            .redeem_start(&secret, &display, &self.relay(client))
            .map_err(said)?;
        {
            let mut notes = self.notes.lock_or_recover();
            prune_notes(&mut notes, now_secs());
            if notes.len() >= MAX_NOTES {
                return Err(refuse(503, ENROLMENTS_FULL));
            }
            notes.insert(c.id.clone(), note);
        }
        Ok(json!({
            "ceremony_id": c.id,
            "publicKey": {
                "challenge": c.challenge,
                "rp": { "id": c.rp_id, "name": self.rp_name },
                "user": { "id": B64.encode(c.user_id.as_bytes()), "name": c.user_name, "displayName": c.display },
                "pubKeyCredParams": [ { "type": "public-key", "alg": -7 } ],
                "timeout": CEREMONY_TIMEOUT_MS,
                "authenticatorSelection": { "userVerification": "preferred", "residentKey": "preferred" },
                "attestation": "none",
                "excludeCredentials": [],
            }
        }))
    }

    /// `POST /-/auth/passkey/register/finish`: the core redeems; a sign-up's repository is bound,
    /// the shop's buyer owns the order's repositories.
    pub fn close_enrolment(&self, body: &Value, auth: &AuthConfig) -> Result<Value, Refusal> {
        let ceremony_id = str_field(body, "ceremony_id")
            .ok_or_else(|| refuse(400, "`ceremony_id` is required"))?;
        // A finish whose note is gone is refused by name, never finished under a default note (a
        // paid sign-up would register with its repository unbound, an admin's enrolment for
        // somebody else would hand the admin that person's session).
        let mut note = self
            .notes
            .lock_or_recover()
            .remove(ceremony_id)
            .ok_or_else(|| refuse(409, ENROLMENT_UNKNOWN))?;
        // What the invite itself says wins over the note: a sign-up's repository is in its
        // invite (`signup:<repo>`), the shop's buyer is its invite's maker.
        if let Some(invite) = note.invite.as_ref().and_then(|id| self.invite_by_id(id)) {
            if let Some(repo) = invite
                .created_by
                .strip_prefix(SIGNUP_BY)
                .filter(|r| !r.is_empty())
            {
                note.bind = Some(repo.to_owned());
            }
            note.buyer |=
                invite.created_by == FIRST_ADMIN_BY || invite.created_by == SIGN_IN_AGAIN_BY;
        }
        let response = body
            .get("response")
            .ok_or_else(|| refuse(400, "`response` is required"))?;
        let client_data = b64_field(response, "clientDataJSON")?;
        let attestation = b64_field(response, "attestationObject")?;
        let origin = self.origin_of(&client_data);
        let (credential_id, public_key) =
            attested_key(&self.rp_id, &origin, &attestation, &client_data)?;
        let first_key = note.invite.as_ref().is_some_and(|id| {
            self.invite_by_id(id)
                .is_some_and(|i| i.created_by.starts_with(FIRST_KEY_BY))
        });
        if first_key && self.has_admin_passkey(auth) {
            if let Some(id) = &note.invite {
                self.withdraw_invite(id).ok();
            }
            self.forget_first_sign_in();
            return Err(refuse(409, FIRST_KEY_ENROLLED));
        }
        let reg = Registration {
            ceremony_id: ceremony_id.to_owned(),
            credential_id: credential_id.clone(),
            public_key,
            client_data_json: client_data,
            attestation_object: attestation,
            rp_id: self.rp_id.clone(),
            origin,
        };
        let done = self.core.redeem_finish(reg).map_err(said)?;
        if first_key {
            self.forget_first_sign_in();
        }
        let subject = done.session.user_id.clone();
        if let Some(repo) = &note.bind {
            if let Err(e) = self.bind_signup(repo, &subject) {
                let _ = self.core.deactivate(&subject, "the sign-up's binding");
                return Err(e);
            }
            log::warn!(
                "passkey: {} ({subject}) redeemed the sign-up of '{repo}' and owns it",
                done.session.display
            );
        }
        if note.buyer {
            self.buyer_registered(&subject);
        }
        log::info!(
            "passkey: enrolled a passkey for {} ({subject})",
            done.session.display
        );
        let mut out = json!({ "subject": subject, "display": done.session.display, "credential_id": B64.encode(&credential_id), "label": note.label });
        if note.for_another {
            // An admin enrolling a key for somebody else gets no session for them.
            let _ = self.core.logout(&done.token, false);
        } else {
            out["token"] = json!(done.token);
            out["expires_at"] = json!(done.session.expires_at);
        }
        Ok(out)
    }

    // ── Invites ──────────────────────────────────────────────────────────────

    /// Does an active person here go by `display`?
    fn name_taken(&self, display: &str) -> bool {
        self.core
            .host()
            .store()
            .users()
            .unwrap_or_default()
            .iter()
            .any(|u| u.state != UserState::Deactivated && u.display == display)
    }

    fn open_invite(&self, secret: &str) -> Option<Invite> {
        let id = nornir_identity_session::invites::hash_secret(secret.trim());
        let now = now_secs();
        self.core
            .host()
            .store()
            .invites()
            .ok()?
            .into_iter()
            .find(|i| i.id == id && i.open(now))
    }

    fn invite_by_id(&self, id: &str) -> Option<Invite> {
        self.core
            .host()
            .store()
            .invites()
            .ok()?
            .into_iter()
            .find(|i| i.id == id)
    }

    fn pin_display(&self, id: &str, display: &str) -> Result<(), Refusal> {
        let display = display.to_owned();
        self.core
            .host()
            .store()
            .update_invites(&mut |all| {
                if let Some(i) = all.iter_mut().find(|i| i.id == id) {
                    i.display = display.clone();
                }
            })
            .map_err(|e| refuse(500, e.0))
    }

    /// **An invite of this box's own making** (where a person comes from: a sign-up, the
    /// shop's buyer, the first key): minted by the shared rule (`nornir_identity_session`, never
    /// an admin unless `admin_ok`), kept in the core's store, redeemed by the core. `secret`:
    /// the secret the invite answers to when it is a fed one (its holder's), else a fresh one.
    fn seed(
        &self,
        display: &str,
        groups: Vec<String>,
        by: &str,
        ttl_secs: i64,
        secret: Option<&str>,
    ) -> Result<(String, Invite), Refusal> {
        let now = now_secs();
        let ask = nornir_identity_session::invites::Ask {
            display: display.to_owned(),
            groups,
            by: by.to_owned(),
            ttl_secs,
        };
        let (fresh, mut invite) = nornir_identity_session::invites::mint(ask, now, |_| false)
            .map_err(|e| refuse(400, e.to_string()))?;
        let secret = match secret {
            Some(s) => {
                invite.id = nornir_identity_session::invites::hash_secret(s.trim());
                s.trim().to_owned()
            }
            None => fresh,
        };
        let kept = invite.clone();
        self.core
            .host()
            .store()
            .update_invites(&mut |all| {
                all.retain(|i| i.id != kept.id);
                all.push(kept.clone());
                nornir_identity_session::invites::trim(
                    all,
                    nornir_identity_session::invites::MAX_INVITES,
                    now,
                );
            })
            .map_err(|e| refuse(500, e.0))?;
        self.core.host().audit(Event::InviteMinted {
            invite: invite.id.clone(),
            by: by.to_owned(),
            mailed: false,
        });
        Ok((secret, invite))
    }

    /// `POST /-/auth/passkey/invites`: an admin's invite (never an admin). A person's session is
    /// the core's invite; a machine admin's (the console, the CLI) is the same rule, here.
    pub fn mint_invite_as(
        &self,
        body: &Value,
        by: &str,
        bearer: Option<&str>,
        auth: &AuthConfig,
    ) -> Result<Value, Refusal> {
        let display = str_field(body, "display").unwrap_or_default().to_owned();
        if !display.is_empty() {
            refuse_reserved(auth, &display)?;
        }
        let mut groups = groups_field(body);
        if let Some(role) = str_field(body, "role") {
            groups.push(group_for_role(auth, role)?);
        }
        if let Some(duty) = str_field(body, "duty") {
            groups.push(group_for_duty(auth, duty)?);
        }
        let would_be = groups_identity(&groups);
        if auth.role_for_identity(&would_be).can_admin() {
            return Err(said(Refused::new(Code::InviteWouldBeAdmin, "")));
        }
        // HOL-NCSC (NCSC P1): an approver is invited by a PERSON whose session is stepped up,
        // never by a machine credential (the console's token, the CLI's bearer, an mTLS
        // cert): a stolen machine token must not be able to mint the second pair of eyes.
        if auth
            .duties_for_identity(&would_be)
            .contains(&crate::auth::Duty::SecurityApprover)
            && !self.person_stepped_up(bearer)
        {
            return Err(refuse(
                403,
                holger_errcode::AUTH_INVITE_NEEDS_PERSON.tag(
                    "an invite that makes a Security Approver is minted by a person's stepped-up passkey session, \
                     never by a machine token: sign in with an admin passkey, step up, and invite again",
                ),
            ));
        }
        let email = str_field(body, "email").map(str::to_owned);
        let (secret, id, expires, mailed) =
            match bearer.filter(|b| crate::identity_host::is_session_token(b)) {
                Some(token) => {
                    let i = self
                        .core
                        .invite(
                            token,
                            InviteAsk {
                                display: display.clone(),
                                email,
                                groups: groups.clone(),
                                ttl_secs: None,
                                person: None,
                            },
                        )
                        .map_err(said)?;
                    (i.secret, i.invite_id, i.expires_at, i.mailed)
                }
                None => {
                    let (secret, i) = self.seed(
                        &display,
                        groups.clone(),
                        by,
                        nornir_identity_session::invites::INVITE_TTL_SECS,
                        None,
                    )?;
                    (secret, i.id, i.expires, false)
                }
            };
        let role = auth.role_for_identity(&would_be);
        log::info!(
            "passkey: {by} minted an invite for {} ({})",
            if display.is_empty() {
                "(their choice of name)"
            } else {
                display.as_str()
            },
            role_word(role)
        );
        Ok(json!({
            "invite": secret,
            "id": id,
            "expires": expires,
            "display": display,
            "groups": groups,
            "role": role_word(role),
            "duties": auth.duties_for_identity(&would_be).into_iter().map(|d| d.name()).collect::<Vec<_>>(),
            "mailed": mailed,
        }))
    }

    /// `POST /-/auth/passkey/invites/list`: every invite, each with the role its groups resolve
    /// to and whether it is still redeemable. Secrets are not part of the record.
    pub fn invites_json(&self, auth: &AuthConfig) -> Value {
        let now = now_secs();
        let rows: Vec<Value> = self
            .core
            .host()
            .store()
            .invites()
            .unwrap_or_default()
            .into_iter()
            .map(|i| {
                let role = auth.role_for_identity(&groups_identity(&i.groups));
                let mut v = serde_json::to_value(&i).unwrap_or(Value::Null);
                v["role"] = json!(role_word(role));
                v["open"] = json!(i.open(now));
                v
            })
            .collect();
        json!({ "invites": rows })
    }

    /// `POST /-/auth/passkey/invites/revoke`: an invite withdrawn by its id.
    pub fn withdraw_invite(&self, id: &str) -> Result<Value, Refusal> {
        let mut found = false;
        self.core
            .host()
            .store()
            .update_invites(&mut |all| {
                if let Some(i) = all.iter_mut().find(|i| i.id == id) {
                    i.revoked = true;
                    found = true;
                }
            })
            .map_err(|e| refuse(500, e.0))?;
        if found {
            Ok(json!({ "revoked": id }))
        } else {
            Err(refuse(404, "no such invite"))
        }
    }

    // ── Sessions ─────────────────────────────────────────────────────────────

    /// `POST /-/auth/passkey/session`: who the caller is, for a console to render.
    pub fn session_json(
        &self,
        identity: &AuthIdentity,
        bearer: Option<&str>,
        auth: &AuthConfig,
    ) -> Value {
        let view = bearer
            .filter(|b| crate::identity_host::is_session_token(b))
            .and_then(|b| self.core.session(b).ok());
        json!({
            "subject": identity.subject,
            "display": identity.display.clone().unwrap_or_else(|| identity.subject.clone()),
            "method": identity.method,
            "groups": identity.groups,
            "role": role_word(auth.role_for_identity(identity)),
            "uv": view.as_ref().is_some_and(|v| v.stepped_up),
            "expires_at": view.as_ref().map(|v| v.expires_at),
            // A machine credential is the operator's own possession and has no session to step up.
            "stepped_up": view.as_ref().map_or(identity.method != "passkey", |v| v.stepped_up),
        })
    }

    /// `POST /-/auth/passkey/logout`: every session of the caller's person ends.
    pub fn logout(&self, bearer: &str) -> Result<Value, Refusal> {
        self.core.logout(bearer, true).map_err(said)?;
        Ok(json!({ "ok": true }))
    }

    /// `POST /-/auth/passkey/sessions/revoke-all`: every session on this server ends.
    pub fn revoke_all_sessions(&self) -> Result<Value, Refusal> {
        let now = now_secs();
        self.core
            .host()
            .store()
            .update_revocations(&mut |r| r.revoke_all(now))
            .map_err(|e| refuse(500, e.0))?;
        Ok(json!({ "ok": true, "floor": now }))
    }

    // ── People ───────────────────────────────────────────────────────────────

    /// `POST /-/auth/passkey/credentials/list`: every passkey, with its person and their role.
    pub fn credentials_json(&self, auth: &AuthConfig) -> Value {
        let store = self.core.host().store();
        let users = store.users().unwrap_or_default();
        let mut rows = Vec::new();
        let mut admins = 0;
        for u in &users {
            let role = auth.role_for_identity(&identity_of(u));
            if u.state == UserState::Active && role.can_admin() {
                admins += 1;
            }
            for c in store.credentials_of(&u.id).unwrap_or_default() {
                rows.push(json!({
                    "id": B64.encode(&c.id),
                    "subject": u.id,
                    "display": u.display,
                    "groups": u.groups,
                    "role": role_word(role),
                    "created": c.created,
                    "disabled": u.state != UserState::Active,
                }));
            }
        }
        json!({ "credentials": rows, "enabled_admins": admins })
    }

    /// `POST /-/auth/passkey/credentials/revoke`: the passkey's person is deactivated (every
    /// session ends) — never the last admin.
    pub fn revoke_credential(
        &self,
        id: &str,
        by: &str,
        auth: &AuthConfig,
    ) -> Result<Value, Refusal> {
        let raw = B64
            .decode(id)
            .map_err(|_| refuse(404, "no such credential"))?;
        let store = self.core.host().store();
        let c = store
            .credential(&raw)
            .map_err(|e| refuse(500, e.0))?
            .ok_or_else(|| refuse(404, "no such credential"))?;
        let user = store
            .user(&c.user)
            .map_err(|e| refuse(500, e.0))?
            .ok_or_else(|| refuse(404, "no such credential"))?;
        if user.state != UserState::Active {
            return Err(refuse(409, "this credential is already revoked"));
        }
        let admin = |u: &nornir_identity::User| {
            u.state == UserState::Active && auth.role_for_identity(&identity_of(u)).can_admin()
        };
        if admin(&user)
            && store
                .users()
                .unwrap_or_default()
                .iter()
                .filter(|u| admin(u))
                .count()
                <= 1
        {
            return Err(refuse(
                409,
                "this is the last admin passkey; register another before revoking it",
            ));
        }
        self.core.deactivate(&user.id, by).map_err(said)?;
        log::warn!(
            "passkey: {} ({}) deactivated by {by}, and every session of theirs ended",
            user.display,
            user.id
        );
        Ok(json!({ "ok": true, "subject": user.id, "display": user.display }))
    }

    // ── The shop's buyer, the first key, a paid sign-up ─────────────────────

    fn marker(&self) -> PathBuf {
        self.root().join(FIRST_ADMIN_FILE)
    }

    fn read_marker(&self) -> Option<FirstAdminSeeded> {
        std::fs::read(self.marker())
            .ok()
            .and_then(|b| serde_json::from_slice(&b).ok())
    }

    fn write_marker(&self, m: &FirstAdminSeeded) -> Result<(), Refusal> {
        let marker = self.marker();
        if let Some(dir) = marker.parent() {
            std::fs::create_dir_all(dir).map_err(|e| {
                refuse(
                    500,
                    format!("cannot make the first-login record's directory: {e}"),
                )
            })?;
        }
        let bytes = serde_json::to_vec_pretty(m)
            .map_err(|e| refuse(500, format!("cannot serialize the first-login record: {e}")))?;
        let tmp = marker.with_extension("tmp");
        std::fs::write(&tmp, bytes)
            .and_then(|()| std::fs::rename(&tmp, &marker))
            .map_err(|e| refuse(500, format!("cannot write the first-login record: {e}")))
    }

    /// **The shop's first-login invite, taken in once** (lane P1): the fed SHA-256 of the
    /// buyer's secret, pinned to the buyer, single use, its life clamped; not kept when an admin
    /// exists already. The box never sees the secret: the buyer's link brings it, and the first
    /// time it does it becomes an invite of the core's ([`Self::open_enrolment`]).
    pub fn seed_first_admin(
        &self,
        fed: &FirstAdminInvite,
        auth: &AuthConfig,
    ) -> Result<String, Refusal> {
        let hex = fed.sha256_hex.trim().to_ascii_lowercase();
        if hex.len() != 64 || !hex.bytes().all(|b| b.is_ascii_hexdigit()) {
            return Err(refuse(
                400,
                "the fed first-login invite is not a SHA-256 (64 hex characters)",
            ));
        }
        let display = fed.display.trim().to_owned();
        if display.is_empty() {
            return Err(refuse(
                400,
                "the fed first-login invite names no first owner (`operator_name`)",
            ));
        }
        refuse_reserved(auth, &display)?;
        let groups = first_admin_groups(auth)?;
        let _held = self.marker_lock.lock_or_recover();
        let ttl = fed.ttl_secs.clamp(60, FIRST_ADMIN_TTL_MAX);
        if self.marker().exists() {
            // HOL-FIRST-ADMIN: a record taken in before the identity move names nobody (the
            // account lived in the old store's invites, which the move did not carry over), so
            // the shop's `sign_in_again` for its first admin was refused. The shop's feed names
            // them: the record takes that name (and the life it was fed), nothing else of it
            // changes. A record that names somebody is never renamed.
            if let Some(mut m) = self.read_marker().filter(|m| m.display.trim().is_empty()) {
                m.display = display.clone();
                if m.ttl_secs <= 0 {
                    m.ttl_secs = ttl;
                }
                self.write_marker(&m)?;
                return Ok(format!("taken in before: not opened again; the record names the shop's first admin {display} now"));
            }
            return Ok("taken in before: not opened again".into());
        }
        let mut m = FirstAdminSeeded {
            sha256: hex,
            at: now_secs(),
            seeded: false,
            display: display.clone(),
            ttl_secs: ttl,
            used_by: None,
        };
        if self.has_admin_passkey(auth) {
            self.write_marker(&m)?;
            return Ok(
                "this server already has an admin passkey: no first-login invite is kept".into(),
            );
        }
        m.seeded = true;
        self.write_marker(&m)?;
        Ok(format!(
            "kept for {display} for {} h, single use: it enrols the estate's first admin ({})",
            ttl / 3600,
            groups.join(", ")
        ))
    }

    /// The fed first-login invite becomes the core's the first time its secret is presented.
    fn take_in_fed(&self, secret: &str, auth: &AuthConfig) -> Result<(), Refusal> {
        let _held = self.marker_lock.lock_or_recover();
        let Some(m) = self.read_marker().filter(|m| m.seeded) else {
            return Ok(());
        };
        if m.sha256 != sha256_hex(secret)
            || self.open_invite(secret).is_some()
            || self
                .invite_by_id(&nornir_identity_session::invites::hash_secret(
                    secret.trim(),
                ))
                .is_some()
        {
            return Ok(());
        }
        let left = (m.at + m.ttl_secs) - now_secs();
        if left <= 0 {
            return Ok(());
        }
        self.seed(
            &m.display,
            first_admin_groups(auth)?,
            FIRST_ADMIN_BY,
            left,
            Some(secret),
        )
        .map(|_| ())
    }

    /// **The invite that enrols the box's FIRST admin key** (`holger.doors.v1.FirstKeyService`):
    /// the core's first-admin invite, refused (409) once an admin exists, one open at a time.
    pub fn mint_first_key_invite(
        &self,
        display: &str,
        by: &str,
        auth: &AuthConfig,
    ) -> Result<(String, Invite), Refusal> {
        let display = display.trim().to_owned();
        if !display.is_empty() {
            refuse_reserved(auth, &display)?;
        }
        if self.has_admin_passkey(auth) {
            return Err(refuse(409, FIRST_KEY_ENROLLED));
        }
        let groups = if auth.duties_enabled() {
            first_admin_groups(auth)?
                .into_iter()
                .filter(|g| g != crate::auth::ESTATE_OWNERS)
                .collect()
        } else {
            vec![group_for_role(auth, "admin")?]
        };
        let now = now_secs();
        self.core
            .host()
            .store()
            .update_invites(&mut |all| {
                for open in all
                    .iter_mut()
                    .filter(|i| i.created_by.starts_with(FIRST_KEY_BY) && i.open(now))
                {
                    open.revoked = true;
                }
            })
            .map_err(|e| refuse(500, e.0))?;
        let invited = self
            .core
            .first_admin_invite(groups, &format!("{FIRST_KEY_BY} ({by})"))
            .map_err(said)?;
        if !display.is_empty() {
            self.pin_display(&invited.invite_id, &display)?;
        }
        let invite = self
            .invite_by_id(&invited.invite_id)
            .ok_or_else(|| refuse(500, "the first-key invite was not kept"))?;
        log::info!(
            "passkey: {by} minted the first-key invite ({})",
            invite.groups.join(", ")
        );
        Ok((invited.secret, invite))
    }

    /// **A resend sign-in link for the estate's buyer** (A16): a fresh single-use invite pinned
    /// to `account` with the first admin's groups, only for the account the shop named (404) and
    /// only while it holds no passkey (409). `(secret, id)`.
    pub fn mint_sign_in_again(
        &self,
        account: &str,
        ttl_secs: u64,
        auth: &AuthConfig,
    ) -> Result<(String, String), Refusal> {
        let account = account.trim();
        if !self.read_marker().is_some_and(|m| m.display == account) {
            return Err(refuse(
                404,
                format!(
                    "{account} is no account the shop named on this box: no sign-in link minted"
                ),
            ));
        }
        let held = self
            .core
            .host()
            .store()
            .users()
            .unwrap_or_default()
            .into_iter()
            .filter(|u| u.state == UserState::Active && u.display == account)
            .count();
        if held > 0 {
            return Err(refuse(409, format!("{account} already signs in with a passkey ({held} held): a sign-in link only enrols the first one — sign in, or add a passkey from a signed-in device")));
        }
        let ttl = i64::try_from(ttl_secs)
            .unwrap_or(FIRST_ADMIN_TTL_MAX)
            .clamp(60, FIRST_ADMIN_TTL_MAX);
        let (secret, invite) = self.seed(
            account,
            first_admin_groups(auth)?,
            SIGN_IN_AGAIN_BY,
            ttl,
            None,
        )?;
        log::warn!("passkey: a resend sign-in link was minted for {account} (monetize's sign_in_again), {ttl} s, single use");
        Ok((secret, invite.id))
    }

    /// **The repository a paid sign-up made that nobody has redeemed yet** (lane P1).
    fn unredeemed_signup(&self, signup: &str) -> Option<String> {
        let signup = signup.trim();
        if signup.len() < 8 {
            return None;
        }
        crate::sold::read(&self.data_root)
            .into_iter()
            .find(|(_, s)| {
                !s.signup.is_empty()
                    && s.owner.is_empty()
                    && (s.signup == signup || s.signup.ends_with(&format!(":{signup}")))
            })
            .map(|(name, _)| name)
    }

    /// **Bind `repo` to `subject`** in the sold record and on the gate.
    fn bind_signup(&self, repo: &str, subject: &str) -> Result<(), Refusal> {
        let root = self.data_root.clone();
        let entry = crate::sold::update(&root, |made| {
            let s = made
                .get_mut(repo)
                .filter(|s| s.owner.is_empty())
                .ok_or_else(|| SIGNUP_REFUSED.to_owned())?;
            s.owner = subject.to_owned();
            Ok(crate::sold::gate_entry(&root, s))
        })
        .map_err(|e| {
            if e == SIGNUP_REFUSED {
                refuse(401, SIGNUP_REFUSED)
            } else {
                refuse(500, e)
            }
        })?;
        if let Some(g) = self.gate.get() {
            g.own_entry(repo, entry);
        }
        Ok(())
    }

    /// **The shop's buyer registered** (lane P1): the first-login record names them, and every
    /// repository the paid order made for the estate's owners is theirs.
    fn buyer_registered(&self, subject: &str) {
        {
            let _held = self.marker_lock.lock_or_recover();
            let mut m = self.read_marker().unwrap_or_default();
            m.used_by = Some(subject.to_owned());
            if let Err((_, e)) = self.write_marker(&m) {
                log::error!("passkey: the first owner {subject} registered, and the first-login record could not say so: {e}");
            }
        }
        match crate::sold::bind_buyer(&self.data_root, subject) {
            Ok(bound) if bound.is_empty() => {}
            Ok(bound) => {
                let made = crate::sold::read(&self.data_root);
                if let Some(g) = self.gate.get() {
                    for name in &bound {
                        if let Some(s) = made.get(name) {
                            g.own_entry(name, crate::sold::gate_entry(&self.data_root, s));
                        }
                    }
                }
                log::warn!("passkey: the first owner {subject} registered and owns {}", bound.join(", "))
            }
            Err(e) => log::error!("passkey: the first owner {subject} registered, and the sign-up record could not name them its owner: {e}"),
        }
    }
}

/// holger's role name for a console's role word (`reader` → `Reader`), as the login answer
/// always said it.
fn role_name(word: &str) -> String {
    let mut c = word.chars();
    c.next()
        .map(|f| f.to_ascii_uppercase().to_string() + c.as_str())
        .unwrap_or_default()
}

/// The session token and person of a step-up: a person signed in with the core's session.
fn step_up_person<'a>(
    identity: Option<&'a AuthIdentity>,
    bearer: Option<&'a str>,
) -> Result<(&'a str, &'a str), Refusal> {
    match (identity, bearer) {
        (Some(id), Some(b))
            if id.method == "passkey" && crate::identity_host::is_session_token(b) =>
        {
            Ok((b, id.subject.as_str()))
        }
        (Some(_), _) => Err(refuse(
            400,
            "a step-up is a signed-in person's touch; this credential has none to refresh",
        )),
        (None, _) => Err(refuse(401, "log in first")),
    }
}

/// The credential id and the SEC1 public key a registration's attestation carries (read by
/// nornir-auth-webauthn's parser; the core verifies the whole answer again).
fn attested_key(
    rp_id: &str,
    origin: &str,
    attestation: &[u8],
    client_data: &[u8],
) -> Result<(Vec<u8>, Vec<u8>), Refusal> {
    let bad = || refuse(400, "that authenticator's response could not be verified");
    let challenge = nornir_auth_webauthn::ceremony::client_data(client_data)
        .map_err(|_| bad())?
        .challenge;
    let rp = nornir_auth_webauthn::pure::PureRelyingParty::new(rp_id, origin, "holger")
        .map_err(|_| bad())?;
    let cred = nornir_auth_webauthn::pure::verify_registration(
        &rp,
        attestation,
        client_data,
        &B64.encode(&challenge),
        false,
    )
    .map_err(|e| {
        log::warn!("passkey: a registration was refused: {e}");
        bad()
    })?;
    let id = B64.decode(&cred.id).map_err(|_| bad())?;
    let mut key = Vec::with_capacity(65);
    key.push(0x04);
    key.extend_from_slice(&cred.x);
    key.extend_from_slice(&cred.y);
    Ok((id, key))
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::audit::MemoryAuditLog;
    use nornir_auth_webauthn::softkey::SoftKey;

    const RP: &str = "holger.lab.internal";
    const ORIGIN: &str = "https://holger.lab.internal";

    fn auth() -> AuthConfig {
        let mut a = AuthConfig {
            methods: vec![
                crate::auth::AuthMethodConfig::StaticToken {
                    subject: "console".into(),
                    token: Some("c".repeat(40)),
                    token_env: None,
                    token_file: None,
                },
                crate::auth::AuthMethodConfig::Passkey {
                    rp_id: RP.into(),
                    rp_origin: ORIGIN.into(),
                    rp_name: "holger".into(),
                    session_key_file: None,
                    bootstrap_until_first_admin: true,
                },
            ],
            roles: HashMap::from([("console".to_string(), crate::auth::Role::Admin)]),
            group_roles: HashMap::from([
                ("holger-admins".to_string(), crate::auth::Role::Admin),
                ("holger-writers".to_string(), crate::auth::Role::Writer),
            ]),
            ..Default::default()
        };
        assert!(!crate::auth::estate_duties(&mut a).is_empty());
        a
    }

    fn door() -> (SharedPasskeyDoor, AuthConfig, tempfile::TempDir) {
        let dir = tempfile::tempdir().unwrap();
        let a = auth();
        let d = PasskeyDoor::open(dir.path(), &a, Arc::new(MemoryAuditLog::new()))
            .unwrap()
            .expect("a passkey method opens a door");
        (d, a, dir)
    }

    fn console() -> AuthIdentity {
        AuthIdentity {
            subject: "console".into(),
            method: "static_token".into(),
            groups: vec![],
            display: None,
        }
    }

    /// One enrolment through the door's JSON: start, the soft key, finish.
    fn enrol(
        d: &PasskeyDoor,
        a: &AuthConfig,
        key: &mut SoftKey,
        ask: Value,
        id: Option<&AuthIdentity>,
        bearer: Option<&str>,
    ) -> Result<Value, Refusal> {
        let start = d.open_enrolment(&ask, id, bearer, a, "203.0.113.1")?;
        let challenge = start["publicKey"]["challenge"].as_str().unwrap().to_owned();
        let (cd, att) = key.register(RP, ORIGIN, &challenge, true);
        d.close_enrolment(&json!({ "ceremony_id": start["ceremony_id"], "id": key.credential_id_b64(), "response": { "clientDataJSON": cd, "attestationObject": att } }), a)
    }

    fn login(d: &PasskeyDoor, key: &mut SoftKey, uv: bool) -> Result<Value, Refusal> {
        let start = d.open_login(&json!({}), "203.0.113.1")?;
        let challenge = start["publicKey"]["challenge"].as_str().unwrap().to_owned();
        let (cd, ad, sig) = key.authenticate(RP, ORIGIN, &challenge, uv);
        d.close_login(&json!({ "ceremony_id": start["ceremony_id"], "id": key.credential_id_b64(), "response": { "clientDataJSON": cd, "authenticatorData": ad, "signature": sig } }))
    }

    /// ★ The break-glass bearer enrols the FIRST admin through the core, and then retires; the
    /// admin's own session invites everybody else (never an admin); a login is the core's session.
    #[test]
    fn the_break_glass_enrols_the_first_admin_once_and_the_admin_invites_the_rest() {
        let (d, a, _dir) = door();
        let mut owner = SoftKey::new(1, &[1u8; 16]);
        let done = enrol(
            &d,
            &a,
            &mut owner,
            json!({ "display": "rickard", "role": "admin" }),
            Some(&console()),
            Some(&"c".repeat(40)),
        )
        .expect("the first admin");
        assert!(
            done["token"]
                .as_str()
                .is_some_and(crate::identity_host::is_session_token),
            "the core's session: {done}"
        );
        assert!(d.has_admin_passkey(&a));
        let mut late = SoftKey::new(2, &[2u8; 16]);
        let again = enrol(
            &d,
            &a,
            &mut late,
            json!({ "display": "eve", "role": "admin" }),
            Some(&console()),
            Some(&"c".repeat(40)),
        )
        .unwrap_err();
        assert_eq!(again.0, 403, "the break-glass retired: {again:?}");
        let session = login(&d, &mut owner, true).expect("the admin logs in")["token"]
            .as_str()
            .unwrap()
            .to_owned();
        let me = d.live_identity(&session).expect("a live session");
        assert!(a.role_for_identity(&me).can_admin());
        let admin_invite = d
            .mint_invite_as(&json!({ "role": "admin" }), "rickard", Some(&session), &a)
            .unwrap_err();
        assert_eq!(admin_invite.0, 403, "an invite never makes an admin");
        let invite = d
            .mint_invite_as(&json!({ "role": "writer" }), "rickard", Some(&session), &a)
            .unwrap();
        let mut vera = SoftKey::new(3, &[3u8; 16]);
        let v = enrol(
            &d,
            &a,
            &mut vera,
            json!({ "invite": invite["invite"], "display": "vera" }),
            None,
            None,
        )
        .expect("vera redeems");
        assert_eq!(v["display"], "vera");
        let reused = enrol(
            &d,
            &a,
            &mut SoftKey::new(4, &[4u8; 16]),
            json!({ "invite": invite["invite"], "display": "eve" }),
            None,
            None,
        )
        .unwrap_err();
        assert_eq!(reused.0, 401, "an invite works once");
        d.logout(v["token"].as_str().unwrap()).unwrap();
        assert!(
            d.live_identity(v["token"].as_str().unwrap()).is_none(),
            "logout ends the session"
        );
    }

    /// ★ HOL-NCSC (NCSC P1): **an invite that makes an approver needs a person's stepped-up
    /// session.** The console's machine token still invites readers, writers and importers; an
    /// approver invite from it is refused by name (`holger-auth-6`), and so is one from a person
    /// whose session is not stepped up. An admin is never invited, by anyone.
    #[test]
    fn an_approver_invite_needs_a_stepped_up_person_never_the_machine_token() {
        let (d, a, _dir) = door();
        let machine = "c".repeat(40);
        let approver_asks = [
            json!({ "duty": "security_approver" }),
            json!({ "groups": [crate::auth::ESTATE_APPROVERS] }),
        ];
        for ask in &approver_asks {
            let e = d
                .mint_invite_as(ask, "console", Some(&machine), &a)
                .unwrap_err();
            assert_eq!(e.0, 403, "{ask}: {e:?}");
            assert!(e.1.contains("[holger-auth-6]"), "{ask}: {e:?}");
            let e = d.mint_invite_as(ask, "console", None, &a).unwrap_err();
            assert!(
                e.1.contains("[holger-auth-6]"),
                "no bearer is no person: {e:?}"
            );
        }
        d.mint_invite_as(&json!({ "role": "writer" }), "console", Some(&machine), &a)
            .expect("the machine token invites a writer");
        d.mint_invite_as(
            &json!({ "duty": "importer" }),
            "console",
            Some(&machine),
            &a,
        )
        .expect("and an importer");
        // A person: first a plain touch (no step-up), then a stepped-up session.
        let mut owner = SoftKey::new(0x61, &[0x61u8; 16]);
        enrol(
            &d,
            &a,
            &mut owner,
            json!({ "display": "rickard", "role": "admin" }),
            Some(&console()),
            Some(&machine),
        )
        .unwrap();
        let plain = login(&d, &mut owner, false).unwrap()["token"]
            .as_str()
            .unwrap()
            .to_owned();
        let e = d
            .mint_invite_as(&approver_asks[0], "rickard", Some(&plain), &a)
            .unwrap_err();
        assert!(
            e.1.contains("[holger-auth-6]"),
            "a session that is not stepped up: {e:?}"
        );
        let me = d.live_identity(&plain).unwrap();
        let start = d
            .open_step_up(Some(&me), Some(&plain), "203.0.113.1")
            .unwrap();
        let challenge = start["publicKey"]["challenge"].as_str().unwrap().to_owned();
        let (cd, ad, sig) = owner.authenticate(RP, ORIGIN, &challenge, true);
        d.close_step_up(&json!({ "id": owner.credential_id_b64(), "response": { "clientDataJSON": cd, "authenticatorData": ad, "signature": sig } }), Some(&me), Some(&plain)).unwrap();
        for ask in &approver_asks {
            let ok = d
                .mint_invite_as(ask, "rickard", Some(&plain), &a)
                .expect("a stepped-up person invites an approver");
            assert!(
                ok["duties"]
                    .as_array()
                    .unwrap()
                    .iter()
                    .any(|d| d == "SecurityApprover"),
                "{ok}"
            );
        }
        assert_eq!(
            d.mint_invite_as(&json!({ "role": "admin" }), "rickard", Some(&plain), &a)
                .unwrap_err()
                .0,
            403,
            "never an admin"
        );
    }

    /// ★ The shop's fed first-login invite: the buyer's secret (its SHA-256 was fed) enrols the
    /// first admin once; taken in again it is never reopened.
    #[test]
    fn the_shops_first_login_invite_enrols_the_buyer_once() {
        let (d, a, _dir) = door();
        let secret = "5".repeat(64);
        let fed = FirstAdminInvite {
            sha256_hex: sha256_hex(&secret),
            ttl_secs: 3600,
            display: "acme".into(),
        };
        assert!(d
            .seed_first_admin(&fed, &a)
            .unwrap()
            .contains("kept for acme"));
        assert!(d
            .seed_first_admin(&fed, &a)
            .unwrap()
            .contains("not opened again"));
        let wrong = enrol(
            &d,
            &a,
            &mut SoftKey::new(5, &[5u8; 16]),
            json!({ "invite": "6".repeat(64) }),
            None,
            None,
        )
        .unwrap_err();
        assert_eq!(wrong.0, 401);
        let done = enrol(
            &d,
            &a,
            &mut SoftKey::new(6, &[6u8; 16]),
            json!({ "invite": secret }),
            None,
            None,
        )
        .expect("the buyer enrols");
        assert_eq!(done["display"], "acme", "pinned to the buyer");
        assert!(d.has_admin_passkey(&a));
        assert_eq!(
            crate::sold::buyer_of(&d.data_root),
            done["subject"].as_str().unwrap()
        );
        assert_eq!(
            enrol(
                &d,
                &a,
                &mut SoftKey::new(7, &[7u8; 16]),
                json!({ "invite": secret }),
                None,
                None
            )
            .unwrap_err()
            .0,
            401,
            "once"
        );
        assert_eq!(
            d.mint_sign_in_again("acme", 86_400, &a).unwrap_err().0,
            409,
            "the buyer holds a passkey already"
        );
        assert_eq!(
            d.mint_sign_in_again("bob", 86_400, &a).unwrap_err().0,
            404,
            "no account the shop named"
        );
    }

    /// A door for the relying party `rp` at the configured origin `origin` (what a paired main
    /// keeps: `rp_origin = https://<rp>`).
    fn door_at(rp: &str, origin: &str) -> (SharedPasskeyDoor, AuthConfig, tempfile::TempDir) {
        let dir = tempfile::tempdir().unwrap();
        let mut a = auth();
        for m in a.methods.iter_mut() {
            if let crate::auth::AuthMethodConfig::Passkey {
                rp_id, rp_origin, ..
            } = m
            {
                *rp_id = rp.into();
                *rp_origin = origin.into();
            }
        }
        let d = PasskeyDoor::open(dir.path(), &a, Arc::new(MemoryAuditLog::new()))
            .unwrap()
            .expect("a passkey method opens a door");
        (d, a, dir)
    }

    fn enrol_at(
        d: &PasskeyDoor,
        a: &AuthConfig,
        key: &mut SoftKey,
        (rp, origin): (&str, &str),
        ask: Value,
        id: Option<&AuthIdentity>,
        bearer: Option<&str>,
    ) -> Result<Value, Refusal> {
        let start = d.open_enrolment(&ask, id, bearer, a, "203.0.113.1")?;
        let challenge = start["publicKey"]["challenge"].as_str().unwrap().to_owned();
        let (cd, att) = key.register(rp, origin, &challenge, true);
        d.close_enrolment(&json!({ "ceremony_id": start["ceremony_id"], "id": key.credential_id_b64(), "response": { "clientDataJSON": cd, "attestationObject": att } }), a)
    }

    fn login_at(
        d: &PasskeyDoor,
        key: &mut SoftKey,
        rp: &str,
        origin: &str,
    ) -> Result<Value, Refusal> {
        let start = d.open_login(&json!({}), "203.0.113.1")?;
        let challenge = start["publicKey"]["challenge"].as_str().unwrap().to_owned();
        let (cd, ad, sig) = key.authenticate(rp, origin, &challenge, true);
        d.close_login(&json!({ "ceremony_id": start["ceremony_id"], "id": key.credential_id_b64(), "response": { "clientDataJSON": cd, "authenticatorData": ad, "signature": sig } }))
    }

    /// ★ WWW-CANON (owner 2026-10-30: the site is www.holger.rs): **a passkey registered with RP
    /// ID holger.rs on the bare host signs in at https://www.holger.rs**, on a main whose kept
    /// config still names the bare origin (no re-pair); a new passkey can be made on www; a
    /// foreign origin is still refused, at login and at enrolment; the door names www as the
    /// site's origin.
    #[test]
    fn a_passkey_of_the_bare_site_signs_in_at_www_and_a_foreign_origin_is_refused() {
        let (d, a, _dir) = door_at("holger.rs", "https://holger.rs");
        let mut owner = SoftKey::new(0x31, &[0x31u8; 16]);
        enrol_at(
            &d,
            &a,
            &mut owner,
            ("holger.rs", "https://holger.rs"),
            json!({ "display": "rickard", "role": "admin" }),
            Some(&console()),
            Some(&"c".repeat(40)),
        )
        .expect("the owner's passkey, made on the bare host");
        let at_www = login_at(&d, &mut owner, "holger.rs", "https://www.holger.rs")
            .expect("the bare site's passkey signs in at www");
        assert!(
            at_www["token"]
                .as_str()
                .is_some_and(crate::identity_host::is_session_token),
            "{at_www}"
        );
        login_at(&d, &mut owner, "holger.rs", "https://holger.rs")
            .expect("the bare origin is kept during the change");
        let evil = login_at(&d, &mut owner, "holger.rs", "https://evil.example").unwrap_err();
        assert!(
            matches!(evil.0, 400 | 401),
            "a foreign origin is refused: {evil:?}"
        );
        let sub = login_at(
            &d,
            &mut owner,
            "holger.rs",
            "https://holger.rs.evil.example",
        )
        .unwrap_err();
        assert!(
            matches!(sub.0, 400 | 401),
            "a look-alike origin is refused: {sub:?}"
        );
        let session = login_at(&d, &mut owner, "holger.rs", "https://www.holger.rs").unwrap()
            ["token"]
            .as_str()
            .unwrap()
            .to_owned();
        let invite = d
            .mint_invite_as(&json!({ "role": "writer" }), "rickard", Some(&session), &a)
            .unwrap();
        let mut vera = SoftKey::new(0x32, &[0x32u8; 16]);
        let bad = enrol_at(
            &d,
            &a,
            &mut vera,
            ("holger.rs", "https://evil.example"),
            json!({ "invite": invite["invite"], "display": "vera" }),
            None,
            None,
        )
        .unwrap_err();
        assert!(
            bad.0 == 401 || bad.0 == 400,
            "an enrolment at a foreign origin is refused: {bad:?}"
        );
        let invite = d
            .mint_invite_as(&json!({ "role": "writer" }), "rickard", Some(&session), &a)
            .unwrap();
        enrol_at(
            &d,
            &a,
            &mut vera,
            ("holger.rs", "https://www.holger.rs"),
            json!({ "invite": invite["invite"], "display": "vera" }),
            None,
            None,
        )
        .expect("a passkey made on www");
        login_at(&d, &mut vera, "holger.rs", "https://www.holger.rs")
            .expect("and it signs in there");
        assert_eq!(
            d.config_json()["rp_origin"],
            "https://www.holger.rs",
            "the site's origin is www"
        );
        assert_eq!(
            d.config_json()["rp_id"],
            "holger.rs",
            "the relying party stays the bare site"
        );
    }

    /// ★ RA5-15: **a paid sign-up's finish never runs without its note.** The door's ceremony
    /// notes (the repository to bind, the buyer, an admin enrolling for another) lived in a map
    /// cleared WHOLE at 1024 open starts, and the finish took `unwrap_or_default()`: the buyer
    /// registered and the paid repository stayed nobody's, with no word. Here the note is lost
    /// (the same core, a door that holds no note of the ceremony): the finish is refused by name,
    /// nobody is registered, and the repository waits for its buyer.
    #[test]
    fn a_paid_sign_ups_finish_without_its_note_is_refused_by_name() {
        let (d, a, _dir) = door();
        crate::sold::update(&d.data_root, |m| {
            m.insert(
                "bolt".into(),
                crate::sold::SoldRepository {
                    template: "raw".into(),
                    signup: "stripe:cs_test_bolt00000001".into(),
                    ..Default::default()
                },
            );
            Ok(())
        })
        .unwrap();
        let mut key = SoftKey::new(0x21, &[0x21u8; 16]);
        let start = d
            .open_enrolment(
                &json!({ "signup": "cs_test_bolt00000001", "display": "bolt" }),
                None,
                None,
                &a,
                "203.0.113.1",
            )
            .expect("the buyer's start");
        let forgot = PasskeyDoor {
            core: Arc::clone(&d.core),
            data_root: d.data_root.clone(),
            rp_id: d.rp_id.clone(),
            origins: d.origins.clone(),
            rp_name: d.rp_name.clone(),
            bootstrap_until_first_admin: d.bootstrap_until_first_admin,
            notes: Mutex::new(HashMap::new()),
            marker_lock: Mutex::new(()),
            gate: OnceLock::new(),
        };
        let challenge = start["publicKey"]["challenge"].as_str().unwrap().to_owned();
        let (cd, att) = key.register(RP, ORIGIN, &challenge, true);
        let finish = json!({ "ceremony_id": start["ceremony_id"], "id": key.credential_id_b64(), "response": { "clientDataJSON": cd, "attestationObject": att } });
        let r = forgot.close_enrolment(&finish, &a);
        assert!(
            matches!(&r, Err((409, _))),
            "a finish without its note is refused by name, never finished unbound: {r:?}"
        );
        assert_eq!(
            crate::sold::read(&d.data_root)
                .get("bolt")
                .map(|s| s.owner.clone()),
            Some(String::new()),
            "the repository waits for its buyer"
        );
        assert!(
            d.core
                .host()
                .store()
                .users()
                .unwrap()
                .iter()
                .all(|u| u.display != "bolt" || u.state != UserState::Active),
            "nobody was registered"
        );
        let done = d
            .close_enrolment(&finish, &a)
            .expect("the door that holds the note finishes it");
        assert_eq!(
            crate::sold::read(&d.data_root)["bolt"].owner,
            done["subject"].as_str().unwrap(),
            "bound to the buyer"
        );
    }

    /// ★ RA5-15: a full door refuses a new enrolment by name and drops no open one; notes
    /// whose ceremony expired make room.
    #[test]
    fn a_full_door_refuses_a_new_enrolment_and_expired_notes_make_room() {
        let (d, a, _dir) = door();
        let now = now_secs();
        {
            let mut notes = d.notes.lock().unwrap();
            for i in 0..MAX_NOTES {
                notes.insert(
                    format!("open-{i}"),
                    Note {
                        opened: now,
                        ..Note::default()
                    },
                );
            }
        }
        let ask = json!({ "display": "rickard", "role": "admin" });
        assert_eq!(
            d.open_enrolment(
                &ask,
                Some(&console()),
                Some(&"c".repeat(40)),
                &a,
                "203.0.113.1"
            )
            .unwrap_err(),
            (503, ENROLMENTS_FULL.to_owned())
        );
        assert_eq!(
            d.notes.lock().unwrap().len(),
            MAX_NOTES,
            "no open enrolment was dropped"
        );
        for n in d.notes.lock().unwrap().values_mut() {
            n.opened = now - 3600;
        }
        assert!(
            d.open_enrolment(
                &ask,
                Some(&console()),
                Some(&"c".repeat(40)),
                &a,
                "203.0.113.1"
            )
            .is_ok(),
            "expired notes make room"
        );
        assert_eq!(d.notes.lock().unwrap().len(), 1);
    }

    /// ★ The first-key door's invite enrols the first admin only: refused once an admin exists.
    #[test]
    fn a_first_key_invite_is_refused_once_an_admin_exists() {
        let (d, a, _dir) = door();
        let (secret, invite) = d.mint_first_key_invite("", "console", &a).unwrap();
        assert!(invite.created_by.starts_with(FIRST_KEY_BY));
        enrol(
            &d,
            &a,
            &mut SoftKey::new(8, &[8u8; 16]),
            json!({ "display": "rickard", "role": "admin" }),
            Some(&console()),
            Some(&"c".repeat(40)),
        )
        .unwrap();
        assert_eq!(
            enrol(
                &d,
                &a,
                &mut SoftKey::new(9, &[9u8; 16]),
                json!({ "invite": secret, "display": "x" }),
                None,
                None
            )
            .unwrap_err(),
            (409, FIRST_KEY_ENROLLED.to_owned())
        );
        assert_eq!(
            d.mint_first_key_invite("", "console", &a).unwrap_err(),
            (409, FIRST_KEY_ENROLLED.to_owned())
        );
    }

    /// ★ **The first sign-in link is single use on disk too** (HOL-DOCFIND f): once its invite
    /// enrolled the admin, `first-sign-in.url` is gone; a copy left by a crash between the
    /// enrolment and the removal is removed at the next start, which writes no new link.
    #[test]
    fn a_used_first_sign_in_link_is_removed_from_disk() {
        let (d, mut a, _dir) = door();
        a.roles.retain(|_, r| *r != crate::auth::Role::Admin);
        let file = d
            .offer_first_sign_in(&a)
            .unwrap()
            .expect("a box with no admin writes its first sign-in link");
        let link = std::fs::read_to_string(&file).unwrap();
        let secret = link.trim().split("invite=").nth(1).unwrap().to_owned();
        enrol(
            &d,
            &a,
            &mut SoftKey::new(11, &[11u8; 16]),
            json!({ "invite": secret, "display": "owner" }),
            None,
            None,
        )
        .unwrap();
        assert!(d.has_admin_passkey(&a), "the link's passkey is the admin");
        assert!(!file.exists(), "a used link is removed: {}", file.display());
        // A copy a crash left behind: the next start removes it and writes none.
        std::fs::write(&file, &link).unwrap();
        assert_eq!(d.offer_first_sign_in(&a).unwrap(), None);
        assert!(!file.exists(), "a stale link is removed at the start");
    }

    /// ★ A step-up is a fresh touch of one's own key through the core; the session says so.
    #[test]
    fn a_step_up_is_a_fresh_touch_of_ones_own_key() {
        let (d, a, _dir) = door();
        let mut owner = SoftKey::new(10, &[10u8; 16]);
        enrol(
            &d,
            &a,
            &mut owner,
            json!({ "display": "rickard", "role": "admin" }),
            Some(&console()),
            Some(&"c".repeat(40)),
        )
        .unwrap();
        let token = login(&d, &mut owner, false).unwrap()["token"]
            .as_str()
            .unwrap()
            .to_owned();
        let me = d.live_identity(&token).unwrap();
        assert!(
            !d.caller_stepped_up(&me, Some(&token)),
            "a plain touch is no step-up"
        );
        let start = d
            .open_step_up(Some(&me), Some(&token), "203.0.113.1")
            .unwrap();
        let challenge = start["publicKey"]["challenge"].as_str().unwrap().to_owned();
        let (cd, ad, sig) = owner.authenticate(RP, ORIGIN, &challenge, true);
        let up = d.close_step_up(&json!({ "id": owner.credential_id_b64(), "response": { "clientDataJSON": cd, "authenticatorData": ad, "signature": sig } }), Some(&me), Some(&token)).unwrap();
        assert_eq!(
            (up["signed_in"].as_bool(), up["stepped_up"].as_bool()),
            (Some(true), Some(true))
        );
        assert!(d.caller_stepped_up(&me, Some(&token)));
        assert!(
            d.caller_stepped_up(&console(), None),
            "a machine credential has no touch to refresh"
        );
        assert_eq!(refusal_json(STEP_UP_REFUSAL)["code"], STEP_UP_CODE);
    }

    /// ★ Revoking a passkey deactivates its person, never the last admin.
    #[test]
    fn revoking_a_passkey_ends_its_person_never_the_last_admin() {
        let (d, a, _dir) = door();
        let mut owner = SoftKey::new(11, &[11u8; 16]);
        let o = enrol(
            &d,
            &a,
            &mut owner,
            json!({ "display": "rickard", "role": "admin" }),
            Some(&console()),
            Some(&"c".repeat(40)),
        )
        .unwrap();
        assert_eq!(
            d.revoke_credential(o["credential_id"].as_str().unwrap(), "console", &a)
                .unwrap_err()
                .0,
            409
        );
        let list = d.credentials_json(&a);
        assert_eq!(list["enabled_admins"], 1);
        assert!(
            is_reserved_name(&a, "console")
                && is_reserved_name(&a, "passkey:x")
                && !is_reserved_name(&a, "anna")
        );
    }
}