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

//! **The repositories a person has here** (`holger.account.v1`, 2026-10-21, lane P4).
//!
//! The console is a thin layer. Its "My repositories" page lists what THIS server says the
//! signed-in person has, makes a private repository through THIS server, and shows the client
//! setup THIS server writes. The console keeps no owners, no permissions and no store.
//!
//! * **Who has what.** A repository made at run time carries its owner in the record the twin
//!   carries and a recharge brings back (`fleet/sold-repositories.json`, [`crate::sold`]). The
//!   owner sees it; an ADMIN of this server sees every repository but the estate's system ones.
//! * **Making one.** An admin makes a PRIVATE repository in a format this server already serves
//!   writable: it is shaped like that repository (its door, its storage, its own bytes beside
//!   it), its reads need a credential from the first request ([`crate::auth::ReadGate::
//!   gate_from_now`]), and it is served at once ([`crate::live_routes::LiveRoutes::serve_as`]).
//!   Private here means OWNER-ONLY (lane P1): its owner, the people its owner granted
//!   ([`Grant`](pb::account_service_server::AccountService::grant)) and the admins.
//! * **Client setup.** One snippet per client, with `YOUR-TOKEN` where a token from the
//!   console's Tokens page goes. The address is `HOLGER_REPOSITORY_URL` when set, else the
//!   console's own origin (the passkey relying party's), else a placeholder that says so.

use std::sync::Arc;

use tonic::{Request, Response, Status};

use crate::audit::{AuditAction, AuditEvent};
use crate::grpc::holger_account_proto as pb;
use crate::grpc::{coded, internal_err, HolgerGrpc};
use holger_errcode as ec;

/// The environment variable that names the address package clients use for this server's
/// repositories (`https://repo.example.org`). Unset: the console's own origin.
pub const REPOSITORY_URL_ENV: &str = "HOLGER_REPOSITORY_URL";

/// Where a token from the Tokens page goes in every snippet.
pub const TOKEN_PLACEHOLDER: &str = "YOUR-TOKEN";

/// The address used when this server knows none.
pub const UNKNOWN_ADDRESS: &str = "https://THIS-HOLGER";

/// What the console shows to a caller who is not signed in.
pub const SIGN_IN: &str = "sign in to see your repositories";
/// What the console shows to an admin.
pub const ADMIN_MAKES: &str =
    "you are an admin of this server: you see every repository and you may make a private one";
/// What the console shows to a person who is no admin.
pub const AN_ADMIN_MAKES: &str =
    "these are the repositories made for you; an admin of this server makes new ones";
/// What the console shows when this server makes no repository at run time.
pub const MAKES_NONE: &str =
    "this server makes no repository at run time; its repositories are set by its operator";

/// Pure: the client's word for a server repository type.
pub fn format_of(repo_type: &str) -> String {
    let repo_type = repo_type.trim().to_lowercase();
    match repo_type.as_str() {
        "rust" => "cargo",
        "maven3" | "maven" => "maven",
        "pip" => "pip",
        "npm" => "npm",
        "raw" => "raw",
        "oci" | "docker" => "oci",
        _ => return repo_type,
    }
    .to_owned()
}

/// Pure: **the client setup for repository `name` of `format` at `base`** (no trailing `/`).
/// `key` is the box's repository key (HOL-SIGN-2): the deb and rpm setup name its
/// fingerprint as the one key the client trusts, or say that the box holds none yet.
pub fn clients_for(
    name: &str,
    format: &str,
    base: &str,
    key: Option<&crate::repo_sign::RepoKey>,
) -> Vec<pb::ClientSetup> {
    let base = base.trim().trim_end_matches('/');
    let host = base.split("://").nth(1).unwrap_or(base);
    let t = TOKEN_PLACEHOLDER;
    let setup = |client: &str, file: &str, snippet: String| pb::ClientSetup {
        client: client.into(),
        file: file.into(),
        snippet,
    };
    // HOL-SIGN-2: which key a client must trust: THIS box's, by fingerprint. A downstream
    // signs with its own key, so its console names its own fingerprint, never its upstream's.
    let trust = match key {
        Some(k) => format!(
            "# The key to trust: this Holger's repository key, fingerprint {} ({}).\n",
            k.fingerprint_hex(),
            k.user_id()
        ),
        None => format!(
            "# This Holger holds no repository key yet, so the client cannot verify its metadata; \
             its admin makes one with `holger repo-key create --config <ron>` ({}).\n",
            crate::repo_sign::KEY_PATH
        ),
    };
    match format {
        "cargo" => vec![
            setup("cargo", "~/.cargo/config.toml", format!("[registries.{name}]\nindex = \"sparse+{base}/{name}/index/\"\n")),
            setup("cargo", "~/.cargo/credentials.toml", format!("[registries.{name}]\ntoken = \"{t}\"\n")),
            setup("cargo", "shell", format!("cargo publish --registry {name}\n")),
        ],
        "pip" => vec![
            setup("pip", "~/.config/pip/pip.conf", format!("[global]\nindex-url = https://token:{t}@{host}/{name}/simple/\n")),
            setup("pip", "shell", format!("pip install --index-url https://token:{t}@{host}/{name}/simple/ <package>\n")),
        ],
        "npm" => vec![setup("npm", ".npmrc", format!("registry={base}/{name}/\n//{host}/{name}/:_authToken={t}\n"))],
        "maven" => vec![setup(
            "maven",
            "~/.m2/settings.xml",
            format!(
                "<settings>\n  <servers>\n    <server>\n      <id>{name}</id>\n      <username>token</username>\n      <password>{t}</password>\n    </server>\n  </servers>\n  <mirrors>\n    <mirror>\n      <id>{name}</id>\n      <mirrorOf>*</mirrorOf>\n      <url>{base}/{name}/</url>\n    </mirror>\n  </mirrors>\n</settings>\n"
            ),
        )],
        // HOL-SIGNED-APT-DNF: the views sign their metadata, and the client checks it with the
        // key `/-/repo-key.asc` serves: no `[trusted=yes]`, no `gpgcheck=0`.
        "deb" => vec![
            setup("apt", "shell", format!("sudo install -d -m 0755 /etc/apt/keyrings\ncurl -fsSL {base}{key} | sudo tee /etc/apt/keyrings/{name}.asc >/dev/null\n{trust}# Check that the file holds that key before apt trusts it:\ngpg --show-keys /etc/apt/keyrings/{name}.asc\n", key = crate::repo_sign::KEY_PATH)),
            setup("apt", &format!("/etc/apt/sources.list.d/{name}.list"), format!("deb [signed-by=/etc/apt/keyrings/{name}.asc] {base}/{name} stable main\n")),
            setup("apt", &format!("/etc/apt/auth.conf.d/{name}.conf"), format!("machine {host}/{name}\nlogin token\npassword {t}\n")),
        ],
        "rpm" => vec![setup(
            "dnf",
            &format!("/etc/yum.repos.d/{name}.repo"),
            format!(
                "{trust}[{name}]\nname={name}\nbaseurl={base}/{name}\nusername=token\npassword={t}\nenabled=1\n# the packages' own signatures, against their vendor's key (rpm --import)\ngpgcheck=1\n# the metadata Holger signs, against Holger's key (dnf shows its fingerprint when it imports it)\nrepo_gpgcheck=1\ngpgkey={base}{key}\n",
                key = crate::repo_sign::KEY_PATH
            ),
        )],
        "oci" => vec![setup("docker", "shell", format!("docker login {host} -u token -p {t}\ndocker push {host}/{name}/<image>:<tag>\n"))],
        _ => vec![setup("curl", "shell", format!("curl -H \"Authorization: Bearer {t}\" -T <file> {base}/{name}/<path>\ncurl -H \"Authorization: Bearer {t}\" -O {base}/{name}/<path>\n"))],
    }
}

/// Pure: **the client setup for a side door** (lane HOL-FIELD-2 P19): a repository outside
/// the intake chain on a Holger whose policy requires a scan refuses writes
/// (`holger-intake-15`), so its setup keeps the reads and drops every publish and upload;
/// a note says publishing goes through the intake door, into `incoming`, or for a crates
/// registry through the chain's cargo door when the chain has one (lane
/// HOL-CARGO-PUBLISH-DOOR).
pub fn clients_for_side_door(
    name: &str,
    format: &str,
    base: &str,
    incoming: &str,
    cargo_door: Option<&str>,
    key: Option<&crate::repo_sign::RepoKey>,
) -> Vec<pb::ClientSetup> {
    let writes = |line: &str| {
        line.starts_with("cargo publish")
            || line.contains(" -T ")
            || line.starts_with("docker push")
    };
    let mut out: Vec<pb::ClientSetup> = clients_for(name, format, base, key)
        .into_iter()
        .filter_map(|mut s| {
            let kept: Vec<&str> = s.snippet.lines().filter(|l| !writes(l)).collect();
            if kept.is_empty() {
                return None;
            }
            s.snippet = kept.join("\n") + "\n";
            Some(s)
        })
        .collect();
    let snippet = match (format, cargo_door) {
        ("cargo", Some(door)) => format!(
            "# '{name}' takes no writes: this server's policy requires a scan, and publishing goes \
             through the intake chain. Publish to the chain's cargo door '{door}':\n\
             cargo publish --registry {door}\n"
        ),
        _ => format!(
            "# '{name}' takes no writes: this server's policy requires a scan, and publishing goes \
             through the intake door. Upload into '{incoming}' and walk it through the intake chain.\n"
        ),
    };
    out.push(pb::ClientSetup {
        client: format.into(),
        file: "publishing".into(),
        snippet,
    });
    out
}

/// Pure: **the client setup for the chain's cargo door** (lane HOL-CARGO-PUBLISH-DOOR): the
/// registry, its token, `cargo publish`, `cargo yank` and `cargo install`, and a note that a
/// publish is served once it walked the intake chain (scanned, checked, approved).
pub fn clients_for_cargo_door(
    name: &str,
    base: &str,
    key: Option<&crate::repo_sign::RepoKey>,
) -> Vec<pb::ClientSetup> {
    let mut out = clients_for(name, "cargo", base, key);
    if let Some(shell) = out.iter_mut().find(|s| s.file == "shell") {
        shell.snippet = format!(
            "cargo publish --registry {name}\n\
             cargo install --registry {name} <crate>\n\
             cargo yank --registry {name} <crate> --version <version>\n"
        );
    }
    out.push(pb::ClientSetup {
        client: "cargo".into(),
        file: "publishing".into(),
        snippet: format!(
            "# '{name}' is this server's cargo door into the intake chain: `cargo publish` is \
             INGESTED, and the crate is listed and installable once it is AVAILABLE (scanned, \
             checked against the policy, approved). A crate with a malware finding, or one pinned \
             to a dependency version with a known advisory, is refused by name.\n"
        ),
    });
    out
}

/// The address package clients use for this server's repositories.
fn repository_base(g: &HolgerGrpc) -> String {
    if let Ok(url) = std::env::var(REPOSITORY_URL_ENV) {
        if !url.trim().is_empty() {
            return url.trim().trim_end_matches('/').to_owned();
        }
    }
    g.auth_config
        .methods
        .iter()
        .find_map(|m| match m {
            // WWW-CANON: the site's own origin (`https://www.<rp_id>`), not the kept bare one.
            crate::auth::AuthMethodConfig::Passkey {
                rp_id, rp_origin, ..
            } => Some(crate::auth::site_origin(rp_id, rp_origin)),
            _ => None,
        })
        .unwrap_or_else(|| UNKNOWN_ADDRESS.to_owned())
}

/// The formats this server can make a repository in now: every format a served, writable,
/// non-system repository of the config has (its template), first such repository by name.
fn templates(g: &HolgerGrpc) -> Vec<(String, String)> {
    let Some(live) = &g.live_routes else {
        return Vec::new();
    };
    let made = g
        .data_root
        .as_deref()
        .map(crate::sold::read)
        .unwrap_or_default();
    let mut served = g.routes.all_repos();
    served.sort_by(|a, b| a.0.cmp(&b.0));
    let mut out: Vec<(String, String)> = Vec::new();
    for (name, b) in served {
        let info = traits::RepositoryInfo::of(&name, b.as_ref());
        if info.is_group
            || !info.writable
            || g.read_gate.is_system(&name)
            || made.contains_key(&name)
            || !live.declares(&name)
        {
            continue;
        }
        let format = format_of(&info.repo_type);
        if !out.iter().any(|(f, _)| *f == format) {
            out.push((format, name));
        }
    }
    out
}

/// **A person this server knows**, by a stable id: their subject, or an e-mail exactly one
/// active person holds: `(subject, name)`. Never by their display name (RA5-7): names are not
/// unique (a stranger may sign up as "Kim" before Kim), so a grant to a name could reach
/// somebody else. People are the identity store's (`holger-passkeys/identity.json`).
fn person_named(data_root: &std::path::Path, person: &str) -> Option<(String, String)> {
    let person = person.trim();
    if person.is_empty() {
        return None;
    }
    #[cfg(feature = "auth-passkey")]
    {
        let people = crate::identity_host::people(data_root);
        let active: Vec<&nornir_identity::User> = people
            .iter()
            .filter(|u| u.state == nornir_identity::UserState::Active)
            .collect();
        if let Some(hit) = active.iter().find(|u| u.id == person) {
            return Some((hit.id.clone(), hit.display.clone()));
        }
        let mail = person.to_ascii_lowercase();
        let held: Vec<&&nornir_identity::User> = active
            .iter()
            .filter(|u| {
                u.email
                    .as_deref()
                    .is_some_and(|e| e.trim().eq_ignore_ascii_case(&mail))
            })
            .collect();
        match held.as_slice() {
            [one] => Some((one.id.clone(), one.display.clone())),
            _ => None,
        }
    }
    #[cfg(not(feature = "auth-passkey"))]
    {
        let _ = data_root;
        None
    }
}

/// The grants of `m`, named as the passkey store names their people.
fn grants_of(g: &HolgerGrpc, m: &crate::sold::SoldRepository) -> Vec<pb::RepositoryGrant> {
    m.grants
        .iter()
        .map(|(subject, access)| {
            let person = g
                .data_root
                .as_deref()
                .and_then(|r| person_named(r, subject))
                .map(|(_, n)| n)
                .unwrap_or_else(|| subject.clone());
            pb::RepositoryGrant {
                person,
                subject: subject.clone(),
                access: access.clone(),
            }
        })
        .collect()
}

impl HolgerGrpc {
    /// Is the caller an admin of this server? (No RBAC: every caller is, as every gate says.)
    fn is_admin_here(&self, identity: &Option<crate::auth::AuthIdentity>) -> bool {
        if !self.auth_config.rbac_enabled() {
            return true;
        }
        identity.as_ref().is_some_and(|id| {
            self.auth_config
                .role_for_identity_in_repo("", id)
                .can_admin()
        })
    }

    /// One row of "My repositories".
    /// The client setup of `name`: a side door's reads only (P19, a chain build), else the
    /// whole setup.
    fn clients_of(&self, name: &str, format: &str, base: &str) -> Vec<pb::ClientSetup> {
        #[cfg(feature = "intake")]
        if let Some(c) = self.intake.as_ref() {
            // HOL-CARGO-PUBLISH-DOOR: the chain's cargo door offers publish, install and yank.
            if c.cargo_door() == Some(name) {
                return clients_for_cargo_door(name, base, self.repo_key.as_deref());
            }
            if c.is_side_door(name) {
                return clients_for_side_door(
                    name,
                    format,
                    base,
                    &c.settings().incoming,
                    c.cargo_door(),
                    self.repo_key.as_deref(),
                );
            }
        }
        clients_for(name, format, base, self.repo_key.as_deref())
    }

    fn my_row(
        &self,
        name: &str,
        made: Option<&crate::sold::SoldRepository>,
        base: &str,
    ) -> pb::MyRepository {
        let served = self.routes.lookup(name);
        let repo_type = match &served {
            Some(b) => traits::RepositoryInfo::of(name, b.as_ref()).repo_type,
            None => made
                .and_then(|m| self.routes.lookup(&m.template))
                .map(|b| traits::RepositoryInfo::of(name, b.as_ref()).repo_type)
                .unwrap_or_default(),
        };
        let format = format_of(&repo_type);
        let private = self.read_gate.repo_requires_auth(name) || made.is_some_and(|m| m.private);
        let words = match (&served, made) {
            (None, _) => "made, and served from this server's next start".to_owned(),
            (Some(_), _) if private => {
                "private: every read needs a credential of this server".to_owned()
            }
            (Some(_), _) => "open: anyone who reaches this server may read it".to_owned(),
        };
        let (plan, plan_state, paid_until_unix_ms, plan_words) = self.plan_of(name);
        pb::MyRepository {
            name: name.to_owned(),
            clients: self.clients_of(name, &format, base),
            format,
            private,
            owner: made.map(|m| m.owner.clone()).unwrap_or_default(),
            grants: made.map(|m| grants_of(self, m)).unwrap_or_default(),
            served: served.is_some(),
            words,
            plan,
            plan_state,
            paid_until_unix_ms,
            plan_words,
        }
    }

    /// **Where its plan stands**: the stored fact's plan, state and paid-until, by this
    /// server's own clock (a lapsed period is said as such). No fact: free.
    #[cfg(feature = "metered")]
    fn plan_of(&self, name: &str) -> (String, String, i64, String) {
        use monetize_product::State;
        let fact = self
            .entitlements
            .as_ref()
            .and_then(|s| s.get(name).ok().flatten());
        let Some(f) = fact else { return free() };
        let until = f.paid_until_unix_ms.unwrap_or(0) as i64;
        let date = if until > 0 {
            crate::metered::civil_date(until as u64)
        } else {
            String::new()
        };
        if let Some(why) = crate::metered::lapsed(&f, crate::clock::now_unix_ms()) {
            return (f.plan.clone(), "lapsed".into(), until, why);
        }
        let (state, words) = match f.state {
            State::Free => return free(),
            State::Paid if date.is_empty() => ("paid", "paid".to_owned()),
            State::Paid => ("paid", format!("paid until {date}")),
            State::Grace => (
                "grace",
                format!(
                    "in its grace period: pay before it is suspended{}",
                    if date.is_empty() {
                        String::new()
                    } else {
                        format!(" (paid until {date})")
                    }
                ),
            ),
            State::Suspended => (
                "suspended",
                "suspended: uploads are refused until it is paid again".to_owned(),
            ),
            State::Retention => (
                "retention",
                "kept for retention only: uploads are refused".to_owned(),
            ),
        };
        (f.plan.clone(), state.into(), until, words)
    }

    #[cfg(not(feature = "metered"))]
    fn plan_of(&self, _name: &str) -> (String, String, i64, String) {
        free()
    }
}

#[tonic::async_trait]
impl pb::account_service_server::AccountService for Arc<HolgerGrpc> {
    async fn my_repositories(
        &self,
        request: Request<pb::MyRepositoriesRequest>,
    ) -> Result<Response<pb::MyRepositoriesReply>, Status> {
        // Never refused: an invalid credential is a caller with nothing here.
        let identity = self
            .authenticate(&request, "list your repositories")
            .await
            .unwrap_or(None);
        if self.auth_config.rbac_enabled() && identity.is_none() {
            return Ok(Response::new(pb::MyRepositoriesReply {
                repositories: Vec::new(),
                formats: Vec::new(),
                words: SIGN_IN.to_owned(),
            }));
        }
        let admin = self.is_admin_here(&identity);
        let subject = identity
            .as_ref()
            .map(|i| i.subject.clone())
            .unwrap_or_default();
        let made = self
            .data_root
            .as_deref()
            .map(crate::sold::read)
            .unwrap_or_default();
        let base = repository_base(self);
        let mut names: Vec<String> = Vec::new();
        if admin {
            names.extend(
                self.routes
                    .all_repos()
                    .into_iter()
                    .map(|(n, _)| n)
                    .filter(|n| !self.read_gate.is_system(n)),
            );
        }
        names.extend(
            made.iter()
                .filter(|(_, m)| {
                    admin
                        || (!subject.is_empty()
                            && (m.owner == subject || m.grants.contains_key(&subject)))
                })
                .map(|(n, _)| n.clone()),
        );
        names.sort();
        names.dedup();
        let repositories = names
            .iter()
            .map(|n| self.my_row(n, made.get(n), &base))
            .collect();
        let formats: Vec<String> = if admin {
            templates(self).into_iter().map(|(f, _)| f).collect()
        } else {
            Vec::new()
        };
        let words = match (admin, formats.is_empty()) {
            (true, true) => MAKES_NONE,
            (true, false) => ADMIN_MAKES,
            (false, _) => AN_ADMIN_MAKES,
        };
        Ok(Response::new(pb::MyRepositoriesReply {
            repositories,
            formats,
            words: words.to_owned(),
        }))
    }

    /// **The owner grants or revokes** (lane P1): one named person, one owner-only repository,
    /// `read`, `write` (read and write) or `none`. Its owner or an admin only; audited; the gate
    /// every door holds learns it at once and the record the twin carries keeps it.
    async fn grant(
        &self,
        request: Request<pb::GrantRequest>,
    ) -> Result<Response<pb::GrantReply>, Status> {
        self.mutation_hold()?;
        let identity = self.authenticate(&request, "grant on a repository").await?;
        let Some(caller) = identity.clone() else {
            return Err(coded(
                &ec::ACCOUNT_GRANT_NOT_OWNER,
                tonic::Code::Unauthenticated,
                "sign in: only the repository's owner or an admin grants on it",
            ));
        };
        let r = request.into_inner();
        let repo = r.repository.trim().to_owned();
        let write = match r.access.trim().to_ascii_lowercase().as_str() {
            "read" => Some(false),
            "write" | "read+write" | "readwrite" => Some(true),
            "none" | "revoke" | "" => None,
            other => {
                return Err(coded(
                    &ec::ACCOUNT_GRANT_ACCESS,
                    tonic::Code::InvalidArgument,
                    format!("{other:?} is no access: read, write or none"),
                ))
            }
        };
        let root = self.data_root.clone().ok_or_else(|| {
            coded(
                &ec::ACCOUNT_MAKES_NONE,
                tonic::Code::FailedPrecondition,
                MAKES_NONE,
            )
        })?;
        let admin = self.is_admin_here(&identity);
        let unknown = || {
            coded(&ec::ACCOUNT_GRANT_UNKNOWN, tonic::Code::NotFound, format!("'{repo}' is no owner-only repository here, or {:?} is nobody here (name the person by their id or e-mail, never their display name)", r.person.trim()))
        };
        let made = crate::sold::read(&root);
        let Some(m) = made
            .get(&repo)
            .filter(|_| self.read_gate.sold_owner(&repo).is_some())
        else {
            return Err(unknown());
        };
        if !admin && (m.owner.is_empty() || m.owner != caller.subject) {
            return Err(coded(
                &ec::ACCOUNT_GRANT_NOT_OWNER,
                tonic::Code::PermissionDenied,
                format!("'{repo}' is not yours: only its owner or an admin grants on it"),
            ));
        }
        let (subject, person) = person_named(&root, r.person.trim()).ok_or_else(unknown)?;
        if subject == m.owner {
            return Err(coded(
                &ec::ACCOUNT_GRANT_UNKNOWN,
                tonic::Code::InvalidArgument,
                format!("{person} owns '{repo}': an owner needs no grant"),
            ));
        }
        let updated = crate::sold::update(&root, |made| {
            let s = made.get_mut(&repo).ok_or_else(|| "gone".to_owned())?;
            match write {
                Some(w) => {
                    s.grants
                        .insert(subject.clone(), crate::sold::access_word(w).to_owned());
                }
                None => {
                    s.grants.remove(&subject);
                }
            }
            Ok(s.clone())
        })
        .map_err(|_| unknown())?;
        self.read_gate.grant(&repo, &subject, write);
        let said = match write {
            Some(w) => format!(
                "{person} may {} '{repo}'",
                if w { "read and write" } else { "read" }
            ),
            None => format!("{person} no longer has '{repo}'"),
        };
        self.record_audit(
            AuditEvent::new(
                caller.audit_name(),
                AuditAction::Other,
                &repo,
                "",
                "account",
                200,
                0,
            )
            .with_detail(format!("grant: {said} ({subject})")),
        );
        Ok(Response::new(pb::GrantReply {
            repository: repo,
            grants: grants_of(self, &updated),
            words: said,
        }))
    }

    async fn create_repository(
        &self,
        request: Request<pb::CreateRepositoryRequest>,
    ) -> Result<Response<pb::CreateRepositoryReply>, Status> {
        self.mutation_hold()?;
        let identity = self
            .authorize_admin_identity(&request, "", "make a repository")
            .await?;
        let owner = identity
            .as_ref()
            .map(|i| i.subject.clone())
            .unwrap_or_default();
        let name = request.get_ref().name.trim().to_owned();
        let format = request.get_ref().format.trim().to_lowercase();
        if !crate::sold::repository_name_ok(&name) {
            return Err(coded(&ec::ACCOUNT_NAME_REFUSED, tonic::Code::InvalidArgument, format!("{name:?} is no repository name (lower-case letters, digits, '-', '_' and '.', at most 64): nothing was made")));
        }
        let (Some(root), Some(live)) = (self.data_root.clone(), self.live_routes.clone()) else {
            return Err(coded(
                &ec::ACCOUNT_MAKES_NONE,
                tonic::Code::FailedPrecondition,
                MAKES_NONE,
            ));
        };
        let offered = templates(self);
        let Some((_, template)) = offered.iter().find(|(f, _)| *f == format).cloned() else {
            let can: Vec<&str> = offered.iter().map(|(f, _)| f.as_str()).collect();
            return Err(coded(
                &ec::ACCOUNT_FORMAT_REFUSED,
                tonic::Code::FailedPrecondition,
                format!(
                    "this server makes no {format:?} repository (it makes: {})",
                    if can.is_empty() {
                        "none".to_owned()
                    } else {
                        can.join(", ")
                    }
                ),
            ));
        };
        if self.routes.lookup(&name).is_some() {
            return Err(coded(
                &ec::ACCOUNT_NAME_TAKEN,
                tonic::Code::AlreadyExists,
                format!("'{name}' is taken: this server serves a repository by that name"),
            ));
        }
        crate::sold::update(&root, |made| {
            if made.contains_key(&name) {
                return Err(format!(
                    "'{name}' is taken: a repository by that name was made here"
                ));
            }
            made.insert(
                name.clone(),
                crate::sold::SoldRepository {
                    template: template.clone(),
                    signup: String::new(),
                    owner: owner.clone(),
                    private: true,
                    ..Default::default()
                },
            );
            Ok(())
        })
        .map_err(|e| {
            if e.contains("is taken") {
                coded(&ec::ACCOUNT_NAME_TAKEN, tonic::Code::AlreadyExists, e)
            } else {
                internal_err(e)
            }
        })?;
        self.record_audit(
            AuditEvent::new(
                if owner.is_empty() {
                    "anonymous"
                } else {
                    &owner
                },
                AuditAction::Other,
                &name,
                "",
                "account",
                200,
                0,
            )
            .with_detail(format!(
                "made private, owned by {owner:?}, shaped like {template}"
            )),
        );
        // Owner-only (lane P1): its owner, the people the owner granted and the admins.
        live.own(&name, &crate::sold::gate_owner(&root, &owner));
        let words = match live.serve_as(&name, &template, true) {
            Ok(()) => format!("'{name}' is made and served: private, and yours"),
            Err(why) => {
                log::warn!("repository '{name}': made private for {owner:?}, served from the next start ({why})");
                format!("'{name}' is made, and served from this server's next start ({why})")
            }
        };
        let made = crate::sold::read(&root);
        let repository = self.my_row(&name, made.get(&name), &repository_base(self));
        Ok(Response::new(pb::CreateRepositoryReply {
            repository: Some(repository),
            words,
        }))
    }
}

/// A repository with no stored fact: nothing limits it.
fn free() -> (String, String, i64, String) {
    (
        String::new(),
        "free".into(),
        0,
        "nothing limits it here".into(),
    )
}

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

    #[test]
    fn the_client_words_name_the_server_types() {
        assert_eq!(format_of("rust"), "cargo");
        assert_eq!(format_of("maven3"), "maven");
        assert_eq!(format_of("docker"), "oci");
        assert_eq!(format_of("helm"), "helm");
        assert_eq!(format_of("Raw"), "raw", "the backend's own spelling");
        assert_eq!(format_of("Rust"), "cargo");
    }

    /// **A side door offers no publish** (lane HOL-FIELD-2 P19): where the policy requires a
    /// scan, a repository outside the intake chain refuses writes, so the console offers no
    /// `cargo publish` or upload for it; it says publishing goes through the intake door.
    #[test]
    fn a_side_door_offers_reads_and_says_publishing_goes_through_the_intake_door() {
        for format in ["cargo", "raw"] {
            let c = clients_for_side_door(
                "crates",
                format,
                "https://repo.example.org",
                "incoming",
                None,
                None,
            );
            let all: String = c.iter().map(|s| s.snippet.as_str()).collect();
            assert!(
                !all.contains("cargo publish") && !all.contains(" -T "),
                "{format}: no publish and no upload: {c:?}"
            );
            assert!(
                all.contains("publishing goes through the intake door")
                    && all.contains("'incoming'"),
                "{format}: it says where publishing goes: {c:?}"
            );
        }
        let cargo = clients_for_side_door(
            "crates",
            "cargo",
            "https://repo.example.org",
            "incoming",
            None,
            None,
        );
        assert!(
            cargo[0]
                .snippet
                .contains("sparse+https://repo.example.org/crates/index/"),
            "reading stays: {cargo:?}"
        );
        // HOL-CARGO-PUBLISH-DOOR: with a cargo door, a crates side door sends `cargo publish` there.
        let to_door = clients_for_side_door(
            "crates-old",
            "cargo",
            "https://repo.example.org",
            "incoming",
            Some("crates"),
            None,
        );
        let all: String = to_door.iter().map(|s| s.snippet.as_str()).collect();
        assert!(
            all.contains("cargo publish --registry crates\n")
                && !all.contains("cargo publish --registry crates-old"),
            "{to_door:?}"
        );
    }

    /// **The cargo door offers `cargo publish` again** (lane HOL-CARGO-PUBLISH-DOOR): the
    /// registry, the token, publish, install and yank, and a note that a publish walks the
    /// intake chain before it is served.
    #[test]
    fn the_cargo_door_offers_publish_install_and_yank_and_says_it_walks_the_chain() {
        let c = clients_for_cargo_door("crates", "https://repo.example.org", None);
        let all: String = c.iter().map(|s| s.snippet.as_str()).collect();
        for want in [
            "sparse+https://repo.example.org/crates/index/",
            "token = \"YOUR-TOKEN\"",
            "cargo publish --registry crates",
            "cargo install --registry crates <crate>",
            "cargo yank --registry crates <crate> --version <version>",
            "INGESTED",
            "AVAILABLE",
        ] {
            assert!(all.contains(want), "{want}: {c:?}");
        }
    }

    #[test]
    fn every_snippet_names_the_repository_its_address_and_the_token_placeholder() {
        for format in ["cargo", "pip", "npm", "maven", "oci", "deb", "rpm", "raw"] {
            let c = clients_for("acme", format, "https://repo.example.org/", None);
            assert!(!c.is_empty(), "{format}");
            assert!(
                c.iter().all(|s| s.snippet.contains("acme")),
                "{format}: {c:?}"
            );
            assert!(
                c.iter().any(|s| s.snippet.contains(TOKEN_PLACEHOLDER)),
                "{format}: {c:?}"
            );
            assert!(
                c.iter().all(|s| !s.snippet.contains("org//")),
                "no doubled slash: {c:?}"
            );
        }
        let cargo = clients_for("acme", "cargo", "https://repo.example.org", None);
        assert_eq!(
            cargo[0].snippet,
            "[registries.acme]\nindex = \"sparse+https://repo.example.org/acme/index/\"\n"
        );
        let npm = clients_for("acme", "npm", "https://repo.example.org", None);
        assert!(
            npm[0]
                .snippet
                .contains("//repo.example.org/acme/:_authToken=YOUR-TOKEN"),
            "{npm:?}"
        );
    }

    /// ★ HOL-SIGNED-APT-DNF: **apt and dnf check the metadata Holger signs.** The deb setup
    /// fetches the key `/-/repo-key.asc` serves and names it `signed-by=`; the rpm setup has
    /// `gpgcheck=1`, `repo_gpgcheck=1` and that key as `gpgkey=`. Neither says
    /// `trusted=yes` or `gpgcheck=0`.
    #[test]
    fn the_apt_and_dnf_setup_verify_the_signed_metadata() {
        let deb = clients_for("acme", "deb", "https://repo.example.org", None);
        let all: String = deb.iter().map(|s| s.snippet.as_str()).collect();
        assert!(
            all.contains("https://repo.example.org/-/repo-key.asc"),
            "{all}"
        );
        assert!(
            all.contains("deb [signed-by=/etc/apt/keyrings/acme.asc] https://repo.example.org/acme stable main"),
            "{all}"
        );
        let rpm = clients_for("acme", "rpm", "https://repo.example.org", None);
        let repo = &rpm[0].snippet;
        for want in [
            "gpgcheck=1",
            "repo_gpgcheck=1",
            "gpgkey=https://repo.example.org/-/repo-key.asc",
        ] {
            assert!(repo.contains(want), "{want}: {repo}");
        }
        for s in deb.iter().chain(&rpm) {
            assert!(
                !s.snippet.contains("trusted=yes") && !s.snippet.contains("gpgcheck=0"),
                "{s:?}"
            );
        }
    }

    /// ★ HOL-SIGN-2: **the console names the key a client must trust**: the deb and rpm
    /// setup carry THIS box's key fingerprint (a downstream's own, never its upstream's),
    /// and a box with no key says so and how to make one.
    #[test]
    fn the_apt_and_dnf_setup_name_the_boxs_key_by_fingerprint() {
        let k =
            crate::repo_sign::RepoKey::generate("Holger downstream key", 1_760_000_000).unwrap();
        let fpr = k.fingerprint_hex();
        for format in ["deb", "rpm"] {
            let all: String = clients_for("acme", format, "https://repo.example.org", Some(&k))
                .iter()
                .map(|s| s.snippet.clone())
                .collect();
            assert!(
                all.contains(&format!("fingerprint {fpr} (Holger downstream key)")),
                "{format}: {all}"
            );
            let none: String = clients_for("acme", format, "https://repo.example.org", None)
                .iter()
                .map(|s| s.snippet.clone())
                .collect();
            assert!(
                none.contains("holds no repository key") && none.contains("holger repo-key create"),
                "{format}: {none}"
            );
        }
        let apt: String = clients_for("acme", "deb", "https://repo.example.org", Some(&k))
            .iter()
            .map(|s| s.snippet.clone())
            .collect();
        assert!(
            apt.contains("gpg --show-keys /etc/apt/keyrings/acme.asc"),
            "{apt}"
        );
    }

    /// ★ RA5-7: **a grant resolves a person by a stable id, never by a display name.** Names
    /// are not unique: a stranger who signed up as "Kim" first must never receive the grant an
    /// owner meant for the real Kim. The subject (or an e-mail one active person holds) is
    /// taken; a name is refused, as is an e-mail two people hold.
    #[cfg(feature = "auth-passkey")]
    #[test]
    fn a_grant_resolves_a_person_by_a_stable_id_never_a_display_name() {
        use nornir_identity::UserStore as _;
        let root = tempfile::tempdir().unwrap();
        let store = crate::identity_host::HolgerStore::open(root.path()).unwrap();
        let kim = |id: &str, email: &str| nornir_identity::User {
            id: id.into(),
            display: "Kim".into(),
            email: Some(email.into()),
            groups: vec![],
            state: nornir_identity::UserState::Active,
            created: 1,
        };
        store
            .put_user(&kim("u-stranger", "stranger@mock.invalid"))
            .unwrap();
        store.put_user(&kim("u-kim", "kim@mock.invalid")).unwrap();
        assert_eq!(
            person_named(root.path(), "Kim"),
            None,
            "a display name is no person"
        );
        assert_eq!(
            person_named(root.path(), "u-kim"),
            Some(("u-kim".into(), "Kim".into())),
            "the subject is"
        );
        assert_eq!(
            person_named(root.path(), " Kim@Mock.Invalid "),
            Some(("u-kim".into(), "Kim".into())),
            "an e-mail one active person holds is"
        );
        assert_eq!(person_named(root.path(), "nobody@mock.invalid"), None);
        store.put_user(&kim("u-dup-1", "dup@mock.invalid")).unwrap();
        store.put_user(&kim("u-dup-2", "dup@mock.invalid")).unwrap();
        assert_eq!(
            person_named(root.path(), "dup@mock.invalid"),
            None,
            "an e-mail two people hold names nobody"
        );
    }
}