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

nordisk/gunnar

crates/gunnar-core/src/auth/clan_table.rs

//! **Clans** (owner, 2026-10-03: "ordinary accounts can group with other in a
//! clan, where group created repos are administered by the group admin"; and
//! 2026-10-05: "when built, each member contributes storage to the clan").
//!
//! A clan is a group of ORDINARY accounts with a namespace of its own — the
//! clan's name, created with the clan, private (and its events too) until its
//! admin says otherwise, as for every account. In the identity table, in the
//! same compare-and-swap as the grants it widens:
//!
//! * **who is in it.** Its admins (the creator first) and its members. An
//!   account joins by invitation: an admin invites, the account accepts. Only
//!   a person's own account can be in a clan — never an enterprise account's
//!   EMPLOYEE (its account admin's rules win), never a machine or a group.
//! * **what it may do.** Rule 2½ of [`AuthTable::may`]: a member reads and
//!   writes every repository in the clan's namespace and creates private ones
//!   there; an ADMIN administers them — visibility, grants, deletion — exactly
//!   as an account's owner administers the account's. Intrinsic, like account
//!   ownership: no grant row a `Revoke` could take away.
//! * **its room.** Every member contributes part of its OWN room; the clan's
//!   room is the sum. A contribution comes out of what the member's account
//!   holds and has neither stored nor promised (its employees, its seats, its
//!   other clans), and goes down only as far as what the clan stores leaves
//!   room for; leaving returns it, and is refused while it is in use.
//!
//! What the caller measures (the member's cap and what it stores, what the
//! clan stores) comes in as arguments: this table holds identities, not bytes,
//! exactly as [`AuthTable::enroll_with_room`] takes its room.

use crate::error::{human_bytes, Error, Result};
use crate::model::{now_ms, Level, PrincipalKind, UnixMillis};

use super::{AuthDoc, AuthTable};

/// **A clan**, as the identity table holds it, keyed by its name — which is
/// also its namespace.
#[derive(Clone, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Clan {
    /// The clan's name = its namespace.
    pub name: String,
    /// The account that created it (its first admin).
    pub created_by: String,
    /// When.
    pub created_at: UnixMillis,
    /// Its members, admins included, in the order they joined.
    #[serde(default)]
    pub members: Vec<ClanMember>,
    /// Accounts invited and not yet answered.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub invites: Vec<ClanInvite>,
    /// Repositories offered out of the clan, not yet answered (round 2).
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub offers: Vec<ClanRepoOffer>,
}

/// **A repository offered out of a clan** to an account (owner, 2026-10-05:
/// "to dissolve, the admin first moves the clan's repos out"): the clan's
/// admin offers, the account's admin accepts or declines.
#[derive(Clone, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ClanRepoOffer {
    /// The repository's name in the clan.
    pub repo: String,
    /// The account it is offered to.
    pub to: String,
    /// The admin who offered it.
    pub by: String,
    /// When.
    pub at: UnixMillis,
}

/// **One member of a clan.**
#[derive(Clone, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ClanMember {
    /// The member's account (its principal id).
    pub principal: String,
    /// Administers the clan and its repositories.
    #[serde(default)]
    pub admin: bool,
    /// Bytes of the member's OWN room it gives the clan.
    #[serde(default)]
    pub contribution_bytes: u64,
    /// When it joined.
    pub joined_at: UnixMillis,
}

/// **An invitation**, waiting for its account's answer.
#[derive(Clone, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ClanInvite {
    /// The account invited.
    pub principal: String,
    /// The admin who invited it.
    pub by: String,
    /// When.
    pub at: UnixMillis,
}

/// **Where a principal stands in a clan.**
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum ClanRole {
    /// Administers the clan.
    Admin,
    /// A member.
    Member,
    /// Invited, not yet answered.
    Invited,
}

/// **Which kind of no** a clan verb answered ([`Error::Clan`]).
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum ClanRefusal {
    /// No such clan (or not one the caller may see).
    NotFound,
    /// The caller may not: it is not the clan's admin, or not its member.
    Denied,
    /// The rule says no: room, the floor, the last admin, an employee.
    Precondition,
    /// The request itself is malformed.
    Invalid,
    /// The name is taken (`reason` is then the noun: "a clan called x").
    Exists,
}

/// **A member's own account's room**, measured by the caller: its cap
/// (`None`: nothing caps this account on this server) and what it stores.
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub struct AccountRoom {
    /// The account's room, bytes. `None`: uncapped here.
    pub cap: Option<u64>,
    /// What the account stores now, bytes.
    pub stored: u64,
}

impl Clan {
    /// **The clan's room**: the sum of its members' contributions.
    pub fn room_bytes(&self) -> u64 {
        self.members
            .iter()
            .map(|m| m.contribution_bytes)
            .fold(0, u64::saturating_add)
    }

    /// The member row of `principal`, if it is one.
    pub fn member(&self, principal: &str) -> Option<&ClanMember> {
        self.members.iter().find(|m| m.principal == principal)
    }

    /// Where `principal` stands in this clan.
    pub fn role_of(&self, principal: &str) -> Option<ClanRole> {
        match self.member(principal) {
            Some(m) if m.admin => Some(ClanRole::Admin),
            Some(_) => Some(ClanRole::Member),
            None if self.invites.iter().any(|i| i.principal == principal) => {
                Some(ClanRole::Invited)
            }
            None => None,
        }
    }

    /// How many admins it has.
    pub fn admins(&self) -> usize {
        self.members.iter().filter(|m| m.admin).count()
    }

    /// **How low `member`'s contribution may go** while the clan stores
    /// `used`: what the clan stores, less what the others give. 0 when the
    /// others' contributions cover it.
    pub fn floor_for(&self, member: &str, used: u64) -> u64 {
        self.floor_in_force(member, used, &[])
    }

    /// [`Clan::floor_for`] with the shares of `lapsed` members not counted
    /// (round 2: a member whose paid period has ended gives the clan nothing
    /// the door counts). A lapsed member's own share is already out of force,
    /// so its floor is 0.
    pub fn floor_in_force(&self, member: &str, used: u64, lapsed: &[String]) -> u64 {
        if lapsed.iter().any(|l| l == member) {
            return 0;
        }
        let others: u64 = self
            .members
            .iter()
            .filter(|m| m.principal != member && !lapsed.contains(&m.principal))
            .map(|m| m.contribution_bytes)
            .fold(0, u64::saturating_add);
        used.saturating_sub(others)
    }

    /// **The room in force**: the sum of the shares of members not in
    /// `lapsed`.
    pub fn room_in_force(&self, lapsed: &[String]) -> u64 {
        self.members
            .iter()
            .filter(|m| !lapsed.contains(&m.principal))
            .map(|m| m.contribution_bytes)
            .fold(0, u64::saturating_add)
    }

    /// The open offer of `repo`, if any.
    pub fn offer_of(&self, repo: &str) -> Option<&ClanRepoOffer> {
        self.offers.iter().find(|o| o.repo == repo)
    }
}

fn refused(clan: &str, refusal: ClanRefusal, reason: String) -> Error {
    Error::Clan {
        clan: clan.to_owned(),
        refusal,
        reason,
    }
}

impl AuthTable {
    // ── reads ──────────────────────────────────────────────────────────────

    /// **One clan**, if there is one by that name.
    pub fn clan(&self, name: &str) -> Result<Option<Clan>> {
        let doc = self.doc.load()?;
        if doc.clans.is_empty() {
            return Ok(None);
        }
        doc.clans.get(name)
    }

    /// **Is `name` a clan's namespace?**
    pub fn is_clan(&self, name: &str) -> Result<bool> {
        Ok(self.clan(name)?.is_some())
    }

    /// **The clans `principal` is in or invited to**, by name.
    pub fn clans_of(&self, principal: &str) -> Result<Vec<Clan>> {
        let doc = self.doc.load()?;
        let mut out = Vec::new();
        for name in doc.clan_index.get(principal)?.unwrap_or_default() {
            if let Some(clan) = doc.clans.get(&name)? {
                out.push(clan);
            }
        }
        Ok(out)
    }

    /// **Where `principal` stands in the clan whose namespace is `namespace`**,
    /// or `None` when it is not a clan or `principal` is not in it.
    pub fn clan_role(&self, principal: &str, namespace: &str) -> Result<Option<ClanRole>> {
        Ok(self
            .clan(namespace)?
            .and_then(|clan| clan.role_of(principal)))
    }

    /// **What `account` gives all its clans**, bytes — what its own room is
    /// short of by being in them. One probe and one per clan.
    pub fn contributed_by(&self, account: &str) -> Result<u64> {
        let doc = self.doc.load()?;
        Self::contributed_in(&doc, account, None)
    }

    /// **What `account` could still give a clan**: its room, less what it
    /// stores, what its employees and open seats hold, and what it gives its
    /// clans. `None` when nothing caps the account here.
    pub fn room_left(&self, account: &str, room: AccountRoom) -> Result<Option<u64>> {
        let Some(cap) = room.cap else { return Ok(None) };
        let doc = self.doc.load()?;
        let committed = room
            .stored
            .saturating_add(Self::promised_by(&doc, account)?)
            .saturating_add(Self::contributed_in(&doc, account, None)?);
        Ok(Some(cap.saturating_sub(committed)))
    }

    /// The level a clan gives `principal` in `namespace`: `Admin` for its
    /// admins, `Write` for its members, `None` otherwise (and for a namespace
    /// that is no clan's). Rule 2½ of [`AuthTable::may`] and
    /// [`AuthTable::may_namespace`] — ONE reading for both.
    pub(super) fn clan_level(
        doc: &AuthDoc,
        principal: &str,
        namespace: &str,
    ) -> Result<Option<Level>> {
        if doc.clans.is_empty() {
            return Ok(None);
        }
        let Some(clan) = doc.clans.get(namespace)? else {
            return Ok(None);
        };
        Ok(clan
            .member(principal)
            .map(|m| if m.admin { Level::Admin } else { Level::Write }))
    }

    /// What `account` gives its clans, the clan `except` left out.
    pub(super) fn contributed_in(
        doc: &AuthDoc,
        account: &str,
        except: Option<&str>,
    ) -> Result<u64> {
        if doc.clan_index.is_empty() {
            return Ok(0);
        }
        let mut sum: u64 = 0;
        for name in doc.clan_index.get(account)?.unwrap_or_default() {
            if Some(name.as_str()) == except {
                continue;
            }
            if let Some(clan) = doc.clans.get(&name)? {
                if let Some(m) = clan.member(account) {
                    sum = sum.saturating_add(m.contribution_bytes);
                }
            }
        }
        Ok(sum)
    }

    /// **The clan, if `by` is its admin** — the refusal its admin verbs give
    /// otherwise, asked without writing (the plane checks this before it
    /// touches the catalog for a dissolve).
    pub fn clan_for(&self, clan: &str, by: &str) -> Result<Clan> {
        let doc = self.doc.load()?;
        Self::clan_for_admin(&doc, clan, by, "dissolve it")
    }

    // ── writes ─────────────────────────────────────────────────────────────

    /// **Make a clan**: `name` (its namespace — the caller creates that in the
    /// catalog), `creator` its first admin, giving `contribution` bytes of its
    /// own room. The name must be no principal's id and no clan's; the creator
    /// must be an ordinary account ([`AuthTable::ordinary_account`]).
    pub fn create_clan(
        &self,
        name: &str,
        creator: &str,
        contribution: u64,
        room: AccountRoom,
    ) -> Result<Clan> {
        crate::sandbox::check_component(name, name)?;
        if name == crate::model::ANONYMOUS_PRINCIPAL_ID {
            return Err(Error::ReservedPrincipalId(name.to_owned()));
        }
        let at = now_ms();
        self.doc.update(|doc| {
            Self::ordinary_account(doc, name, creator, "make a clan")?;
            if doc.clans.contains_key(name)? {
                return Err(refused(
                    name,
                    ClanRefusal::Exists,
                    format!("a clan called {name}"),
                ));
            }
            if doc.principals.contains_key(name)? {
                return Err(refused(
                    name,
                    ClanRefusal::Exists,
                    format!("an account called {name}"),
                ));
            }
            Self::fits_in_room(doc, name, creator, contribution, room)?;
            let clan = Clan {
                name: name.to_owned(),
                created_by: creator.to_owned(),
                created_at: at,
                members: vec![ClanMember {
                    principal: creator.to_owned(),
                    admin: true,
                    contribution_bytes: contribution,
                    joined_at: at,
                }],
                invites: Vec::new(),
                offers: Vec::new(),
            };
            doc.clans.insert(name.to_owned(), clan.clone())?;
            Self::index_clan(doc, creator, name)?;
            Ok(clan)
        })
    }

    /// **Invite `principal`** into `clan` — an admin's (`by`). The account
    /// must be an ordinary one, not in the clan and not invited already.
    pub fn invite_to_clan(&self, clan: &str, by: &str, principal: &str) -> Result<Clan> {
        let at = now_ms();
        self.doc.update(|doc| {
            let mut row = Self::clan_for_admin(doc, clan, by, "invite an account")?;
            Self::ordinary_account(doc, clan, principal, "join a clan")?;
            match row.role_of(principal) {
                Some(ClanRole::Admin | ClanRole::Member) => {
                    return Err(refused(
                        clan,
                        ClanRefusal::Precondition,
                        format!("{principal} is already in clan {clan}"),
                    ))
                }
                Some(ClanRole::Invited) => {
                    return Err(refused(
                        clan,
                        ClanRefusal::Precondition,
                        format!(
                            "{principal} is already invited to clan {clan}, and has not answered"
                        ),
                    ))
                }
                None => {}
            }
            row.invites.push(ClanInvite {
                principal: principal.to_owned(),
                by: by.to_owned(),
                at,
            });
            doc.clans.insert(clan.to_owned(), row.clone())?;
            Self::index_clan(doc, principal, clan)?;
            Ok(row)
        })
    }

    /// **The invited account answers.** `accept`: it joins, giving
    /// `contribution` bytes of its own room (checked against `room`, as any
    /// raise is); otherwise the invitation is declined. Only the invited
    /// account answers its own invitation.
    pub fn answer_clan_invite(
        &self,
        clan: &str,
        principal: &str,
        accept: bool,
        contribution: u64,
        room: AccountRoom,
    ) -> Result<Clan> {
        let at = now_ms();
        self.doc.update(|doc| {
            let mut row = Self::visible_clan(doc, clan, principal)?;
            if row.role_of(principal) != Some(ClanRole::Invited) {
                return Err(refused(
                    clan,
                    ClanRefusal::Precondition,
                    format!("{principal} has no invitation to clan {clan} to answer"),
                ));
            }
            row.invites.retain(|i| i.principal != principal);
            if accept {
                // The rules again, at the moment of joining: an account may
                // have become an employer's since it was invited, never an
                // employee (that is only ever a NEW principal), but it may
                // have been locked out.
                Self::ordinary_account(doc, clan, principal, "join a clan")?;
                Self::fits_in_room(doc, clan, principal, contribution, room)?;
                row.members.push(ClanMember {
                    principal: principal.to_owned(),
                    admin: false,
                    contribution_bytes: contribution,
                    joined_at: at,
                });
            } else {
                Self::unindex_clan(doc, principal, clan)?;
            }
            doc.clans.insert(clan.to_owned(), row.clone())?;
            Ok(row)
        })
    }

    /// **An admin withdraws an invitation** it (or another admin) made.
    pub fn withdraw_clan_invite(&self, clan: &str, by: &str, principal: &str) -> Result<Clan> {
        self.doc.update(|doc| {
            let mut row = Self::clan_for_admin(doc, clan, by, "withdraw an invitation")?;
            if row.role_of(principal) != Some(ClanRole::Invited) {
                return Err(refused(
                    clan,
                    ClanRefusal::Precondition,
                    format!("{principal} has no invitation to clan {clan}"),
                ));
            }
            row.invites.retain(|i| i.principal != principal);
            Self::unindex_clan(doc, principal, clan)?;
            doc.clans.insert(clan.to_owned(), row.clone())?;
            Ok(row)
        })
    }

    /// **A member sets its own contribution** to `bytes`. Up: out of its own
    /// room (`room`). Down: no lower than what the clan stores (`used`) leaves
    /// room for — `None` (not measured) refuses a lowering rather than
    /// guessing it is free.
    pub fn set_clan_contribution(
        &self,
        clan: &str,
        member: &str,
        bytes: u64,
        room: AccountRoom,
        used: Option<u64>,
        lapsed: &[String],
    ) -> Result<Clan> {
        self.doc.update(|doc| {
            let mut row = Self::visible_clan(doc, clan, member)?;
            Self::refuse_gated(doc, clan, member)?;
            let Some(now) = row.member(member).map(|m| m.contribution_bytes) else {
                return Err(refused(
                    clan,
                    ClanRefusal::Denied,
                    format!("{member} is not a member of clan {clan}: only a member gives it room"),
                ));
            };
            if bytes > now {
                Self::fits_in_room(doc, clan, member, bytes, room)?;
            } else if bytes < now {
                Self::above_floor(
                    &row,
                    clan,
                    member,
                    bytes,
                    used,
                    lapsed,
                    "lower its contribution",
                )?;
            }
            for m in &mut row.members {
                if m.principal == member {
                    m.contribution_bytes = bytes;
                }
            }
            doc.clans.insert(clan.to_owned(), row.clone())?;
            Ok(row)
        })
    }

    /// **A member leaves**: its contribution goes back to its own account.
    /// Refused while the clan uses it (`used` past what the others give), and
    /// for the clan's last admin.
    pub fn leave_clan(
        &self,
        clan: &str,
        member: &str,
        used: Option<u64>,
        lapsed: &[String],
    ) -> Result<Clan> {
        self.doc.update(|doc| {
            let mut row = Self::visible_clan(doc, clan, member)?;
            Self::refuse_gated(doc, clan, member)?;
            let Some(me) = row.member(member).cloned() else {
                return Err(refused(clan, ClanRefusal::Precondition, format!("{member} is not a member of clan {clan}")));
            };
            if me.admin && row.admins() == 1 {
                return Err(refused(
                    clan,
                    ClanRefusal::Precondition,
                    format!(
                        "{member} is clan {clan}'s last admin, and the last admin cannot leave: make another \
                         member admin first, or dissolve the clan"
                    ),
                ));
            }
            Self::above_floor(&row, clan, member, 0, used, lapsed, "leave")?;
            row.members.retain(|m| m.principal != member);
            Self::unindex_clan(doc, member, clan)?;
            doc.clans.insert(clan.to_owned(), row.clone())?;
            Ok(row)
        })
    }

    /// **An admin removes a member** — not itself (that is leaving), and not
    /// while the clan uses the member's contribution; its contribution goes
    /// back to its own account.
    pub fn remove_from_clan(
        &self,
        clan: &str,
        by: &str,
        member: &str,
        used: Option<u64>,
        lapsed: &[String],
    ) -> Result<Clan> {
        self.doc.update(|doc| {
            let mut row = Self::clan_for_admin(doc, clan, by, "remove a member")?;
            if member == by {
                return Err(refused(
                    clan,
                    ClanRefusal::Invalid,
                    format!("{by} cannot remove itself from clan {clan}: that is leaving"),
                ));
            }
            if row.member(member).is_none() {
                return Err(refused(
                    clan,
                    ClanRefusal::Precondition,
                    format!("{member} is not a member of clan {clan}"),
                ));
            }
            Self::above_floor(&row, clan, member, 0, used, lapsed, "be removed")?;
            row.members.retain(|m| m.principal != member);
            Self::unindex_clan(doc, member, clan)?;
            doc.clans.insert(clan.to_owned(), row.clone())?;
            Ok(row)
        })
    }

    /// **An admin makes a member admin, or not.** The last admin is never
    /// made a plain member.
    pub fn set_clan_admin(&self, clan: &str, by: &str, member: &str, admin: bool) -> Result<Clan> {
        self.doc.update(|doc| {
            let mut row = Self::clan_for_admin(doc, clan, by, "change who administers it")?;
            let Some(was) = row.member(member).map(|m| m.admin) else {
                return Err(refused(
                    clan,
                    ClanRefusal::Precondition,
                    format!("{member} is not a member of clan {clan}"),
                ));
            };
            if was && !admin && row.admins() == 1 {
                return Err(refused(
                    clan,
                    ClanRefusal::Precondition,
                    format!(
                        "{member} is clan {clan}'s last admin: make another member admin first"
                    ),
                ));
            }
            for m in &mut row.members {
                if m.principal == member {
                    m.admin = admin;
                }
            }
            doc.clans.insert(clan.to_owned(), row.clone())?;
            Ok(row)
        })
    }

    /// **An admin dissolves the clan**: every member's contribution goes
    /// back, every invitation goes. The caller checks first that the clan's
    /// namespace holds nothing, and removes the namespace after.
    pub fn dissolve_clan(&self, clan: &str, by: &str) -> Result<Clan> {
        self.doc.update(|doc| {
            let row = Self::clan_for_admin(doc, clan, by, "dissolve it")?;
            for m in &row.members {
                Self::unindex_clan(doc, &m.principal, clan)?;
            }
            for i in &row.invites {
                Self::unindex_clan(doc, &i.principal, clan)?;
            }
            doc.clans.remove(clan)?;
            Ok(row)
        })
    }

    // ── moving a repository out (round 2) ──────────────────────────────────

    /// **The clan's admin offers `repo` to account `to`.** The plane checks
    /// first that the repository is the clan's; this checks who may offer and
    /// to whom: an account (a person's, a principal of its own — never a
    /// machine, a group, an employee or another clan), one open offer per
    /// repository.
    pub fn offer_clan_repo(&self, clan: &str, by: &str, repo: &str, to: &str) -> Result<Clan> {
        let at = now_ms();
        self.doc.update(|doc| {
            let mut row = Self::clan_for_admin(doc, clan, by, "move a repository out of it")?;
            if to == clan || doc.clans.contains_key(to)? {
                return Err(refused(
                    clan,
                    ClanRefusal::Invalid,
                    format!("{to} is a clan: a repository moves out of a clan into an account"),
                ));
            }
            let Some(account) = doc.principals.get(to)? else {
                return Err(refused(
                    clan,
                    ClanRefusal::NotFound,
                    format!("there is no account {to}"),
                ));
            };
            if account.kind != PrincipalKind::User || doc.employers.contains_key(to)? {
                return Err(refused(
                    clan,
                    ClanRefusal::Precondition,
                    format!("{to} holds no account of its own to move {clan}/{repo} into"),
                ));
            }
            if let Some(open) = row.offer_of(repo) {
                return Err(refused(
                    clan,
                    ClanRefusal::Precondition,
                    format!(
                        "{clan}/{repo} is already offered to {}; withdraw that offer first",
                        open.to
                    ),
                ));
            }
            row.offers.push(ClanRepoOffer {
                repo: repo.to_owned(),
                to: to.to_owned(),
                by: by.to_owned(),
                at,
            });
            doc.clans.insert(clan.to_owned(), row.clone())?;
            Ok(row)
        })
    }

    /// The open offer of `clan/repo`, if any.
    pub fn clan_repo_offer(&self, clan: &str, repo: &str) -> Result<Option<ClanRepoOffer>> {
        Ok(self.clan(clan)?.and_then(|row| row.offer_of(repo).cloned()))
    }

    /// **Every open offer to account `to`**, as `(clan, offer)`, by clan. A
    /// listing: it walks the clans.
    pub fn clan_offers_to(&self, to: &str) -> Result<Vec<(String, ClanRepoOffer)>> {
        let doc = self.doc.load()?;
        if doc.clans.is_empty() {
            return Ok(Vec::new());
        }
        let mut out = Vec::new();
        for (name, row) in doc.clans.entries()? {
            for offer in row.offers {
                if offer.to == to {
                    out.push((name.clone(), offer));
                }
            }
        }
        Ok(out)
    }

    /// **The clan's admin withdraws an offer.**
    pub fn withdraw_clan_repo_offer(&self, clan: &str, by: &str, repo: &str) -> Result<Clan> {
        self.doc.update(|doc| {
            let mut row = Self::clan_for_admin(doc, clan, by, "withdraw an offer")?;
            if row.offer_of(repo).is_none() {
                return Err(refused(
                    clan,
                    ClanRefusal::Precondition,
                    format!("{clan}/{repo} is offered to nobody"),
                ));
            }
            row.offers.retain(|o| o.repo != repo);
            doc.clans.insert(clan.to_owned(), row.clone())?;
            Ok(row)
        })
    }

    /// **The offer is answered** (declined, or taken once the move is done):
    /// it goes, and the offer as it was comes back. Who may answer — the
    /// offered account's admin — is the caller's to have asked; this only
    /// checks the offer is the one named.
    pub fn close_clan_repo_offer(&self, clan: &str, repo: &str, to: &str) -> Result<ClanRepoOffer> {
        self.doc.update(|doc| {
            let mut row = doc.clans.get(clan)?.ok_or_else(|| {
                refused(
                    clan,
                    ClanRefusal::NotFound,
                    format!("there is no clan {clan}"),
                )
            })?;
            let Some(offer) = row.offer_of(repo).filter(|o| o.to == to).cloned() else {
                return Err(refused(
                    clan,
                    ClanRefusal::NotFound,
                    format!("{clan}/{repo} is not offered to {to}"),
                ));
            };
            row.offers.retain(|o| o.repo != repo);
            doc.clans.insert(clan.to_owned(), row)?;
            Ok(offer)
        })
    }

    // ── inside the compare-and-swap ────────────────────────────────────────

    /// **A principal that is going** (`remove_principal`): out of every clan
    /// it is in or invited to, its contribution with it. A clan it was the
    /// last admin of is handed to its longest-standing member, so no clan is
    /// left with nobody to administer it.
    pub(super) fn forget_clan_member(doc: &mut AuthDoc, id: &str) -> Result<()> {
        if doc.clan_index.is_empty() {
            return Ok(());
        }
        for name in doc.clan_index.remove(id)?.unwrap_or_default() {
            let Some(mut row) = doc.clans.get(&name)? else {
                continue;
            };
            row.members.retain(|m| m.principal != id);
            row.invites.retain(|i| i.principal != id);
            if row.admins() == 0 {
                if let Some(first) = row.members.iter_mut().min_by_key(|m| m.joined_at) {
                    first.admin = true;
                }
            }
            doc.clans.insert(name, row)?;
        }
        Ok(())
    }

    /// The clan `clan`, for a verb only its admin `by` may make.
    fn clan_for_admin(doc: &AuthDoc, clan: &str, by: &str, what: &str) -> Result<Clan> {
        let row = Self::visible_clan(doc, clan, by)?;
        Self::refuse_gated(doc, clan, by)?;
        if row.role_of(by) != Some(ClanRole::Admin) {
            return Err(refused(
                clan,
                ClanRefusal::Denied,
                format!("only clan {clan}'s admin may {what}; {by} is not its admin"),
            ));
        }
        Ok(row)
    }

    /// The clan `clan` as `who` may know it: a member's or an invited
    /// account's. To anybody else it does not exist — the same answer as a
    /// clan that does not, so a name is never confirmed to an outsider.
    fn visible_clan(doc: &AuthDoc, clan: &str, who: &str) -> Result<Clan> {
        match doc.clans.get(clan)? {
            Some(row) if row.role_of(who).is_some() => Ok(row),
            _ => Err(refused(
                clan,
                ClanRefusal::NotFound,
                format!("there is no clan {clan} you are in or invited to"),
            )),
        }
    }

    /// **Only an ORDINARY account is in a clan**: a person (a user) that
    /// exists, that is no employee — an enterprise account's people belong to
    /// their account, whose admin's rules win — and that is not locked out.
    fn ordinary_account(doc: &AuthDoc, clan: &str, id: &str, what: &str) -> Result<()> {
        let Some(principal) = doc.principals.get(id)? else {
            return Err(refused(
                clan,
                ClanRefusal::NotFound,
                format!("there is no account {id}"),
            ));
        };
        if principal.kind != PrincipalKind::User {
            return Err(refused(
                clan,
                ClanRefusal::Precondition,
                format!(
                    "{id} is a {:?}, and only a person's own account may {what}",
                    principal.kind
                ),
            ));
        }
        if let Some(employer) = doc.employers.get(id)? {
            return Err(refused(
                clan,
                ClanRefusal::Precondition,
                format!(
                    "{id} is an employee of {employer}: an enterprise account's people may not {what} — \
                     their account admin's rules win"
                ),
            ));
        }
        Self::refuse_gated(doc, clan, id)
    }

    /// A locked-out account acts nowhere — not in a clan either.
    fn refuse_gated(doc: &AuthDoc, clan: &str, id: &str) -> Result<()> {
        if Self::gated(doc, id)? {
            return Err(refused(
                clan,
                ClanRefusal::Precondition,
                format!("account {id} is locked"),
            ));
        }
        Ok(())
    }

    /// **Does `bytes` for `clan` fit in `account`'s own room?** What the
    /// account stores, what its employees and open seats hold and what it
    /// gives its OTHER clans are taken; `bytes` must fit in the rest. An
    /// uncapped account fits anything.
    fn fits_in_room(
        doc: &AuthDoc,
        clan: &str,
        account: &str,
        bytes: u64,
        room: AccountRoom,
    ) -> Result<()> {
        let Some(cap) = room.cap else { return Ok(()) };
        let committed = room
            .stored
            .saturating_add(Self::promised_by(doc, account)?)
            .saturating_add(Self::contributed_in(doc, account, Some(clan))?);
        let free = cap.saturating_sub(committed);
        if bytes > free {
            return Err(refused(
                clan,
                ClanRefusal::Precondition,
                format!(
                    "{account} has {} of its own room unused, and {} is asked for clan {clan}: {} short. \
                     A clan's room is its members' own; add room to the account, or give less",
                    human_bytes(free),
                    human_bytes(bytes),
                    human_bytes(bytes - free)
                ),
            ));
        }
        Ok(())
    }

    /// **Is the clan's room, with `member` giving `bytes`, still at least what
    /// it stores?** `used` unmeasured refuses.
    fn above_floor(
        row: &Clan,
        clan: &str,
        member: &str,
        bytes: u64,
        used: Option<u64>,
        lapsed: &[String],
        what: &str,
    ) -> Result<()> {
        let Some(used) = used else {
            return Err(refused(
                clan,
                ClanRefusal::Precondition,
                format!(
                    "what clan {clan} stores could not be measured, so {member} may not {what} now: a room \
                     that is in use is never given back unseen"
                ),
            ));
        };
        let floor = row.floor_in_force(member, used, lapsed);
        if bytes < floor {
            let own = row.member(member).map_or(0, |m| m.contribution_bytes);
            return Err(refused(
                clan,
                ClanRefusal::Precondition,
                format!(
                    "clan {clan} stores {} in its room of {}, so {member}'s {} is in use: it may go down to {} \
                     and no lower, and {member} may not {what} until the clan stores less",
                    human_bytes(used),
                    human_bytes(row.room_in_force(lapsed)),
                    human_bytes(own),
                    human_bytes(floor)
                ),
            ));
        }
        Ok(())
    }

    fn index_clan(doc: &mut AuthDoc, principal: &str, clan: &str) -> Result<()> {
        let mut names = doc.clan_index.get(principal)?.unwrap_or_default();
        if !names.iter().any(|n| n == clan) {
            names.push(clan.to_owned());
            names.sort();
        }
        doc.clan_index.insert(principal.to_owned(), names)?;
        Ok(())
    }

    fn unindex_clan(doc: &mut AuthDoc, principal: &str, clan: &str) -> Result<()> {
        let Some(mut names) = doc.clan_index.get(principal)? else {
            return Ok(());
        };
        names.retain(|n| n != clan);
        if names.is_empty() {
            doc.clan_index.remove(principal)?;
        } else {
            doc.clan_index.insert(principal.to_owned(), names)?;
        }
        Ok(())
    }
}