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

nordisk/gunnar

crates/gunnar-server/src/entitlement.rs

//! **The metered appliance: verify, log, store, swap — and answer the front
//! door in nanoseconds.** Compiled only with the `metered` feature.
//!
//! ```text
//!   monetize ──Entitlement.Set(fact, sig)──▶ EntitlementSlot::set
//!                                              │ 1. verify_fact (monetize-embed's canonical form, monetize's public key)
//!                                              │ 1b. check_not_replayed                     (newer than the fact this box holds)
//!                                              │ 2. append one line to attestations.jsonl   (the durable, signed-tree-head'd record)
//!                                              │ 3. EntitlementStore::set                   (the control document, so a reboot loads it)
//!                                              │ 4. EntitlementCache::push                  (the arc-swap snapshot the front door reads)
//!                                              │ 5. caps["cache_bytes"] → Namespace::explode_budget_bytes
//!                                              ▼
//!   authorise_service ──PlanGate::verdict──▶ EntitlementCache::allows   (one atomic load, one map lookup)
//! ```
//!
//! The order in `set` is the correctness argument: a fact is logged before it
//! is enforced, so there is no window in which the box acts on a plan its own
//! log does not show; and nothing at all happens to a fact that does not
//! verify — not a log line, not a store write, not a swap. Step 1 refusing is
//! the whole security property, and it is `monetize_embed::signing::verify_fact`
//! over `monetize_embed::signing::fact_message`: gunnar writes no canonical
//! form of its own, so there is no second encoding for a forger to aim at.
//!
//! **Step 1b is there because step 1 is not enough.** A signature says the fact
//! is monetize's; it does not say WHEN. Until 2026-09-17 an `EntitlementFact`
//! was replayable for ever — capture a `Paid` fact, wait for the tenant to
//! lapse, push the captured bytes back, and every check above passed, because
//! they were all real. `paid_until` cannot tell a replayed `Paid` from a
//! legitimate `Suspended` at the same date; that is what a lapse IS. So the
//! fact now carries `issued_unix_ms`, signed, and
//! [`EntitlementSlot::check_not_replayed`] refuses one that is not newer than
//! the fact this box holds. It runs under the same lock as the write, and
//! before the log, so a refused replay leaves no trace of having been tried
//! anywhere but the refusal line.
//!
//! # Boot
//!
//! [`EntitlementSlot::open`] loads every stored fact and pushes it into the
//! cache **through the same verification**. A fact that was accepted under a
//! key this box no longer trusts, or a control document somebody edited, is
//! refused at boot and logged by namespace; that namespace is then Free until
//! monetize pushes a fact that verifies. Refusing to boot would be the outage
//! that ends the relationship (the metered plan); serving a tenant for free until the
//! next signed fact arrives is the cheap failure.
//!
//! # What is NOT here
//!
//! No clock arithmetic: the state is monetize's verdict, stored, and the
//! ladder moves when monetize pushes the next fact. No network: monetize
//! reaches this box, never the reverse, and `git push` never waits on anyone's
//! uptime. No payment code, ever (the metered plan).

use std::sync::{Arc, Mutex};

use gunnar_common::monetize_key::MonetizePubkey;
use gunnar_core::{Entitlement, EntitlementState, EntitlementStore};
use gunnar_proto::EntitlementFact as ProtoFact;
use monetize_embed::{Action, EntitlementCache, State, TenantId, Verdict};

use crate::attest::{AttestationLog, ENTITLEMENT_KEY};
use crate::stores::ServerStores;

/// **The explode budget of a namespace the front door is refusing: one byte,
/// and not zero.**
///
/// # `0` is UNLIMITED, at every layer, and that is the trap this names
///
/// `Namespace::explode_budget_bytes` is `Option<u64>` where `None` is "the
/// server's default" and `Some(0)` is "unlimited — a deliberate answer, not an
/// absent one" (its own doc). The engine agrees: `ExplodedCache::over_budget`
/// is `self.budget != 0 && ...` and `evict_to_budget` returns early on
/// `budget == 0`. So writing `Some(0)` for a suspended tenant would hand them
/// an UNBOUNDED cache — the exact inverse of the intent, silently, and the
/// symptom would be a suspended namespace growing on a full disk.
///
/// # The type cannot express "no cache", and this is said rather than faked
///
/// There is no value of `Option<u64>` that means zero bytes of cache, and
/// `1` does not become one by being small. What `1` actually buys, read out of
/// `exploded_cache.rs` rather than assumed:
///
/// * `segment_for(1)` = `(1/16).clamp(min(16 MiB, 0).max(64 KiB), 1 GiB)` =
///   **64 KiB**, so the lazy pool rotates every 64 KiB;
/// * `evict_to_budget` drops the cheapest segment while more than one exists,
///   so the pool converges on its single live segment — ~64 KiB per store, not
///   0;
/// * the **eager** pool is never evicted by either path, by design: it is the
///   commit-and-tree graph that `graph`, `reach` and a tag peel read, not a
///   cache. A suspended namespace keeps it.
///
/// So: a suspended tenant is held to ~64 KiB of lazily-exploded objects per
/// store plus their graph pool. That is the floor this appliance can express,
/// it is two to five orders of magnitude under a real budget, and calling it
/// "no cache" in a changelog would be the lie. Making a true zero expressible
/// means a new variant on the field — a schema change to a persisted control
/// document, which is not something a cache-reclaim change gets to do on its
/// way past.
const NO_CACHE_BUDGET_BYTES: u64 = 1;

/// What the metered server is started with: the key that signs facts and
/// where a tenant goes to pay.
#[derive(Clone, Debug)]
pub struct MeteredConfig {
    /// monetize-server's Ed25519 verifying key, 32 bytes. Either spelled
    /// inline on `--monetize-pubkey` as its 64 hex characters, or read from
    /// the file that flag names — which holds the raw 32 bytes, or their 64 hex
    /// characters, or `ed25519:<base64>`. See [`MeteredConfig::load`].
    ///
    /// `None` while the key is AWAITED: the medium says `fed`
    /// ([`MeteredConfig::awaiting_feed`]) and the installer has not fed it yet.
    /// The key then arrives LIVE ([`EntitlementSlot::set_key`]), never by a
    /// restart (owner 2026-09-25: avoid restarts).
    pub monetize_pubkey: Option<[u8; 32]>,
    /// The pay page, with `{tenant}` where the namespace goes:
    /// `https://gunnar.rs/billing/{tenant}`. A template without the
    /// placeholder gets `/{tenant}` appended, so the URL always names the
    /// tenant.
    pub billing_url: String,
    /// `--monetize-vital` (default `true`): a boot that refuses stored facts raises
    /// `entitlement.key_mismatch` and reports `maintenance`; `false` only logs.
    pub vital: bool,
}

impl MeteredConfig {
    /// Resolve `key` — inline, or the file it names — and pair it with
    /// `billing_url`.
    ///
    /// An inline key was already decoded when the flag parsed, so there is
    /// nothing to read and nothing that can fail here: that is the whole point
    /// of it on an appliance whose only writable channel is the cmdline.
    ///
    /// # `billing_url` is `Option`, and `None` is a REFUSAL
    ///
    /// It used to be `&str`, and the caller in `gunnar_server::dispatch`
    /// filled a missing `--billing-url` in with `https://gunnar.rs/billing` —
    /// Vetra's own page, hardcoded as the OPEN product's default. Anybody else
    /// building a metered gunnar and forgetting the flag sent their lapsed
    /// tenants to somebody else's payment page, and nothing anywhere said so:
    /// the box booted, the plans enforced correctly, and the only symptom was
    /// a URL at the end of a refusal that a tenant would read as their own
    /// supplier's.
    ///
    /// The alternative considered was tolerating the absence — refusals would
    /// simply carry no link. It was rejected because of what this URL is FOR.
    /// [`EntitlementSlot::open`] logs it at boot, every Grace warning ends in
    /// it, and `PlanGate`'s refusal is a sentence whose last clause is the way
    /// out; a metered appliance whose refusals dead-end is a support ticket
    /// per tenant, silently, forever. An operator who reads one line at boot
    /// and adds one flag is the cheap failure. So a `metered` build handed
    /// `--monetize-pubkey` and no `--billing-url` stops, in the same voice as
    /// the open build's refusal of `--monetize-pubkey` itself.
    pub fn load(key: &MonetizePubkey, billing_url: Option<&str>) -> anyhow::Result<Self> {
        let billing_url = billing_url.ok_or_else(|| {
            anyhow::anyhow!(
                "--monetize-pubkey was given, but --billing-url was not, and this build has no \
                 default for it: it would have to invent the page your lapsed tenants are sent \
                 to. Every refusal and every Grace warning ends in that link, so a metered \
                 appliance without one refuses tenants with no way out. Pass \
                 --billing-url <URL> (or GUNNAR_BILLING_URL), with `{{tenant}}` where the \
                 namespace goes."
            )
        })?;
        let monetize_pubkey = read_pubkey(key)?;
        Ok(MeteredConfig {
            monetize_pubkey: Some(monetize_pubkey),
            billing_url: billing_template(billing_url),
            vital: true,
        })
    }

    /// **A metered box whose key the installer feeds** (`--monetize-pubkey fed`
    /// with no fed key yet — the first boot, before the join). The slot opens
    /// KEYLESS: every namespace is Free and `Entitlement.Set` refuses every fact
    /// until the key arrives, and then it applies in place
    /// ([`EntitlementSlot::set_key`]). `--billing-url` is refused missing here
    /// exactly as in [`MeteredConfig::load`]: a key that arrives later would
    /// otherwise have no pay page to enforce with.
    pub fn awaiting_feed(billing_url: Option<&str>) -> anyhow::Result<Self> {
        let billing_url = billing_url.ok_or_else(|| {
            anyhow::anyhow!(
                "--monetize-pubkey {} (the installer feeds the key) was given, but --billing-url was not, and this \
                 build has no default for it: the fed key could never be enforced with a pay page. Pass \
                 --billing-url <URL> (or GUNNAR_BILLING_URL), with `{{tenant}}` where the namespace goes.",
                gunnar_common::monetize_key::FED
            )
        })?;
        Ok(MeteredConfig {
            monetize_pubkey: None,
            billing_url: billing_template(billing_url),
            vital: true,
        })
    }
}

/// The 32 key bytes `key` names: inline, or read out of the file it names.
/// Split out of [`MeteredConfig::load`] so the start-up can compare the
/// medium's key with the installer's fed one (`crate::feed`) before loading.
pub fn read_pubkey(key: &MonetizePubkey) -> anyhow::Result<[u8; 32]> {
    Ok(match key {
            MonetizePubkey::Fed => anyhow::bail!(
                "--monetize-pubkey {} names no key: the installer's door feeds it (gunnar-server resolves \
                 `fed` against <data>/pairing/joined.toml before it ever reads a key)",
                gunnar_common::monetize_key::FED
            ),
            MonetizePubkey::Literal(k) => *k,
            MonetizePubkey::Path(path) => {
                let bytes = std::fs::read(path).map_err(|err| {
                    anyhow::anyhow!("reading the monetize public key {}: {err}", path.display())
                })?;
                parse_pubkey(&bytes).ok_or_else(|| {
                    anyhow::anyhow!(
                        "{} is not an Ed25519 public key: expected 32 raw bytes, 64 hex \
                         characters, or `ed25519:<base64>` ({} byte(s) read)",
                        path.display(),
                        bytes.len()
                    )
                })?
            }
    })
}

/// `{tenant}` in the template, or appended: the refusal line must always end
/// in a URL that names the tenant, because that is the link a lapsed
/// customer clicks.
fn billing_template(url: &str) -> String {
    if url.contains("{tenant}") {
        url.to_owned()
    } else {
        format!("{}/{{tenant}}", url.trim_end_matches('/'))
    }
}

/// The three spellings a key file may use. Raw bytes first, because a file
/// that is exactly 32 bytes long is a key and not text.
fn parse_pubkey(bytes: &[u8]) -> Option<[u8; 32]> {
    if bytes.len() == 32 {
        return bytes.try_into().ok();
    }
    let text = std::str::from_utf8(bytes).ok()?.trim();
    if let Some(b64) = text.strip_prefix("ed25519:") {
        return decode_base64(b64.trim())?.try_into().ok();
    }
    // The hex spelling is ONE decoder, shared with the inline `--monetize-pubkey`
    // arm in `gunnar_common::monetize_key` — a second copy here is a second
    // place for a byte order to drift.
    gunnar_common::monetize_key::parse_hex32(text).ok()
}

/// Standard base64 with or without padding. Written out rather than linked,
/// because a decoder for one key file is not worth a crate in PID 1.
fn decode_base64(text: &str) -> Option<Vec<u8>> {
    fn value(c: u8) -> Option<u32> {
        Some(match c {
            b'A'..=b'Z' => (c - b'A') as u32,
            b'a'..=b'z' => (c - b'a') as u32 + 26,
            b'0'..=b'9' => (c - b'0') as u32 + 52,
            b'+' | b'-' => 62,
            b'/' | b'_' => 63,
            _ => return None,
        })
    }
    let mut out = Vec::with_capacity(text.len() * 3 / 4);
    let mut acc = 0u32;
    let mut bits = 0u32;
    for c in text.bytes().filter(|c| *c != b'=') {
        acc = (acc << 6) | value(c)?;
        bits += 6;
        if bits >= 8 {
            bits -= 8;
            out.push((acc >> bits) as u8);
            acc &= (1 << bits) - 1;
        }
    }
    Some(out)
}

/// What [`EntitlementSlot::restate`] did: facts applied, and facts this box
/// already held as new or newer.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct Restated {
    pub applied: usize,
    pub held: usize,
}

/// Why a fact was not accepted. Every variant names its reason, and the
/// gRPC layer maps the first to `PERMISSION_DENIED` and the rest to
/// `INVALID_ARGUMENT` / `FAILED_PRECONDITION` — a forged fact is told apart
/// from a malformed one and from a box that cannot store one.
#[derive(Debug, thiserror::Error)]
pub enum SetRefused {
    /// The signature does not verify under the configured monetize key.
    /// **Nothing was logged, stored or swapped.**
    #[error("the fact for {namespace} does not verify under the configured monetize key ({why}); refused, nothing moved")]
    Forged { namespace: String, why: String },
    /// The fact's shape is wrong (a state gunnar cannot name).
    #[error("invalid fact: {0}")]
    Malformed(String),
    /// The fact verified but could not be logged or stored. It is NOT
    /// enforced either: a plan the log does not show is not a plan.
    #[error("the fact for {namespace} verified but could not be recorded: {why}")]
    NotRecorded { namespace: String, why: String },
    /// **A fact this box has already moved past.** The signature is monetize's
    /// own and it verifies; the fact is simply not newer than the one stored
    /// for this namespace, so accepting it would let a captured `Paid` fact
    /// restore a tenant who has since lapsed. **Nothing was logged, stored or
    /// swapped.**
    ///
    /// THE WORDS SAY NOTHING THE CALLER DID NOT BRING. Not the issue time this
    /// box holds, not the state, not whether the namespace is stamped at all —
    /// the numbers go to gunnar's own log and no further. `Entitlement.Set` is
    /// server-admin and `Entitlement.Get` is only server-read, so a caller who
    /// can reach this can usually read that anyway; the refusal is kept blind
    /// so that the day those grants are split, this does not quietly become the
    /// way to read one tenant's billing state through another's push.
    #[error("the fact for {namespace} is not newer than the one this server already holds for it; refused as a replay, nothing moved")]
    Replayed { namespace: String },
    /// **The fact carries no issue time** (stinky row 12): nothing tells it
    /// from a replay of any fact ever signed for the namespace, so it is
    /// refused whatever the box holds.
    #[error("the fact for {namespace} carries no issue time (issued_unix_ms); an unstamped fact cannot be told from a replay and is refused, nothing moved")]
    Unstamped { namespace: String },
    /// **No monetize key yet**: the medium said `fed` and the installer has
    /// not fed it. Nothing can be verified, so nothing is taken — once the key
    /// arrives ([`EntitlementSlot::set_key`]) the same push is accepted with no
    /// restart.
    #[error("the fact for {namespace} cannot be checked: this box has no monetize key yet (the installer feeds it); refused, nothing moved")]
    NoKey { namespace: String },
    /// **c-stale-suspend**: a newer-stamped fact that takes back paid time the
    /// box holds, from another payment — a monetize restored from an OLDER
    /// backup lost the renewal this box holds. Worded "already holds" so
    /// monetize's re-push files it as held, not as a refused cap.
    #[error("the fact for {namespace} is behind this box: it already holds a verdict paid until {held_until} by `{held_source}`, and this one pays only until {offered_until} by `{offered_source}` — a monetize restored from an older backup; refused, nothing moved")]
    Behind {
        namespace: String,
        held_until: u64,
        held_source: String,
        offered_until: u64,
        offered_source: String,
    },
}

/// The counts the console band and `Admin.Stats` show — the band's own type,
/// so the two surfaces cannot disagree about what is counted.
pub use crate::dashboard::TenantCounts;

/// What `EntitlementSlot::open` refused, kept for the wire.
#[derive(Debug, Clone, Default)]
struct Refusal {
    namespaces: Vec<String>,
    at_unix_ms: i64,
    reason: String,
}

fn now_unix_ms() -> i64 {
    std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|d| d.as_millis() as i64)
        .unwrap_or(0)
}

/// The `Admin.Health` alarm a vital box raises while its stored facts do not verify.
pub const KEY_MISMATCH_ALARM: &str = "entitlement.key_mismatch";

/// **The verified cache, swappable** — `None` while the monetize key is awaited
/// (the medium said `fed`, the installer has not fed it yet). The key arrives
/// LIVE ([`EntitlementSlot::set_key`]): a new cache is built under it, loaded
/// with every stored fact, and swapped in, so no restart is needed. A read is
/// an `Arc` clone under an uncontended read lock; the cache's own snapshot
/// swap underneath is unchanged.
///
/// Keyless, it answers what a box with no slot answered: every namespace Free
/// (`allows` is Allow, no notice, no fact).
struct Cache(std::sync::RwLock<Option<Arc<EntitlementCache>>>);

impl Cache {
    fn current(&self) -> Option<Arc<EntitlementCache>> {
        self.0.read().unwrap_or_else(|p| p.into_inner()).clone()
    }
    fn swap_in(&self, cache: Arc<EntitlementCache>) {
        *self.0.write().unwrap_or_else(|p| p.into_inner()) = Some(cache);
    }
    fn len(&self) -> usize {
        self.current().map_or(0, |c| c.len())
    }
    fn fact(&self, tenant: &TenantId) -> Option<monetize_embed::EntitlementFact> {
        self.current()?.fact(tenant)
    }
    fn allows(&self, tenant: &TenantId, action: Action) -> Verdict {
        match self.current() {
            Some(c) => c.allows(tenant, action),
            None => Verdict::Allow,
        }
    }
    fn notice(&self, tenant: &TenantId) -> Option<String> {
        self.current()?.notice(tenant)
    }
}

/// The live entitlement state of this server, shared by the control plane
/// (which writes it) and the front doors (which read it).
pub struct EntitlementSlot {
    cache: Cache,
    store: EntitlementStore,
    log: AttestationLog,
    stores: Arc<ServerStores>,
    /// The pay page template: the medium's `--billing-url` until the installer's door feeds
    /// the SITE's own ([`EntitlementSlot::set_billing_url`], UW-6), then that one.
    billing_url: std::sync::RwLock<String>,
    /// The stored facts that did NOT verify when this slot was opened: which
    /// namespaces, when, and why. Set once, at boot, and never moved: a fact
    /// that verifies later arrives through `set`. See
    /// [`EntitlementSlot::refused_at_boot`] and [`EntitlementSlot::refusal`].
    refused: std::sync::RwLock<Refusal>,
    /// Serialises `set`: count-and-append on the log must not interleave with
    /// another `set`, and neither must store-then-swap. Never held across a
    /// read — the front door reads the cache, which is lock-free.
    write: Mutex<()>,
    /// `--monetize-vital`: a boot refusal is an alarm and `maintenance` (true) or a log line (false).
    vital: bool,
    /// A fact has VERIFIED since this boot (`set` accepted one). While the boot refused facts and
    /// this is false, a vital box is in maintenance; the first verifying fact clears it, because it
    /// proves monetize and this box now agree on the key.
    verified_since_boot: std::sync::atomic::AtomicBool,
    /// **This estate SELLS room** (the feed's `sells_room`; gunnar-grpc `selling`): a person's
    /// own account with no fact may push nothing here until its fact lands. Shared with the
    /// gRPC context ([`EntitlementSlot::sells_room_flag`]).
    sells_room: Arc<std::sync::atomic::AtomicBool>,
}

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

impl EntitlementSlot {
    /// Open the slot over the server's control store and attestation log,
    /// loading — and re-verifying — every stored fact. See the module docs
    /// for what a fact that no longer verifies does at boot.
    pub fn open(stores: Arc<ServerStores>, config: &MeteredConfig) -> anyhow::Result<Arc<Self>> {
        let store = EntitlementStore::new(Arc::clone(stores.control_store()))?;
        let log = AttestationLog::under(stores.root());
        let slot = EntitlementSlot {
            cache: Cache(std::sync::RwLock::new(None)),
            store,
            log,
            stores,
            billing_url: std::sync::RwLock::new(config.billing_url.clone()),
            refused: std::sync::RwLock::new(Refusal::default()),
            write: Mutex::new(()),
            vital: config.vital,
            verified_since_boot: std::sync::atomic::AtomicBool::new(false),
            sells_room: Arc::new(std::sync::atomic::AtomicBool::new(false)),
        };
        match &config.monetize_pubkey {
            Some(key) => slot.load_under(key)?,
            None => tracing::warn!(
                target: "gunnar::entitlement",
                event = "entitlement.awaiting_key",
                billing = %config.billing_url,
                "metered: the monetize key is FED by the installer and has not arrived — every namespace is Free and \
                 Entitlement.Set refuses every fact until it does; it then applies in place, with no restart"
            ),
        }
        Ok(Arc::new(slot))
    }

    /// **Build the cache under `key`, load — and re-verify — every stored fact,
    /// and swap it in.** What `open` does at boot with a key, and what
    /// [`EntitlementSlot::set_key`] does when the key arrives while running.
    fn load_under(&self, key: &[u8; 32]) -> anyhow::Result<()> {
        let cache = Arc::new(
            EntitlementCache::new(key, &self.billing_url()).map_err(|_| {
                anyhow::anyhow!("the monetize public key is not a valid Ed25519 point")
            })?,
        );
        let mut loaded = 0usize;
        // ★ **THE REFUSED ONES ARE COUNTED, and the summary says so.**
        //
        // MEASURED 2026-09-21 (lane T3): this loop counted only the facts that
        // verified, and the summary below reported `tenants = loaded`. A box
        // that refused EVERY fact it held therefore logged `tenants = 0` and
        // booted normally — the same line a box with no tenants at all logs, and
        // the same line a healthy box logs on the day nobody bought anything.
        //
        // That is this estate's recurring disease in its most expensive place: a
        // health line that cannot fail. The shape it hides is the one that costs
        // money — a box RESTORED FROM ITS TWIN under a monetize key that is not
        // the one the facts were signed with keeps every fact on disk, verifies
        // none of them, and serves every paying tenant as Free. Nothing on the
        // box is wrong; the only evidence was one `error!` line per namespace in
        // a journal nobody reads on the day of a rescue.
        //
        // The count does not change what happens to a refused fact — it is still
        // kept, and the namespace is still Free until a fact that verifies
        // arrives. **Whether a box that refused facts should REFUSE TO START is
        // a ruling for the owner, not a change to smuggle in under a log line**,
        // and the line below is what makes that ruling possible to make: until
        // now there was no number to rule about.
        let mut refused = 0usize;
        let mut refusal = Refusal {
            at_unix_ms: now_unix_ms(),
            ..Refusal::default()
        };
        for stored in self.store.all()? {
            let namespace = stored.namespace.clone();
            match cache.push(to_monetize(&stored)) {
                Ok(()) => loaded += 1,
                Err(err) => {
                    refused += 1;
                    refusal.namespaces.push(namespace.clone());
                    if refusal.reason.is_empty() {
                        refusal.reason = format!(
                            "does not verify under the configured monetize public key ({err}); the likeliest cause after a restore is the wrong --monetize-pubkey"
                        );
                    }
                    tracing::error!(
                        target: "gunnar::entitlement",
                        event = "entitlement.boot.refused",
                        %namespace,
                        state = %stored.state,
                        error = %err,
                        "a stored entitlement fact does not verify under the configured monetize key; \
                         the namespace is Free until a fact that verifies arrives"
                    )
                }
            }
        }
        if refused > 0 {
            // A second line, at WARN, that names the sum rather than the parts:
            // one line per namespace is a thing to scroll past, and `refused=40`
            // beside `tenants=0` is a thing to stop at.
            tracing::warn!(
                target: "gunnar::entitlement",
                event = "entitlement.boot.mismatch",
                tenants = loaded,
                refused,
                billing = %self.billing_url(),
                "THIS BOX IS SERVING {refused} NAMESPACE(S) WITH A SIGNED FACT AS FREE: their facts are on disk and \
                 not one of them verifies under the monetize public key this box was given. Nothing on \
                 the box is broken — the KEY is the wrong one. After a restore from a twin that is the \
                 likeliest cause, and the fix is the right --monetize-pubkey, never a re-push"
            );
        }
        tracing::info!(
            target: "gunnar::entitlement",
            event = "entitlement.boot",
            tenants = loaded,
            refused,
            billing = %self.billing_url(),
            "metered: entitlement facts loaded"
        );
        *self.refused.write().unwrap_or_else(|p| p.into_inner()) = refusal;
        self.cache.swap_in(cache);
        Ok(())
    }

    /// **The monetize key, arriving while this box runs** (owner 2026-09-25:
    /// avoid restarts). The installer feeds `monetize_pubkey` (or vm1 offers
    /// it) after this slot opened keyless; the key is applied IN PLACE — a
    /// cache under it, every stored fact re-verified, swapped in — and from
    /// the next request on the front doors and `Entitlement.Set` enforce it.
    ///
    /// `Ok(true)` applied, `Ok(false)` already in force. A DIFFERENT key while
    /// one is in force is refused by name ([`crate::feed::FEED_MISMATCH`]):
    /// which monetize this box bills for is not changed silently mid-life.
    pub fn set_key(&self, key: &[u8; 32]) -> Result<bool, String> {
        let _serial = self.write.lock().unwrap_or_else(|e| e.into_inner());
        if let Some(current) = self.cache.current() {
            return if current.public_key().as_bytes() == key {
                Ok(false)
            } else {
                Err(format!(
                    "REFUSED [{}] this box already enforces monetize key {} and was handed {}; it will not switch which monetize it bills for while running",
                    crate::feed::FEED_MISMATCH,
                    hex32(current.public_key().as_bytes()),
                    hex32(key)
                ))
            };
        }
        self.load_under(key).map_err(|e| format!("{e:#}"))?;
        tracing::info!(target: "gunnar::entitlement", event = "entitlement.key_applied", key = %hex32(key), "metered: the fed monetize key is IN FORCE — applied in place, no restart");
        Ok(true)
    }

    /// **`restate_entitlements`** (5.4): the ONE monetize's snapshot, after a
    /// recharge brought this box back without its facts. Verified WHOLE first —
    /// the envelope and every fact under this box's monetize key — so a forgery
    /// anywhere moves nothing; then each fact through [`EntitlementSlot::set`]:
    /// the same fact or one this box has moved past (`Replayed`, `Behind`) is
    /// counted as held, never re-logged. Nothing is deleted. Without a key only an
    /// empty snapshot is answered (there is nothing in it to verify).
    pub fn restate(&self, snap: &monetize_embed::signing::Snapshot) -> Result<Restated, String> {
        let Some(cache) = self.cache.current() else {
            return if snap.facts.is_empty() {
                Ok(Restated::default())
            } else {
                Err(format!("this box enforces no monetize key yet, so the snapshot's {} entitlement facts cannot be verified; nothing restated", snap.facts.len()))
            };
        };
        monetize_embed::signing::verify_snapshot(snap, cache.public_key())
            .map_err(|e| format!("the snapshot does not verify under this box's monetize key ({e}); refused whole, nothing restated"))?;
        let mut done = Restated::default();
        for fact in &snap.facts {
            let held = self.store.get(&fact.tenant.0).ok().flatten();
            if held.is_some_and(|h| {
                h.signature == fact.signature && h.issued_signature == fact.issued_signature
            }) {
                done.held += 1;
                continue;
            }
            match self.set(monetize_to_proto(fact)) {
                Ok(_) => done.applied += 1,
                Err(SetRefused::Replayed { .. } | SetRefused::Behind { .. }) => done.held += 1,
                Err(e) => {
                    return Err(format!(
                        "restated {} entitlement facts, then stopped: {e}",
                        done.applied
                    ))
                }
            }
        }
        tracing::info!(target: "gunnar::entitlement", event = "entitlement.restated", applied = done.applied, held = done.held, snapshot_issued_unix_ms = snap.issued_unix_ms, "monetize's snapshot restated");
        Ok(done)
    }

    /// Whether a monetize key is in force (a fed box may not have one yet).
    pub fn keyed(&self) -> bool {
        self.cache.current().is_some()
    }

    /// [`EntitlementSlot::set`] for monetize's own fact (the pulled `entitlement` order).
    pub fn push(
        &self,
        fact: &monetize_embed::EntitlementFact,
    ) -> Result<(EntitlementState, u64), SetRefused> {
        self.set(monetize_to_proto(fact))
    }

    /// **Verify, log, store, swap.** See the module docs for the order and
    /// why. Returns the state now enforced and the attestation sequence
    /// number of the line the fact was written as.
    pub fn set(&self, fact: ProtoFact) -> Result<(EntitlementState, u64), SetRefused> {
        let namespace = fact.namespace.clone();
        let state = state_from_proto(fact.state()).ok_or_else(|| {
            SetRefused::Malformed(format!(
                "state {} is not one of Free, Paid, Grace, Suspended, Retention",
                fact.state
            ))
        })?;
        let monetize = monetize_embed::EntitlementFact {
            tenant: TenantId(fact.namespace.clone()),
            plan: fact.plan.clone(),
            state: to_monetize_state(state),
            paid_until_unix_ms: fact.paid_until_unix_ms,
            caps: fact.caps.clone(),
            source: fact.source.clone(),
            signature: fact.signature.clone(),
            issued_unix_ms: fact.issued_unix_ms,
            issued_signature: fact.issued_signature.clone(),
        };
        // 1. Verify — with monetize-embed's verifier over monetize-embed's
        //    canonical form. Refused here means NOTHING below runs.
        let cache = self.cache.current().ok_or_else(|| SetRefused::NoKey {
            namespace: namespace.clone(),
        })?;
        monetize_embed::signing::verify_fact(&monetize, cache.public_key()).map_err(|err| {
            SetRefused::Forged {
                namespace: namespace.clone(),
                why: err.to_string(),
            }
        })?;
        let accepted_at_unix_ms = gunnar_core::now_ms();
        let stored = Entitlement {
            namespace: namespace.clone(),
            plan: fact.plan,
            state,
            paid_until_unix_ms: fact.paid_until_unix_ms,
            caps: fact.caps,
            source: fact.source,
            signature: fact.signature,
            issued_unix_ms: fact.issued_unix_ms,
            issued_signature: fact.issued_signature,
            accepted_at_unix_ms,
        };
        let _serial = self.write.lock().unwrap_or_else(|e| e.into_inner());
        // 1b. **THE REPLAY GUARD**, under the same lock as the write it
        //     guards — read the held fact and decide before anything moves, or
        //     two concurrent `Set`s could both read the old high-water mark.
        //     Read from the STORE and not the cache: the store is what survives
        //     a reboot, and it still holds a fact the cache refused at boot.
        let held = self.store.get(&namespace).ok().flatten();
        self.check_not_replayed(&namespace, held.as_ref(), &stored)?;
        // 2. Log. The line carries monetize's own JSON of the fact (its serde
        //    form, `tenant` and all), so a reader with only monetize's
        //    vocabulary can re-verify the signature from the log alone.
        let line = serde_json::json!({
            ENTITLEMENT_KEY: monetize,
            "accepted_at_unix_ms": accepted_at_unix_ms,
        });
        let seq =
            self.log
                .append_line(&line.to_string())
                .map_err(|err| SetRefused::NotRecorded {
                    namespace: namespace.clone(),
                    why: format!("appending to {}: {err:#}", self.log.path().display()),
                })?;
        // 3. Store.
        self.store
            .set(stored.clone())
            .map_err(|err| SetRefused::NotRecorded {
                namespace: namespace.clone(),
                why: format!("writing the control document: {err}"),
            })?;
        // 4. Swap. `push` re-verifies; it cannot fail here because step 1
        //    passed the same bytes through the same function, and a failure
        //    would be a bug worth a loud line rather than a silent Free.
        if let Err(err) = cache.push(to_monetize(&stored)) {
            tracing::error!(
                target: "gunnar::entitlement",
                %namespace,
                error = %err,
                "a fact that verified in step 1 was refused by the cache in step 4"
            );
        }
        // 5. The one number a plan is allowed to move — and, at the bottom of
        //    the ladder, the derived bytes that number was buying. Still under
        //    `_serial`: the reclaim appends its own line, and `append_line`
        //    counts the file to number it.
        self.enforce_cache_for_state(&namespace, state, stored.cache_cap());
        tracing::info!(
            target: "gunnar::entitlement",
            event = "entitlement.set",
            %namespace,
            state = %state,
            plan = %stored.plan,
            source = %stored.source,
            seq,
            "entitlement fact accepted"
        );
        if !self
            .verified_since_boot
            .swap(true, std::sync::atomic::Ordering::SeqCst)
            && self.vital
            && !self.refused().namespaces.is_empty()
        {
            tracing::warn!(
                target: "gunnar::entitlement",
                event = "entitlement.key_mismatch.cleared",
                %namespace,
                "a fact VERIFIES under this box's monetize key: the key mismatch at boot is over, the alarm and the maintenance status clear"
            );
        }
        Ok((state, seq))
    }

    /// **Is this fact newer than the one this box already holds?**
    ///
    /// Everything in a fact except the issue time can be replayed: the six
    /// signed fields of a `Paid` fact from April are still a genuine, genuinely
    /// signed `Paid` fact in September, and `paid_until` cannot tell the two
    /// apart because a lapse keeps the date and moves the ladder. So the one
    /// comparison that can is against `issued_unix_ms`, and it is made HERE,
    /// on the box being lied to, before the fact is logged, stored or swapped.
    ///
    /// # The rules, and the one that is not obvious
    ///
    /// * **The offered fact is unstamped** — refused, ALWAYS (stinky row 12,
    ///   2026-10-10). It used to be accepted for a namespace with no stamped
    ///   fact yet — a grandfather clause for facts signed before the field
    ///   existed — which meant a captured old `Paid` fact could be replayed onto
    ///   every namespace that had not had a stamped one yet, and stripping
    ///   fields 8 and 9 off any captured fact made it the old form again.
    ///   monetize stamps every fact it signs, and no box holds facts from
    ///   before the stamp (monetize is a test system), so nothing legitimate
    ///   is unstamped.
    /// * **The namespace has no stamped fact** — nothing to compare a stamped
    ///   fact to, so it is accepted; the guard ARMS on it and never disarms.
    /// * **Older** — refused. That is the replay.
    /// * **Newer** — accepted.
    /// * **THE SAME MILLISECOND, and the same fact** — accepted, and this is the
    ///   rule that has to be right. monetize's poller re-pushes a fact the
    ///   product never acknowledged (`retry_pushes`), verbatim, which is how a
    ///   lost push is repaired at all. Those bytes carry the stamp they were
    ///   signed with, so a re-push is never "newer" — and refusing it would
    ///   break the one path whose job is to fix a push that went missing. It is
    ///   the SAME fact, so re-applying it changes nothing: idempotent by being
    ///   identical, not by being excused.
    /// * **The same millisecond, a DIFFERENT fact** — refused. Two verdicts for
    ///   one tenant inside one millisecond is not something monetize's poller
    ///   produces (one ladder move per tenant per tick), and allowing it would
    ///   leave exactly the hole this guard exists to close: an attacker holding
    ///   a `Paid` fact minted in the same millisecond as the `Suspended` one
    ///   could still push it. The cost is a refusal an operator can see and
    ///   answer, against a replay nobody would ever see.
    ///
    /// # What this does NOT do
    ///
    /// It does not refuse a fact stamped in the future. It cannot: monetize
    /// holds the key, so only monetize can stamp one, and refusing it would
    /// withdraw service from a paying customer over a clock. It is logged
    /// loudly instead — see the `entitlement.issued.ahead` line — because a
    /// stamp far in the future DOES wedge the namespace: no later fact can beat
    /// it, and the remedy is monetize's clock, not this box.
    fn check_not_replayed(
        &self,
        namespace: &str,
        held: Option<&Entitlement>,
        offered: &Entitlement,
    ) -> Result<(), SetRefused> {
        let refuse = || {
            Err(SetRefused::Replayed {
                namespace: namespace.to_owned(),
            })
        };
        // c-stale-suspend: paid time the box holds, taken back by a verdict
        // about ANOTHER payment, while that paid time is still running.
        if let (Some(h), Some(offered_until)) = (held, offered.paid_until_unix_ms) {
            let now = gunnar_core::now_ms().max(0) as u64;
            if let Some(held_until) = h
                .paid_until_unix_ms
                .filter(|u| *u > now && offered_until < *u)
            {
                if h.state == EntitlementState::Paid && offered.source != h.source {
                    tracing::warn!(target: "gunnar::entitlement", event = "entitlement.behind", %namespace, held_until, offered_until, held_source = %h.source, offered_source = %offered.source, "a fact behind this box (a restored monetize lost a payment it holds); refused");
                    return Err(SetRefused::Behind {
                        namespace: namespace.to_owned(),
                        held_until,
                        held_source: h.source.clone(),
                        offered_until,
                        offered_source: offered.source.clone(),
                    });
                }
            }
        }
        let Some(issued) = offered.issued_unix_ms else {
            tracing::warn!(
                target: "gunnar::entitlement",
                event = "entitlement.replay_guard.refused",
                %namespace,
                why = "unstamped",
                held_issued_unix_ms = held.and_then(|h| h.issued_unix_ms),
                "a fact with no issue time; refused — nothing tells it from a replay"
            );
            return Err(SetRefused::Unstamped {
                namespace: namespace.to_owned(),
            });
        };
        let Some(high_water) = held.and_then(|h| h.issued_unix_ms) else {
            // Unlatched: the first stamped fact this namespace has had.
            tracing::info!(
                target: "gunnar::entitlement",
                event = "entitlement.replay_guard.armed",
                %namespace,
                issued_unix_ms = issued,
                "the first stamped fact for this namespace: replays of anything older are \
                 refused from here on"
            );
            return Ok(());
        };
        if issued < high_water || (issued == high_water && !same_fact(held, offered)) {
            tracing::warn!(
                target: "gunnar::entitlement",
                event = "entitlement.replay_guard.refused",
                %namespace,
                why = if issued < high_water { "older" } else { "same instant, different fact" },
                offered_issued_unix_ms = issued,
                held_issued_unix_ms = high_water,
                state = %offered.state,
                source = %offered.source,
                "a fact that is not newer than the one this server holds; refused as a replay"
            );
            return refuse();
        }
        let now = gunnar_core::now_ms();
        if now > 0 && issued > (now as u64).saturating_add(MAX_ISSUED_AHEAD_MS) {
            tracing::warn!(
                target: "gunnar::entitlement",
                event = "entitlement.issued.ahead",
                %namespace,
                issued_unix_ms = issued,
                now_unix_ms = now,
                "ACCEPTED a fact issued far ahead of this server's clock — service is never \
                 withdrawn over a clock — but no later fact can beat this stamp, so the \
                 namespace is now pinned to it. Check monetize's clock."
            );
        }
        Ok(())
    }

    /// **The cache half of a plan change: what the new state is allowed to
    /// hold, and — at the bottom of the ladder — giving back what it holds
    /// now.**
    ///
    /// Suspended and Retention are the two states [`PlanGate::verdict`] already
    /// refuses at the door. A tenant the door refuses is holding space nobody
    /// can sell: the packs stay (a plan lapsing is NOT a deletion, and nothing
    /// here touches one), but the *derived* read cache is regenerable from
    /// those packs and is therefore the one thing that can be given back at no
    /// risk at all. So it is dropped, and the budget is put where a new one
    /// cannot grow.
    ///
    /// **Grace does not reclaim, and that is the point of Grace.** A tenant in
    /// the 14-day window is expected to pay and come back; pulling their
    /// exploded objects would make the first push after they paid re-inflate
    /// every delta chain it touches (~0.5–2 ms an object) — a punishment for a
    /// customer who just paid, bought with nothing, because Grace is short and
    /// the space comes back anyway if they do not.
    ///
    /// Every other state applies `caps["cache_bytes"]` exactly as before.
    fn enforce_cache_for_state(
        &self,
        namespace: &str,
        state: EntitlementState,
        cache_cap: Option<u64>,
    ) {
        match state {
            EntitlementState::Suspended | EntitlementState::Retention => {
                self.write_explode_budget(namespace, Some(NO_CACHE_BUDGET_BYTES));
                self.reclaim_explode_caches(namespace, state);
            }
            // Free, Paid and Grace: the plan's own number, never a default
            // invented here.
            EntitlementState::Free | EntitlementState::Paid | EntitlementState::Grace => {
                match cache_cap {
                    Some(cache_bytes) => self.apply_cache_cap(namespace, cache_bytes),
                    // **The way back up, for a plan that names no cache cap.**
                    //
                    // Without this arm a tenant who was suspended and then paid
                    // stayed on `NO_CACHE_BUDGET_BYTES` for ever: the old code
                    // only ever wrote the row when the fact carried a
                    // `cache_bytes` cap, so a plan without one left our own
                    // sentinel in place and the returning customer read at
                    // uncached speed with no line anywhere saying why.
                    //
                    // Only OUR sentinel is undone, and only back to `None` =
                    // the server's default. An operator's hand-set quota
                    // (`gunnar namespace set-explode-budget`) is not a thing a
                    // plan change is entitled to clear, so any other value is
                    // left exactly as found.
                    None => {
                        if matches!(
                            self.stores.catalog().namespace_explode_budget(namespace),
                            Ok(Some(NO_CACHE_BUDGET_BYTES))
                        ) {
                            self.write_explode_budget(namespace, None);
                            self.reopen_on_server_default(namespace);
                        }
                    }
                }
            }
        }
    }

    /// **Let the namespace's open stores fall back to the server's default
    /// budget, by forgetting their handles.**
    ///
    /// Needed only on the way back up out of [`NO_CACHE_BUDGET_BYTES`], and
    /// only because `None` on the catalog row is an ABSENCE rather than a
    /// number: `ServerStores::apply_explode_quota` skips a `None` row
    /// (`Ok(None) => {}`), which is right for its usual caller and wrong here —
    /// a store that was open through the suspension would otherwise keep the
    /// 1-byte budget in its own atomic for as long as the handle cache held it,
    /// and a tenant who had just paid would read at uncached speed with the
    /// catalog row saying they were on the default.
    ///
    /// The server's default is computed from the filesystem inside the engine
    /// (`exploded_cache::budget_from_env`) and there is no "put it back"
    /// number to write, so the honest move is to let the store be opened
    /// again: `close_repo` releases the handle, the next request re-opens on
    /// the default. The cost is one refold per repository, once, at the moment
    /// a customer returns — paid by the side that just started paying again,
    /// and only for the repositories whose handles we ourselves had capped.
    fn reopen_on_server_default(&self, namespace: &str) {
        for repo in self
            .stores
            .catalog()
            .list(Some(namespace))
            .unwrap_or_default()
        {
            self.stores.close_repo(&repo.path());
        }
    }

    /// **Drop the derived read cache of every repository in `namespace`, and
    /// journal what came back.**
    ///
    /// Safe by construction, and each leg of that was checked rather than
    /// assumed:
    ///
    /// * the exploded cache is derived — every row in it is re-inflatable from
    ///   the packs, which this does not touch;
    /// * `drop_cache` unlinks the *lazy* pool only. The eager pool stays: it is
    ///   the commit-and-tree graph that `graph`, `reach` and a tag peel read,
    ///   not a cache;
    /// * a twin never held it. `twin::under_root` refuses any framed path
    ///   `txlog::never_framed` names — "a cache or per-box path — never applied
    ///   on a twin" — so there is no mirror anywhere that this can desynchronise.
    ///
    /// # Only the stores that are OPEN, and why that is the right line
    ///
    /// `Engine::drop_explode_cache` reaches a store through the live handle
    /// cache and answers 0 for a closed one; it will not open a store to empty
    /// it. Taken deliberately, not worked around:
    ///
    /// * an open store is where the *process's* share of this is — the
    ///   descriptors, the redb page caches, the segment being written into —
    ///   and it is the only store that can still be GROWING a cache right now.
    ///   That growth is what a suspension has to stop, and this stops it;
    /// * a closed store's lazy segments stay on disk, and the sentinel budget
    ///   means they cannot survive the next open: `evict_to_budget` runs on the
    ///   first maintenance tick after the quota is applied at open
    ///   (`ServerStores::apply_explode_quota`, from the catalog row this just
    ///   wrote) and takes the pool down to its one live segment;
    /// * opening every repository of a suspended namespace to empty it is a
    ///   full store open (the refold cost) per repository, on the control-plane
    ///   thread, under `EntitlementSlot`'s write mutex, for a tenant the front
    ///   door is refusing. The reclaim would cost more at the moment of
    ///   suspension than the space is worth, and a monetize push that fans out
    ///   into hundreds of store opens is a stall an operator would read as an
    ///   outage.
    ///
    /// **So the honest limit is stated rather than hidden**: a suspended
    /// namespace whose repositories are all closed reclaims 0 bytes here, and
    /// the journal line says 0 rather than a number nobody gave back. That
    /// disk comes back on `gunnar repo gc` (`admin::gc`), which opens the store
    /// anyway and already drops the cache as part of its own work.
    fn reclaim_explode_caches(&self, namespace: &str, state: EntitlementState) {
        let engine = self.stores.engine();
        let mut freed = 0u64;
        let mut repos = 0usize;
        let mut reclaimed_repos = 0usize;
        for repo in self
            .stores
            .catalog()
            .list(Some(namespace))
            .unwrap_or_default()
        {
            let Ok(dir) = self.stores.repo_dir(&repo.path()) else {
                continue;
            };
            repos += 1;
            let bytes = engine.drop_explode_cache(&dir);
            if bytes > 0 {
                reclaimed_repos += 1;
                freed += bytes;
            }
        }
        // **Journalled even when it is 0.** The owner's question is "what did
        // suspending this tenant give back", and a line that only ever appears
        // when the answer is flattering is not an answer. A zero here is the
        // closed-store case above, on the record, by namespace.
        let line = serde_json::json!({
            crate::attest::RECLAIM_KEY: {
                "namespace": namespace,
                "state": state.to_string(),
                "bytes_freed": freed,
                "repos": repos,
                "repos_reclaimed": reclaimed_repos,
                "budget_bytes": NO_CACHE_BUDGET_BYTES,
            },
            "reclaimed_at_unix_ms": gunnar_core::now_ms(),
        });
        // The caller holds `self.write`; this must not take it again.
        if let Err(err) = self.log.append_line(&line.to_string()) {
            // Same rule as a failed push attestation (`attest`): the plan has
            // already moved and the cache is already gone, so returning an
            // error here could only report a refusal that did not happen.
            tracing::error!(
                target: "gunnar::entitlement",
                %namespace,
                error = %err,
                "the cache reclaim could not be journalled; the bytes are gone but the line is not there"
            );
        }
        tracing::info!(
            target: "gunnar::entitlement",
            event = "entitlement.cache_reclaimed",
            %namespace,
            state = %state,
            bytes = freed,
            repos,
            repos_reclaimed = reclaimed_repos,
            "the derived read cache of a namespace the door refuses was given back"
        );
    }

    /// `caps["cache_bytes"]` → `Namespace::explode_budget_bytes`, on the
    /// catalog row and on every store of the namespace that is open right now.
    /// `0` is "unlimited", which is `Some(0)` on the row — a deliberate answer,
    /// as the field's own doc says.
    fn apply_cache_cap(&self, namespace: &str, cache_bytes: u64) {
        self.write_explode_budget(namespace, Some(cache_bytes));
    }

    /// The one writer of `Namespace::explode_budget_bytes` on this path: the
    /// catalog row, then every store of the namespace that is open right now.
    /// `None` is "back to the server's default", which is what the field's
    /// absence means.
    fn write_explode_budget(&self, namespace: &str, budget: Option<u64>) {
        match self
            .stores
            .catalog()
            .set_namespace_explode_budget(namespace, budget)
        {
            // Applied to the stores open right now as well, exactly as
            // `gunnar namespace set-explode-budget` does (admin.rs).
            Ok(_) => {
                for repo in self
                    .stores
                    .catalog()
                    .list(Some(namespace))
                    .unwrap_or_default()
                {
                    if let Ok(dir) = self.stores.repo_dir(&repo.path()) {
                        self.stores.apply_explode_quota(namespace, &dir);
                    }
                }
            }
            // A namespace monetize knows and the catalog does not yet (a plan
            // bought before the first push): the cap is in the stored fact
            // and is applied when the namespace is created. Not an error.
            Err(gunnar_core::Error::NoSuchNamespace(_)) => {}
            Err(err) => tracing::warn!(
                target: "gunnar::entitlement",
                %namespace,
                error = %err,
                "caps[cache_bytes] could not be applied to the namespace"
            ),
        }
    }

    /// **The fact being ENFORCED for `namespace`**, or `None` = Free.
    ///
    /// ★ Off the CACHE, the snapshot `PlanGate` reads — not off the document.
    ///
    /// MEASURED 2026-09-21 (lane T3): this read the STORE, which keeps a fact the
    /// cache refused at boot. On a box restored under the wrong
    /// `--monetize-pubkey`, `Entitlement.Get` therefore answered `PAID` with a
    /// renewal date for a tenant the front door was serving as Free, and
    /// `Usage.Read` — which reports the plan from here — told the tenant the
    /// same. The seam said so in its own doc (`ControlPlane::entitlement_get`:
    /// "a rejected fact must not be reported as an allowance … must answer out
    /// of the same snapshot `PlanGate` reads") and nothing did it. Now every
    /// reader of a plan answers what the box DOES; the refused facts are still
    /// on disk, and [`EntitlementSlot::refused_at_boot`] is how they are seen.
    pub fn get(&self, namespace: &str) -> anyhow::Result<Option<ProtoFact>> {
        Ok(self
            .cache
            .fact(&TenantId(namespace.to_owned()))
            .as_ref()
            .map(monetize_to_proto))
    }

    /// Every ENFORCED fact, one per namespace, in namespace order. A namespace
    /// whose stored fact was refused at boot is absent — it is Free, which is
    /// what "no fact" means on this wire. See [`EntitlementSlot::get`].
    pub fn list(&self) -> anyhow::Result<Vec<ProtoFact>> {
        Ok(self
            .store
            .all()?
            .iter()
            .filter_map(|stored| self.cache.fact(&TenantId(stored.namespace.clone())))
            .map(|fact| monetize_to_proto(&fact))
            .collect())
    }

    /// How many stored facts did not verify under this box's monetize key when
    /// it booted. Non-zero is a box serving signed tenants as Free; after a
    /// restore the likeliest cause is the wrong `--monetize-pubkey`.
    fn refused(&self) -> Refusal {
        self.refused
            .read()
            .unwrap_or_else(|p| p.into_inner())
            .clone()
    }

    pub fn refused_at_boot(&self) -> usize {
        self.refused().namespaces.len()
    }

    /// The boot's refusal as the wire carries it (`Entitlement.List`), or `None`
    /// when nothing was refused.
    pub fn refusal(&self) -> Option<gunnar_proto::RefusedAtBoot> {
        (!self.refused().namespaces.is_empty()).then(|| gunnar_proto::RefusedAtBoot {
            count: self.refused().namespaces.len() as u64,
            at_unix_ms: self.refused().at_unix_ms,
            reason: self.refused().reason.clone(),
            namespaces: self.refused().namespaces.clone(),
        })
    }

    /// **Is this box in maintenance for its monetize key?** Owner ruling 2026-09-22: with
    /// `--monetize-vital` (the default), a boot that refused stored facts is `maintenance` until
    /// monetize pushes a fact that VERIFIES; the box starts and serves meanwhile. With
    /// `--monetize-vital false` it never is — the refusal is only logged.
    pub fn maintenance(&self) -> bool {
        self.vital
            && !self.refused().namespaces.is_empty()
            && !self
                .verified_since_boot
                .load(std::sync::atomic::Ordering::SeqCst)
    }

    /// The same, as an `Admin.Health` alarm, `entitlement.key_mismatch`, naming the namespaces:
    /// the WARN `entitlement.boot.mismatch` on the wire, for a machine whose console nobody reads.
    /// Only while [`EntitlementSlot::maintenance`] holds; `None` on a box that is not vital.
    pub fn alarm(&self) -> Option<gunnar_proto::HealthAlarm> {
        if !self.maintenance() {
            return None;
        }
        self.refusal().map(|r| gunnar_proto::HealthAlarm {
            event: KEY_MISMATCH_ALARM.into(),
            count: r.count,
            since_unix_ms: r.at_unix_ms,
            detail: format!(
                "MAINTENANCE: {} namespace(s) hold a signed entitlement this box does not verify, and are served as Free: {}: {}. \
                 It clears when monetize pushes a fact that verifies. The fix is the right --monetize-pubkey, never a re-push",
                r.count,
                r.namespaces.join(", "),
                r.reason
            ),
        })
    }

    /// **The state being ENFORCED for `namespace`**, off the cache: `None` is
    /// Free — including a stored fact that did not verify at boot. The band
    /// and `Admin.Stats` count from this and not from the document, so what
    /// the operator sees is what the front door does.
    pub fn enforced_state(&self, namespace: &str) -> Option<EntitlementState> {
        self.cache
            .fact(&TenantId(namespace.to_owned()))
            .map(|fact| from_monetize_state(fact.state))
    }

    /// The counts for the band and `Admin.Stats`: the namespaces the document
    /// names, each at the state the cache enforces.
    pub fn counts(&self) -> TenantCounts {
        let mut counts = TenantCounts::default();
        for fact in self.store.all().unwrap_or_default() {
            match self.enforced_state(&fact.namespace) {
                Some(EntitlementState::Paid) => counts.paid += 1,
                Some(EntitlementState::Grace) => counts.grace += 1,
                Some(EntitlementState::Suspended | EntitlementState::Retention) => {
                    counts.suspended += 1
                }
                Some(EntitlementState::Free) | None => {}
            }
        }
        counts
    }

    /// The pay page for `namespace`: the same template the cache renders
    /// into every refusal line, so the quota line and the plan line name one
    /// URL.
    fn url_for(&self, namespace: &str) -> String {
        self.billing_url().replace("{tenant}", namespace)
    }

    /// The pay page template in force.
    pub fn billing_url(&self) -> String {
        self.billing_url
            .read()
            .unwrap_or_else(|p| p.into_inner())
            .clone()
    }

    /// ★ UW-6: **the SITE's pay page, fed by the installer's door** (`refusal_link`), taken
    /// LIVE: the medium is one for every estate, so the page it bakes is a placeholder until
    /// the site says its own. `Ok(true)` when it changed (the cache is rebuilt under the key
    /// in force, so the next refusal and Grace line name it), `Ok(false)` for the page
    /// already in force. Only an `https://` URL is taken; any other is refused by name and
    /// the page in force stays.
    pub fn set_billing_url(&self, url: &str) -> Result<bool, String> {
        let url = url.trim();
        let host = url
            .strip_prefix("https://")
            .map(|r| r.split('/').next().unwrap_or_default())
            .unwrap_or_default();
        if host.is_empty() || url.chars().any(char::is_whitespace) {
            return Err(format!("REFUSED [pairing-feed-billing-url] the installer fed `{}` = `{url}`, which is not an https:// page; the pay page in force stays", crate::feed::FEED_BILLING_URL));
        }
        let url = billing_template(url);
        let _serial = self.write.lock().unwrap_or_else(|e| e.into_inner());
        if self.billing_url() == url {
            return Ok(false);
        }
        *self.billing_url.write().unwrap_or_else(|p| p.into_inner()) = url.clone();
        if let Some(current) = self.cache.current() {
            let key = *current.public_key().as_bytes();
            self.load_under(&key).map_err(|e| format!("the pay page {url} is set, but the cache did not rebuild under the key in force: {e:#}"))?;
        }
        tracing::info!(target: "gunnar::entitlement", event = "entitlement.billing_url", billing = %url, "the site's pay page is fed: every refusal and Grace line ends in it");
        Ok(true)
    }
}

/// The metered server's [`gunnar_ssh::PlanGate`]: the cache's verdict,
/// rendered as the front door's.
impl EntitlementSlot {
    /// **THE counting point for a `cap` refusal**: the plan said no, before a
    /// byte moved.
    ///
    /// Both doors this gate has come through here — the ladder
    /// (`PlanGate::verdict`, a Suspended or Retention tenant) and the pack
    /// quota (`PlanGate::pack_quota`, a tenant already at `caps["pack_bytes"]`)
    /// — because to a tenant they are one sentence: *your plan will not let you
    /// do this*. Which of the two it was is already in the refusal line the
    /// client reads and in the attestation log; the flow ledger counts how
    /// OFTEN, which is the number that says whether a customer is stuck.
    ///
    /// **The two MID-transfer cuts are deliberately not counted here**, and
    /// they are not missing. `gunnar_wire::receive_pack`'s pack ceiling and
    /// `gunnar_lfs`'s allowance both cut a transfer that is already running,
    /// and a cut transfer reaches the flow ledger as what it is: bytes in, and
    /// no push. A tenant whose pushes arrive and never complete is visible in
    /// exactly that shape, and counting the cut as a refusal as well would
    /// report one event twice under two names.
    fn charge_refusal(&self, namespace: &str) {
        self.stores
            .flow()
            .refuse(namespace, gunnar_core::now_ms(), gunnar_core::Refusal::Cap);
    }
}

impl gunnar_ssh::PlanGate for EntitlementSlot {
    fn verdict(&self, namespace: &str, action: gunnar_ssh::PlanAction) -> gunnar_ssh::PlanVerdict {
        let tenant = TenantId(namespace.to_owned());
        let action = match action {
            gunnar_ssh::PlanAction::Write => Action::Write,
            gunnar_ssh::PlanAction::AnonymousRead => Action::AnonymousRead,
        };
        match self.cache.allows(&tenant, action) {
            Verdict::Refuse { reason_line, .. } => {
                self.charge_refusal(namespace);
                gunnar_ssh::PlanVerdict::Refuse(reason_line)
            }
            Verdict::Allow => match self.cache.notice(&tenant) {
                Some(line) => gunnar_ssh::PlanVerdict::Warn(line),
                None => gunnar_ssh::PlanVerdict::Allow,
            },
        }
    }

    fn name(&self) -> &'static str {
        "entitlement"
    }

    fn pack_quota(&self, namespace: &str) -> gunnar_ssh::PlanVerdict {
        let Some(quota) = self.pack_quota_state(namespace) else {
            return gunnar_ssh::PlanVerdict::Allow;
        };
        if quota.plan == gunnar_grpc::selling::NO_ROOM_YET {
            self.charge_refusal(namespace);
            return gunnar_ssh::PlanVerdict::Refuse(format!(
                "{}: {}",
                gunnar_grpc::selling::NO_ROOM_YET,
                self.url_for(namespace)
            ));
        }
        if quota.held >= quota.cap {
            self.charge_refusal(namespace);
            gunnar_ssh::PlanVerdict::Refuse(format!(
                "plan {} is at its pack quota ({} of {} bytes held): writes closed; \
                 delete or buy more at {}",
                quota.plan,
                quota.held,
                quota.cap,
                self.url_for(namespace)
            ))
        } else {
            gunnar_ssh::PlanVerdict::Allow
        }
    }

    /// **The mid-upload ceiling for `caps["pack_bytes"]`.**
    ///
    /// [`Self::pack_quota`] above refuses a tenant who is ALREADY at the cap,
    /// before a byte moves. It cannot refuse the push that crosses the cap
    /// *during* the transfer, because a pack header carries an object count and
    /// not a byte count — so until this existed, a tenant one byte under a
    /// 10 GiB cap could send 400 GiB and the appliance would take all of it,
    /// spool it, and only then notice. This is the number the receive loop
    /// counts against; `gunnar_wire::receive_pack` cuts the stream past it.
    ///
    /// # The slack, and why it is not zero
    ///
    /// The ceiling is `what is left under the cap` **plus slack**, and the
    /// slack is deliberate rather than defensive rounding. Three reasons, each
    /// of which alone would make a zero-slack ceiling wrong:
    ///
    /// 1. **Wire bytes are not stored bytes.** What is counted here is what the
    ///    client sent; what the cap is measured in is
    ///    `control_plane::repo_bytes().pack`, which is what the engine wrote.
    ///    A thin push is completed with bases the client never sent (larger),
    ///    the engine stores the pack verbatim and compressed (smaller), and
    ///    neither direction is known before the push. A ceiling exactly equal
    ///    to the remaining room would therefore refuse pushes that would have
    ///    fitted, and it would do so *mid-transfer*, which is the worst moment
    ///    to be wrong.
    /// 2. **A push that lands exactly at the cap must land.** `pack_quota`
    ///    refuses at `held >= cap`, so `held + n == cap` is a legal push; a
    ///    hard ceiling with no slack cuts it at its last byte.
    /// 3. **This is a backstop, not a meter.** Billing is `Usage.Read`'s job
    ///    and the polite refusal is `pack_quota`'s, before the bytes. What is
    ///    left for this to prevent is the *unbounded* push — 400 GiB, not
    ///    400 MiB — so it is set generously on purpose. A tenant who overshoots
    ///    by a little is billed for it and refused on their NEXT push, with a
    ///    sentence they can read, rather than cut off mid-transfer.
    ///
    /// The number is the larger of a proportion and an absolute floor, which is
    /// the shape `gunnar_ssh::headroom::DiskFloor` already argues for one
    /// module away and for the same reason: a percentage alone is nothing on a
    /// small plan, and an absolute alone is nothing on a large one.
    ///
    /// * **`cap / 16`** (6.25 %) so a 1 TiB plan gets 64 GiB of room and a
    ///   10 GiB plan gets 640 MiB — proportionate to how wrong the wire-versus-
    ///   stored estimate can be, since that error scales with the push.
    /// * **`64 MiB`** as the floor, because on a small plan 6.25 % is smaller
    ///   than one ordinary push: at a 256 MiB cap it is 16 MiB, so a tenant
    ///   holding 250 MiB would be held to a 22 MiB ceiling and a 60 MiB
    ///   repository would be cut in flight by the arithmetic. With the floor
    ///   the ceiling is 70 MiB and that push lands — and is then refused,
    ///   politely and before its first byte, on the NEXT one.
    ///
    /// Neither number is measured — they cannot be, the quantity they cover is
    /// the difference between two things gunnar does not know before the push —
    /// so they are stated as a judgement with its reasons, and the test that
    /// holds them is `entitlement.rs`'s, which asserts the arithmetic rather
    /// than a magic total.
    fn pack_ceiling(&self, namespace: &str) -> Option<gunnar_ssh::PackCeiling> {
        let quota = self.pack_quota_state(namespace)?;
        let remaining = quota.cap.saturating_sub(quota.held);
        let slack = (quota.cap / PACK_CEILING_SLACK_DIVISOR).max(PACK_CEILING_SLACK_FLOOR_BYTES);
        let bytes = remaining.saturating_add(slack);
        Some(gunnar_ssh::PackCeiling {
            bytes,
            // The same three facts `pack_quota`'s refusal carries — the plan,
            // the cap, where to pay — because a tenant who meets one of these
            // sentences has usually just met the other, and two different
            // spellings of "you are out of room" is how a support ticket
            // becomes two.
            refusal: format!(
                "plan {} would pass its pack quota with this push (cap {} bytes, {} already \
                 held, this push sent more than {bytes} bytes): the transfer was stopped and \
                 nothing was stored; delete or buy more at {}",
                quota.plan,
                quota.cap,
                quota.held,
                self.url_for(namespace)
            ),
        })
    }

    /// The LFS twin of [`gunnar_ssh::PlanGate::pack_ceiling`]. See
    /// [`Self::lfs_allowance_state`] for the arithmetic and for why it is
    /// `caps["lfs_bytes"]` and not the room left under `caps["pack_bytes"]`.
    fn lfs_allowance(&self, namespace: &str) -> Option<gunnar_lfs::Allowance> {
        self.lfs_allowance_state(namespace)
    }
}

impl EntitlementSlot {
    /// **The tenant's remaining LFS room**, for `gunnar_lfs::Session` and the
    /// HTTP object endpoint.
    ///
    /// Off `caps["lfs_bytes"]` and the namespace's measured `lfs` bytes — the
    /// same two numbers `Usage.Read` reports as `allowance.lfs_bytes` and
    /// `usage.lfs_bytes`, so what the console shows, what the door enforces and
    /// what monetize bills are one arithmetic and not three (one implementation). That
    /// identity is the whole reason this reads `pack_quota_state`'s twin rather
    /// than re-walking the disk with its own rules.
    ///
    /// # Why this is `lfs_bytes` and NOT the room left under `pack_bytes`
    ///
    /// Tried the other way first, and it is wrong for a reason worth writing
    /// down, because it looks generous: LFS bytes land on the same filesystem
    /// the tenant bought, so bounding them by the pack allowance would close the
    /// hole with no new cap and no monetize change at all.
    ///
    /// It would also make `Usage.Read` a liar. That surface reports
    /// `allowance.lfs_bytes` ABSENT for a fact that names no `lfs_bytes` cap,
    /// and absent has one documented meaning — uncapped (`usage.proto`). A door
    /// that silently held LFS to the pack allowance would refuse a tenant at a
    /// number no surface anywhere shows them, which is precisely the support
    /// incident this work exists to prevent. The number a user is shown, the
    /// number the gate enforces and the number monetize bills have to be the
    /// same number or be labelled as different things.
    ///
    /// So the wall is honest and narrow: it binds exactly when a signed fact
    /// says `lfs_bytes`, and it is `None` — take what arrives — when nothing
    /// does. **A plan that does not set `lfs_bytes` still has an unbounded LFS
    /// tier**, and that is now a fact about what monetize sells rather than a
    /// hole in what gunnar enforces: `products/gunnar`'s `METERS` says
    /// `lfs_bytes` is `Enforced` and the console prices it, so an order that
    /// caps it gets a real wall on this box.
    ///
    /// # No slack, unlike [`Self::pack_ceiling`]
    ///
    /// That one is generous because a pack's wire bytes and its stored bytes are
    /// different numbers nobody can know before the transfer. An LFS object is
    /// stored raw and byte-identical, and its size is declared before the
    /// payload and then verified byte for byte — so the two numbers are the same
    /// number and the answer here is exact. `saturating_sub`, so a tenant already
    /// over their cap gets `0` and is refused every non-empty object rather than
    /// wrapping into 18 exabytes of room.
    fn lfs_allowance_state(&self, namespace: &str) -> Option<gunnar_lfs::Allowance> {
        // A CLAN's LFS objects come out of its one room (round 2, owner
        // 2026-10-05): what is left is the room in force less what it stores
        // in packs and in LFS together.
        if let Ok(Some(clan)) = self.stores.auth().clan(namespace) {
            let (pack, lfs) = self.namespace_bytes(namespace);
            let room = clan.room_in_force(&self.lapsed_members(&clan));
            let held = pack.saturating_add(lfs);
            return Some(gunnar_lfs::Allowance {
                bytes: room.saturating_sub(held),
                refusal: format!(
                    "clan {namespace} has {} bytes of room left ({room} bytes given by its members, {held} \
                     already held in packs and LFS objects): this upload does not fit; its members give it \
                     more room",
                    room.saturating_sub(held)
                ),
            });
        }
        let tenant = TenantId(namespace.to_owned());
        let fact = self.cache.fact(&tenant)?;
        // The same "0 is unlimited, not zero" rule `pack_quota_state` documents:
        // a plan document that omitted the number renders as zero, and reading
        // that as a total prohibition would close every LFS upload on the box
        // the day a plan grew a typo.
        let cap = fact.caps.get("lfs_bytes").copied().filter(|cap| *cap > 0)?;
        let held: u64 = self
            .stores
            .catalog()
            .list(Some(namespace))
            .unwrap_or_default()
            .iter()
            .filter_map(|repo| gunnar_core::RepoRef::new(&repo.namespace, &repo.name).ok())
            .filter_map(|path| self.stores.repo_dir(&path).ok())
            .map(|dir| crate::control_plane::repo_bytes(&dir).lfs)
            .sum();
        Some(gunnar_lfs::Allowance {
            bytes: cap.saturating_sub(held),
            // The same four facts the pack refusals carry — the plan, the cap,
            // what is held, where to pay — because a tenant who meets one of
            // these sentences has usually just met the other, and two different
            // spellings of "you are out of room" is how one support ticket
            // becomes two.
            refusal: format!(
                "plan {} has {} bytes of lfs room left (cap {} bytes, {} already held): \
                 this upload does not fit; delete or buy more at {}",
                fact.plan,
                cap.saturating_sub(held),
                cap,
                held,
                self.url_for(namespace)
            ),
        })
    }
}

/// **The slack in a mid-upload ceiling, as a divisor of the cap** — `16`, i.e.
/// 6.25 %. See [`EntitlementSlot::pack_ceiling`] for why there is slack at all.
pub const PACK_CEILING_SLACK_DIVISOR: u64 = 16;

/// **…and the floor under it**, 64 MiB, for the small plan where a percentage
/// is smaller than one ordinary push. See [`EntitlementSlot::pack_ceiling`].
pub const PACK_CEILING_SLACK_FLOOR_BYTES: u64 = 64 * 1024 * 1024;

/// What a namespace holds against what its plan allows.
///
/// One walk and one lookup, computed once and read by both
/// [`gunnar_ssh::PlanGate::pack_quota`] and
/// [`gunnar_ssh::PlanGate::pack_ceiling`]: the refusal before the bytes and the
/// ceiling during them are two answers about ONE fact, and two copies of this
/// arithmetic could disagree about what a tenant holds (one implementation).
struct PackQuota {
    /// The plan's name, for the refusal line.
    plan: String,
    /// `caps["pack_bytes"]`.
    cap: u64,
    /// What the namespace's repositories hold, off the disk.
    held: u64,
}

impl EntitlementSlot {
    /// [`PackQuota`] for `namespace`, or `None` when there is nothing to hold
    /// it to: no signed fact, or a fact with no `pack_bytes` cap.
    ///
    /// A cap of `0` is "no cap" and not "no bytes": a plan document that
    /// omitted the number renders as zero, and reading that as a total
    /// prohibition would close every write on the box the day a plan grew a
    /// typo.
    fn pack_quota_state(&self, namespace: &str) -> Option<PackQuota> {
        // **A CLAN's room is its members' contributions** (owner, 2026-10-05:
        // "each member contributes storage to the clan") — the sum, and a sum
        // of 0 is 0 bytes, not "no cap": a clan nobody gave room to stores
        // nothing. No fact is signed for a clan; nothing is bought for one.
        //
        // Round 2 (owner, 2026-10-05): the sum of the shares IN FORCE — a
        // member whose paid period has ended gives the clan nothing the door
        // counts ([`Self::lapsed_members`]) — held against what the clan
        // stores in packs AND in LFS objects: one room for both.
        if let Ok(Some(clan)) = self.stores.auth().clan(namespace) {
            let (pack, lfs) = self.namespace_bytes(namespace);
            return Some(PackQuota {
                plan: format!("clan {namespace}"),
                cap: clan.room_in_force(&self.lapsed_members(&clan)),
                held: pack.saturating_add(lfs),
            });
        }
        let tenant = TenantId(namespace.to_owned());
        let Some(fact) = self.cache.fact(&tenant) else {
            return self.no_fact_quota(namespace);
        };
        let cap = fact
            .caps
            .get("pack_bytes")
            .copied()
            .filter(|cap| *cap > 0)?;
        // …and what a member gives its clans is out of its OWN room: the
        // account's cap, less that. An unreadable table gives nothing away.
        let cap = cap.saturating_sub(self.stores.auth().contributed_by(namespace).unwrap_or(0));
        // The namespace's pack bytes, off the disk — the same walk
        // `Usage.Read` bills from (one implementation), so the number a tenant is refused
        // at is the number on their bill. Only reached for a write on a
        // namespace WITH a cap.
        let held = self.namespace_pack_bytes(namespace);
        Some(PackQuota {
            plan: fact.plan.clone(),
            cap,
            held,
        })
    }

    /// **The flag "this estate sells room"** — the one `serve` sets from the feed and hands
    /// the gRPC context too.
    pub fn sells_room_flag(&self) -> Arc<std::sync::atomic::AtomicBool> {
        Arc::clone(&self.sells_room)
    }

    /// [`PackQuota`] for a namespace with NO fact: none (uncapped, as always) — except at an
    /// estate that SELLS room, where a person's own account holds no room until its fact
    /// lands ([`gunnar_grpc::selling::no_room_yet`]). An unreadable mark says seller.
    fn no_fact_quota(&self, namespace: &str) -> Option<PackQuota> {
        let sells = self.sells_room.load(std::sync::atomic::Ordering::Relaxed);
        if !sells {
            return None;
        }
        let auth = self.stores.auth();
        let person = matches!(auth.principal(namespace), Ok(Some(p)) if p.kind == gunnar_core::PrincipalKind::User);
        let seller = auth.holds_server_grant(namespace).unwrap_or(true);
        gunnar_grpc::selling::no_room_yet(sells, false, person, seller).then(|| PackQuota {
            plan: gunnar_grpc::selling::NO_ROOM_YET.to_string(),
            cap: 0,
            held: self.namespace_pack_bytes(namespace),
        })
    }

    /// What a namespace's repositories hold, off the disk — the same walk
    /// `Usage.Read` bills from (one implementation).
    fn namespace_pack_bytes(&self, namespace: &str) -> u64 {
        self.namespace_bytes(namespace).0
    }

    /// `(pack, lfs)` bytes a namespace's repositories hold, off the disk.
    fn namespace_bytes(&self, namespace: &str) -> (u64, u64) {
        self.stores
            .catalog()
            .list(Some(namespace))
            .unwrap_or_default()
            .iter()
            .filter_map(|repo| gunnar_core::RepoRef::new(&repo.namespace, &repo.name).ok())
            .filter_map(|path| self.stores.repo_dir(&path).ok())
            .map(|dir| crate::control_plane::repo_bytes(&dir))
            .fold((0u64, 0u64), |(pack, lfs), bytes| {
                (
                    pack.saturating_add(bytes.pack),
                    lfs.saturating_add(bytes.lfs),
                )
            })
    }

    /// **Is `member`'s share of its clans in force?** (owner, 2026-10-05: "the
    /// clan KEEPS that member's room share until the member's paid period
    /// ends, then the share drops".) Out of force once its fact says the paid
    /// period is over — `paid_until` passed (Grace included: grace is the
    /// ACCOUNT's, after the period), or Suspended, or Retention. A member with
    /// no fact, or a Free one with no period, has nothing that ends.
    fn share_in_force(&self, member: &str, now: u64) -> bool {
        let Some(fact) = self.cache.fact(&TenantId(member.to_owned())) else {
            return true;
        };
        match fact.state {
            State::Suspended | State::Retention => false,
            _ => fact.paid_until_unix_ms.map_or(true, |until| until > now),
        }
    }

    /// **The members of `clan` whose shares are out of force now** — the one
    /// reading the door and the `Clans` answers both make.
    pub fn lapsed_members(&self, clan: &gunnar_core::Clan) -> Vec<String> {
        let now = u64::try_from(now_unix_ms()).unwrap_or(0);
        clan.members
            .iter()
            .filter(|m| !self.share_in_force(&m.principal, now))
            .map(|m| m.principal.clone())
            .collect()
    }
}

// ── the three vocabularies, one mapping each way ────────────────────────────

fn state_from_proto(state: gunnar_proto::EntitlementState) -> Option<EntitlementState> {
    use gunnar_proto::EntitlementState as P;
    Some(match state {
        P::Unspecified => return None,
        P::Free => EntitlementState::Free,
        P::Paid => EntitlementState::Paid,
        P::Grace => EntitlementState::Grace,
        P::Suspended => EntitlementState::Suspended,
        P::Retention => EntitlementState::Retention,
    })
}

pub(crate) fn state_to_proto(state: EntitlementState) -> gunnar_proto::EntitlementState {
    use gunnar_proto::EntitlementState as P;
    match state {
        EntitlementState::Free => P::Free,
        EntitlementState::Paid => P::Paid,
        EntitlementState::Grace => P::Grace,
        EntitlementState::Suspended => P::Suspended,
        EntitlementState::Retention => P::Retention,
    }
}

fn from_monetize_state(state: State) -> EntitlementState {
    match state {
        State::Free => EntitlementState::Free,
        State::Paid => EntitlementState::Paid,
        State::Grace => EntitlementState::Grace,
        State::Suspended => EntitlementState::Suspended,
        State::Retention => EntitlementState::Retention,
    }
}

fn to_monetize_state(state: EntitlementState) -> State {
    match state {
        EntitlementState::Free => State::Free,
        EntitlementState::Paid => State::Paid,
        EntitlementState::Grace => State::Grace,
        EntitlementState::Suspended => State::Suspended,
        EntitlementState::Retention => State::Retention,
    }
}

/// **How far ahead of this server's clock a fact may be stamped before the log
/// says so.** Not a refusal — see [`EntitlementSlot::check_not_replayed`] — and
/// generous, because the appliance syncs its clock from the front and a few
/// minutes of drift is ordinary. An hour is not.
const MAX_ISSUED_AHEAD_MS: u64 = 60 * 60 * 1000;

/// **Are these the same signed statement?** The V2 signature covers every field
/// including the issue time, so equal signatures mean equal facts — that is what
/// Ed25519 is for, and comparing the bytes monetize signed is stronger than
/// comparing fields gunnar copied. The V1 half is compared too: a fact whose two
/// signatures disagree with the held one's is not the held one however its
/// fields read.
fn same_fact(held: Option<&Entitlement>, offered: &Entitlement) -> bool {
    held.is_some_and(|h| {
        h.signature == offered.signature && h.issued_signature == offered.issued_signature
    })
}

/// The stored fact as monetize signed it. Field for field, nothing derived:
/// this is what `verify_fact` hashes, so it must be the bytes monetize saw.
fn to_monetize(fact: &Entitlement) -> monetize_embed::EntitlementFact {
    monetize_embed::EntitlementFact {
        tenant: TenantId(fact.namespace.clone()),
        plan: fact.plan.clone(),
        state: to_monetize_state(fact.state),
        paid_until_unix_ms: fact.paid_until_unix_ms,
        caps: fact.caps.clone(),
        source: fact.source.clone(),
        signature: fact.signature.clone(),
        issued_unix_ms: fact.issued_unix_ms,
        issued_signature: fact.issued_signature.clone(),
    }
}

/// The enforced fact, as the wire carries it. Field for field: the signatures travel so a reader can re-verify what it is told.
fn monetize_to_proto(fact: &monetize_embed::EntitlementFact) -> ProtoFact {
    ProtoFact {
        namespace: fact.tenant.0.clone(),
        plan: fact.plan.clone(),
        state: state_to_proto(from_monetize_state(fact.state)) as i32,
        paid_until_unix_ms: fact.paid_until_unix_ms,
        caps: fact.caps.clone(),
        source: fact.source.clone(),
        signature: fact.signature.clone(),
        issued_unix_ms: fact.issued_unix_ms,
        issued_signature: fact.issued_signature.clone(),
    }
}

fn hex32(k: &[u8; 32]) -> String {
    k.iter().map(|b| format!("{b:02x}")).collect()
}