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

nordisk/holger

server/lib/src/metered.rs

//! **The paid state — compiled ONLY by the metered build.**
//!
//! The community holger does not decline to enforce a plan; it does not contain
//! one. This module, the `holger.metered.v1` proto package and the `monetize-embed`
//! dependency all hang off the `metered` feature, so an open binary has no plan
//! vocabulary in its string table and no slot on `HolgerGrpc` to put one in.
//!
//! ## What arrives, and what is checked before anything moves
//!
//! monetize pushes a signed [`EntitlementFact`] per repository. `Set` is ADMIN, and
//! in this order:
//!
//! 1. **shape** — a fact missing its repository, plan, state or source, or carrying
//!    a signature that is not 64 bytes, is `INVALID_ARGUMENT`. Cheap, and it tells a
//!    malformed push apart from a forged one.
//! 2. **the caps are ones holger can police** — `caps` is an open map, and a cap on
//!    a meter this server only measures SERVER-WIDE (`bytes_served`, `requests`,
//!    `proxy_upstream_hits`, `repos`) is a promise about a number that is not the
//!    tenant's. Refused at the sale rather than stored, logged and then ignored for
//!    the life of the plan — gunnar learned that one the expensive way.
//! 3. **the signature** — Ed25519 by monetize's key over the canonical JSON of the
//!    RUST `monetize_product::EntitlementFact` minus `signature`.
//!    `monetize_embed::signing::{fact_message, verify_fact}` is the ONE writer and
//!    the ONE verifier of that form; holger writes no second canonical form. A fact
//!    that does not verify is `PERMISSION_DENIED` and nothing moves.
//!
//! ⚠ **The proto shape is NOT the signed shape**, and that is the trap in this file.
//! monetize signs the Rust struct (`tenant`, `state` as the variant NAME `"Paid"`,
//! `caps` an object with sorted keys); the wire carries a prost message
//! (`repository`, `state` as an `i32`, `caps` a `HashMap`). [`to_signed`] is the one
//! conversion, and `the_conversion_is_what_monetize_signed` pins it — get it wrong
//! and every signature fails for a reason no message would explain.
//!
//! ## Where a fact lives
//!
//! `<data_root>/holger-entitlements/<repository>.json`, one file per repository,
//! closed to the repository roster exactly as the SBOM and property stores are: a
//! repository name arrives off the wire and is authorized as a raw string, so
//! without a roster a caller writes plan files for repositories that do not exist.
//! holger has no separate attestation log — the append-only audit log IS the record
//! that a plan changed, and `Set` writes an [`AuditAction::Entitlement`] line
//! carrying the fact's `source` (`operator` or `payment:<vendor>:<reference>`).

use crate::lock::{LockOrRecover as _, RwLockOrRecover as _};
use std::collections::{BTreeMap, BTreeSet};
use std::path::{Path, PathBuf};
use std::sync::Arc;

use monetize_embed::VerifyingKey;
use monetize_product::{EntitlementFact as SignedFact, State, TenantId};
use tonic::{Request, Response, Status};

use crate::audit::{AuditAction, AuditEvent};
use crate::grpc::holger_metered_proto::entitlement_service_server::EntitlementService;
use crate::grpc::holger_metered_proto::{
    EntitlementFact, EntitlementState, GetEntitlementRequest, GetEntitlementResponse,
    ListEntitlementsRequest, ListEntitlementsResponse, PurgeRepositoryRequest,
    PurgeRepositoryResponse, SetEntitlementRequest, SetEntitlementResponse,
};
use crate::grpc::{coded, internal_err, HolgerGrpc};
use crate::metering::PER_REPOSITORY_METERS;
use holger_errcode as ec;

/// One file per repository, under the data root beside the other metadata stores.
pub const STORE_DIR: &str = "holger-entitlements";

/// The paid state on disk, closed to the repositories this server actually serves.
pub struct EntitlementStore {
    root: PathBuf,
    roster: Arc<BTreeSet<String>>,
    /// monetize's Ed25519 verifying key. Without one this store REFUSES every
    /// write: a metered build that cannot check a signature must not keep a fact it
    /// cannot vouch for. `None` while the key is AWAITED (an estate box whose front
    /// offers it after the start, A7 row 8): [`EntitlementStore::set_key`] applies it
    /// in place, no restart.
    key: std::sync::RwLock<Option<VerifyingKey>>,
}

impl EntitlementStore {
    /// `public_key` is monetize's 32-byte Ed25519 verifying key.
    pub fn new(
        root: impl Into<PathBuf>,
        roster: Arc<BTreeSet<String>>,
        public_key: &[u8; 32],
    ) -> Result<Self, monetize_embed::KeyError> {
        Ok(Self {
            root: root.into(),
            roster,
            key: std::sync::RwLock::new(Some(
                VerifyingKey::from_bytes(public_key).map_err(|_| monetize_embed::KeyError)?,
            )),
        })
    }

    /// **A store whose key is awaited** (A7 row 8): it keeps nothing and polices nothing
    /// until [`EntitlementStore::set_key`] gives it monetize's key.
    pub fn awaiting(root: impl Into<PathBuf>, roster: Arc<BTreeSet<String>>) -> Self {
        Self {
            root: root.into(),
            roster,
            key: std::sync::RwLock::new(None),
        }
    }

    /// **monetize's key, applied in place** when the installer feeds it after the start.
    pub fn set_key(&self, public_key: &[u8; 32]) -> Result<(), monetize_embed::KeyError> {
        let key = VerifyingKey::from_bytes(public_key).map_err(|_| monetize_embed::KeyError)?;
        *self.key.write_or_recover() = Some(key);
        Ok(())
    }

    /// True once this store has monetize's key (it verifies, keeps and polices).
    pub fn keyed(&self) -> bool {
        self.key.read_or_recover().is_some()
    }

    fn path_of(&self, repository: &str) -> PathBuf {
        self.root.join(format!("{}.json", sanitize(repository)))
    }

    /// The store's directory — also what names this server to the purge gate
    /// ([`crate::purge::close`]), since every door opens the same one.
    pub fn root(&self) -> &Path {
        &self.root
    }

    /// True when this server serves `repository` — the roster gate every metadata
    /// store here applies.
    pub fn serves(&self, repository: &str) -> bool {
        self.roster.contains(repository)
    }

    pub fn get(&self, repository: &str) -> std::io::Result<Option<SignedFact>> {
        match std::fs::read_to_string(self.path_of(repository)) {
            Ok(text) => Ok(serde_json::from_str(&text).ok()),
            Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
            Err(e) => Err(e),
        }
    }

    /// Verify, then write. The order is the point: a fact that does not verify never
    /// reaches the disk.
    pub fn put(&self, fact: &SignedFact) -> Result<(), PutRefused> {
        let key = (*self.key.read_or_recover()).ok_or_else(|| {
            PutRefused::Signature("no monetize key has reached this box yet".into())
        })?;
        monetize_embed::signing::verify_fact(fact, &key)
            .map_err(|e| PutRefused::Signature(e.to_string()))?;
        std::fs::create_dir_all(&self.root).map_err(|e| PutRefused::Io(e.to_string()))?;
        let text = serde_json::to_string_pretty(fact)
            .map_err(|e| PutRefused::Io(format!("serialize the fact: {e}")))?;
        std::fs::write(self.path_of(&fact.tenant.0), text)
            .map_err(|e| PutRefused::Io(e.to_string()))
    }

    pub fn list(&self) -> Vec<SignedFact> {
        let mut out = Vec::new();
        let Ok(dir) = std::fs::read_dir(&self.root) else {
            return out;
        };
        for entry in dir.flatten() {
            if let Ok(text) = std::fs::read_to_string(entry.path()) {
                if let Ok(f) = serde_json::from_str::<SignedFact>(&text) {
                    out.push(f);
                }
            }
        }
        out.sort_by(|a, b| a.tenant.0.cmp(&b.tenant.0));
        out
    }
}

/// Why a write was refused, kept apart so the handler can answer
/// `PERMISSION_DENIED` for a signature and `INTERNAL` for a disk.
#[derive(Debug)]
pub enum PutRefused {
    Signature(String),
    Io(String),
}

/// A repository name is a file name here, so it may not escape the store.
fn sanitize(repository: &str) -> String {
    repository.replace(['/', '\\'], "_").replace("..", "_")
}

/// **The one conversion from the wire shape to the shape monetize SIGNED.**
///
/// Field for field, and the two that differ are the whole reason this function
/// exists: `repository` is the signed `tenant`, and the numeric `state` is the
/// variant NAME in the canonical JSON.
pub fn to_signed(fact: &EntitlementFact) -> Result<SignedFact, Status> {
    let state = match EntitlementState::try_from(fact.state) {
        Ok(EntitlementState::Free) => State::Free,
        Ok(EntitlementState::Paid) => State::Paid,
        Ok(EntitlementState::Grace) => State::Grace,
        Ok(EntitlementState::Suspended) => State::Suspended,
        Ok(EntitlementState::Retention) => State::Retention,
        Ok(EntitlementState::Unspecified) | Err(_) => {
            return Err(coded(
                &ec::METERED_FACT_MALFORMED,
                tonic::Code::InvalidArgument,
                "fact.state is unspecified; a fact names one of Free, Paid, Grace, \
                 Suspended, Retention",
            ))
        }
    };
    Ok(SignedFact {
        tenant: TenantId(fact.repository.clone()),
        plan: fact.plan.clone(),
        state,
        paid_until_unix_ms: fact.paid_until_unix_ms,
        caps: fact
            .caps
            .iter()
            .map(|(k, v)| (k.clone(), *v))
            .collect::<BTreeMap<_, _>>(),
        source: fact.source.clone(),
        signature: fact.signature.clone(),
        // Carried through, never invented: dropping the stamp would turn a
        // replay-guarded fact into the legal unstamped case and lose the guard
        // with the signature still verifying.
        issued_unix_ms: fact.issued_unix_ms,
        issued_signature: fact.issued_signature.clone(),
    })
}

/// The wire shape of a stored fact, for `Get`/`List` — the inverse of [`to_signed`].
///
/// `pub` so the round trip can be PINNED from a test: the two functions carry the
/// issue stamp (`issued_unix_ms`, `issued_signature`) through in both directions,
/// and a fixture that is not stamped cannot tell whether they do.
pub fn to_wire(fact: &SignedFact) -> EntitlementFact {
    let state = match fact.state {
        State::Free => EntitlementState::Free,
        State::Paid => EntitlementState::Paid,
        State::Grace => EntitlementState::Grace,
        State::Suspended => EntitlementState::Suspended,
        State::Retention => EntitlementState::Retention,
    };
    EntitlementFact {
        repository: fact.tenant.0.clone(),
        plan: fact.plan.clone(),
        state: state as i32,
        paid_until_unix_ms: fact.paid_until_unix_ms,
        caps: fact.caps.iter().map(|(k, v)| (k.clone(), *v)).collect(),
        source: fact.source.clone(),
        signature: fact.signature.clone(),
        issued_unix_ms: fact.issued_unix_ms,
        issued_signature: fact.issued_signature.clone(),
    }
}

/// **The caps this server cannot police**, named so the refusal can list them.
///
/// A cap is a promise. holger measures `artifacts` and `archive_bytes` per
/// repository and everything else server-wide, so a cap on a server-wide meter
/// would be a wall the tenant can neither hit nor be told about.
pub fn caps_holger_cannot_honour<'a>(keys: impl Iterator<Item = &'a String>) -> Vec<String> {
    keys.filter(|k| !PER_REPOSITORY_METERS.contains(&k.as_str()))
        .cloned()
        .collect()
}

/// The shape checks, before the signature is looked at. `pub` so a test can drive
/// the door itself rather than a copy of it.
pub fn well_formed(fact: &EntitlementFact) -> Result<(), Status> {
    if fact.repository.is_empty() {
        return Err(coded(
            &ec::METERED_FACT_MALFORMED,
            tonic::Code::InvalidArgument,
            "fact.repository is required",
        ));
    }
    if fact.plan.is_empty() {
        return Err(coded(
            &ec::METERED_FACT_MALFORMED,
            tonic::Code::InvalidArgument,
            "fact.plan is required",
        ));
    }
    if fact.source.is_empty() {
        return Err(coded(
            &ec::METERED_FACT_MALFORMED,
            tonic::Code::InvalidArgument,
            "fact.source is required: `operator` or `payment:<vendor>:<reference>`",
        ));
    }
    if fact.signature.len() != 64 {
        return Err(coded(
            &ec::METERED_FACT_MALFORMED,
            tonic::Code::InvalidArgument,
            format!(
                "fact.signature is {} byte(s); an Ed25519 signature is 64",
                fact.signature.len()
            ),
        ));
    }
    let unknown = caps_holger_cannot_honour(fact.caps.keys());
    if !unknown.is_empty() {
        return Err(coded(
            &ec::METERED_CAP_UNPOLICEABLE,
            tonic::Code::InvalidArgument,
            format!(
                "caps name meter(s) holger cannot police per repository: {}. holger measures \
                 {} per repository; everything else is server-wide, and a cap on a server-wide \
                 number is a promise about something that is not this tenant's.",
                unknown.join(", "),
                PER_REPOSITORY_METERS.join(", ")
            ),
        ));
    }
    Ok(())
}

impl HolgerGrpc {
    /// The ceilings in force for one repository, echoed onto `ReadUsage` so a
    /// tenant can see its own rather than discovering it at a refusal.
    ///
    /// A ceiling of `0` is carried through unchanged and means NO CEILING. It has
    /// never meant "may hold nothing", and a reader that treats it that way would
    /// refuse every write to a tenant whose order simply did not name one.
    ///
    /// An unconfigured or unreadable store yields NO caps, which is the honest
    /// answer: this box is not policing anything.
    pub(crate) fn caps_for_repository(
        &self,
        repository: &str,
    ) -> Vec<crate::grpc::holger_proto::MeterCap> {
        let Some(store) = self.entitlements.as_ref() else {
            return Vec::new();
        };
        let Ok(Some(fact)) = store.get(repository) else {
            return Vec::new();
        };
        let mut caps: Vec<crate::grpc::holger_proto::MeterCap> = fact
            .caps
            .iter()
            .map(|(meter, limit)| crate::grpc::holger_proto::MeterCap {
                meter: meter.clone(),
                limit: *limit,
            })
            .collect();
        caps.sort_by(|a, b| a.meter.cmp(&b.meter));
        caps
    }

    /// The store, or the refusal that says this build was never configured with
    /// monetize's key.
    fn entitlement_store(&self) -> Result<&Arc<EntitlementStore>, Status> {
        self.entitlements
            .as_ref()
            .filter(|s| s.keyed())
            .ok_or_else(|| {
                coded(
                    &ec::METERED_NOT_CONFIGURED,
                    tonic::Code::FailedPrecondition,
                    "this server has no monetize public key configured, so it cannot verify a \
                 signed entitlement — no plan is enforced",
                )
            })
    }
}

#[tonic::async_trait]
impl EntitlementService for Arc<HolgerGrpc> {
    async fn get(
        &self,
        request: Request<GetEntitlementRequest>,
    ) -> Result<Response<GetEntitlementResponse>, Status> {
        let repo = request.get_ref().repository.clone();
        // A plan is the provider's business with the tenant: ADMIN, like gunnar's.
        self.authorize_admin(&request, &repo).await?;
        let store = self.entitlement_store()?;
        let fact = store
            .get(&repo)
            .map_err(|e| internal_err(format!("read the fact: {e}")))?;
        Ok(Response::new(GetEntitlementResponse {
            fact: fact.as_ref().map(to_wire),
        }))
    }

    async fn set(
        &self,
        request: Request<SetEntitlementRequest>,
    ) -> Result<Response<SetEntitlementResponse>, Status> {
        let source_ip = self.audit_peer(&request);
        let fact = request.get_ref().fact.clone().ok_or_else(|| {
            coded(
                &ec::METERED_FACT_MALFORMED,
                tonic::Code::InvalidArgument,
                "request carries no fact",
            )
        })?;
        let repo = fact.repository.clone();
        let identity = self.authorize_admin(&request, &repo).await?;
        let ident = identity
            .as_ref()
            .map(|id| id.subject.clone())
            .unwrap_or_else(|| "anonymous".to_string());

        well_formed(&fact)?;
        let store = self.entitlement_store()?;
        if !store.serves(&repo) {
            return Err(coded(
                &ec::METERED_UNKNOWN_REPOSITORY,
                tonic::Code::NotFound,
                format!("this server serves no repository '{repo}'"),
            ));
        }
        let signed = to_signed(&fact)?;

        match store.put(&signed) {
            Ok(()) => {
                // The audit log IS holger's attestation trail: who presented the
                // fact, for which repository, and what it came from.
                self.record_audit(
                    AuditEvent::new(
                        &ident,
                        AuditAction::Entitlement,
                        &repo,
                        "",
                        &source_ip,
                        200,
                        0,
                    )
                    .with_detail(format!(
                        "state={:?} plan={} source={}",
                        signed.state, signed.plan, signed.source
                    )),
                );
                crate::grpc::functional_status(
                    "holger-grpc/entitlement_set",
                    "entitlement_stored",
                    true,
                    &repo,
                );
                Ok(Response::new(SetEntitlementResponse { state: fact.state }))
            }
            Err(PutRefused::Signature(why)) => {
                self.record_audit(
                    AuditEvent::new(
                        &ident,
                        AuditAction::Entitlement,
                        &repo,
                        "",
                        &source_ip,
                        403,
                        0,
                    )
                    .with_detail(format!("signature refused: {why}")),
                );
                crate::grpc::functional_status(
                    "holger-grpc/entitlement_set",
                    "entitlement_stored",
                    false,
                    "signature did not verify",
                );
                Err(coded(
                    &ec::METERED_SIGNATURE_INVALID,
                    tonic::Code::PermissionDenied,
                    "the fact's signature was not made by this server's configured monetize key",
                ))
            }
            Err(PutRefused::Io(why)) => Err(internal_err(format!("store the fact: {why}"))),
        }
    }

    async fn list(
        &self,
        request: Request<ListEntitlementsRequest>,
    ) -> Result<Response<ListEntitlementsResponse>, Status> {
        // Server-wide read: every plan on the box. ADMIN, scoped to no repository.
        self.authorize_admin(&request, "").await?;
        let store = self.entitlement_store()?;
        Ok(Response::new(ListEntitlementsResponse {
            facts: store.list().iter().map(to_wire).collect(),
        }))
    }

    /// **Delete every artifact of one repository** — see `Purge` in
    /// `proto/holger-metered.proto` for the contract, [`crate::purge`] for the walk.
    async fn purge(
        &self,
        request: Request<PurgeRepositoryRequest>,
    ) -> Result<Response<PurgeRepositoryResponse>, Status> {
        let source_ip = self.audit_peer(&request);
        let req = request.get_ref().clone();
        let repo = req.repository.clone();
        // The same door as `Set`: ADMIN, and a twin refuses it like every mutation.
        let identity = self.authorize_admin(&request, &repo).await?;
        let ident = identity
            .as_ref()
            .map(|id| id.subject.clone())
            .unwrap_or_else(|| "anonymous".to_string());
        if repo.is_empty() {
            return Err(coded(
                &ec::METERED_FACT_MALFORMED,
                tonic::Code::InvalidArgument,
                "request.repository is required",
            ));
        }
        let store = Arc::clone(self.entitlement_store()?);
        if !store.serves(&repo) {
            return Err(coded(
                &ec::METERED_UNKNOWN_REPOSITORY,
                tonic::Code::NotFound,
                format!("this server serves no repository '{repo}'"),
            ));
        }
        // holger's OWN guard, independent of whoever asks: only a repository whose
        // stored fact says it is not paid for may lose a byte.
        let fact = store
            .get(&repo)
            .map_err(|e| internal_err(format!("read the fact: {e}")))?;
        if let Err(why) = purgeable(&repo, fact.as_ref(), crate::clock::now_unix_ms()) {
            self.record_audit(
                AuditEvent::new(&ident, AuditAction::Purge, &repo, "", &source_ip, 412, 0)
                    .with_detail(format!("refused: {why}; reason given: {}", req.reason)),
            );
            return Err(coded(
                &ec::METERED_PURGE_REFUSED,
                tonic::Code::FailedPrecondition,
                why,
            ));
        }
        let backend = self.get_repo(&repo)?;
        let (walk_store, walk_repo, reason) =
            (Arc::clone(&store), repo.clone(), req.reason.clone());
        // The walk runs off the reactor and OWNS the gate: a caller that gives up
        // (a deadline) does not stop it, and the gate opens only when it ends.
        let run = tokio::task::spawn_blocking(move || {
            purge_walk(&walk_store, backend.as_ref(), &walk_repo, &reason)
        })
        .await
        .map_err(|e| internal_err(format!("the purge walk failed: {e}")))?;
        let PurgeRun::Done {
            walked,
            failures,
            record,
        } = run
        else {
            return Ok(Response::new(PurgeRepositoryResponse {
                repository: repo.clone(),
                gate_released: false,
                state: fact.as_ref().map(|f| to_wire(f).state).unwrap_or_default(),
                detail: format!(
                    "a purge of `{repo}` is already walking; ask again when it has ended"
                ),
                ..Default::default()
            }));
        };

        let after = store.get(&repo).ok().flatten();
        let detail = format!(
            "{} artifact(s), {} byte(s) deleted from `{repo}`{}",
            walked.deleted,
            walked.bytes,
            if failures.is_empty() {
                String::new()
            } else {
                format!("; {} could not be", failures.len())
            }
        );
        self.record_audit(
            AuditEvent::new(
                &ident,
                AuditAction::Purge,
                &repo,
                "",
                &source_ip,
                if failures.is_empty() { 200 } else { 500 },
                walked.bytes,
            )
            .with_detail(format!("{detail}; reason: {}", req.reason)),
        );
        crate::grpc::functional_status(
            "holger-grpc/entitlement_purge",
            "repository_purged",
            failures.is_empty(),
            &detail,
        );
        Ok(Response::new(PurgeRepositoryResponse {
            repository: repo,
            artifacts_deleted: walked.deleted,
            bytes_reclaimed: walked.bytes,
            failures,
            gate_released: true,
            state: after.as_ref().map(|f| to_wire(f).state).unwrap_or_default(),
            record,
            detail,
        }))
    }
}

/// What one purge walk did ([`purge_walk`]).
pub enum PurgeRun {
    /// A walk of the repository is already running (its gate is closed).
    Busy,
    Done {
        walked: crate::purge::Walked,
        failures: Vec<String>,
        record: String,
    },
}

/// **Empty one repository**, blocking: close its gate (or answer [`PurgeRun::Busy`]),
/// delete every artifact, and write the record its twin applies. The ONE walk both the
/// admin door (`EntitlementService.Purge`) and a pulled `purge_tenant` order run. The
/// caller has checked the stored fact says Suspended or Retention.
pub fn purge_walk(
    store: &EntitlementStore,
    backend: &dyn traits::RepositoryBackendTrait,
    repo: &str,
    reason: &str,
) -> PurgeRun {
    let scope = store.root().to_path_buf();
    let Some(gate) = crate::purge::close(&scope, repo) else {
        return PurgeRun::Busy;
    };
    let _gate = gate;
    let data_root = scope
        .parent()
        .map(Path::to_path_buf)
        .unwrap_or_else(|| scope.clone());
    let w = crate::purge::walk(backend, &|_| crate::purge::Verdict::Delete);
    let mut failures = w.failures.clone();
    let mut record = String::new();
    if failures.is_empty() {
        let r = crate::purge::Record {
            repository: repo.to_string(),
            at_unix_ms: crate::clock::now_unix_ms().max(0) as u64,
            reason: reason.to_string(),
            artifacts: w.deleted,
            bytes: w.bytes,
        };
        match crate::purge::write_record(&data_root, &r) {
            Ok(p) => {
                record = format!(
                    "{}/{}",
                    crate::purge::RECORDS_DIR,
                    p.file_name()
                        .map(|n| n.to_string_lossy().to_string())
                        .unwrap_or_default()
                )
            }
            // Deleted here but the twin would never learn: not complete.
            Err(e) => failures.push(format!(
                "the purge record for the twin could not be written: {e}"
            )),
        }
    }
    PurgeRun::Done {
        walked: w,
        failures,
        record,
    }
}

/// The store path under a data root — one place, so the server and a test agree.
pub fn store_dir(data_root: &Path) -> PathBuf {
    data_root.join(STORE_DIR)
}

// ── ENFORCEMENT: the paid `disk_bytes` limit on the write doors ──────────────
//
// One check, [`Quota`], asked by the HTTP door and the gRPC door alike before a
// body is read, so the two cannot disagree about whether a repository is full.
// No fact for a repository ⇒ nothing is policed (the open behaviour). A fact in
// `Suspended` or `Retention` refuses every upload. A fact with a positive
// `disk_bytes` cap refuses an upload that would take what the repository
// OCCUPIES (`metering::disk_bytes`, the reading monetize bills) past it — and
// the refusal carries the numbers and WHERE the bytes are, so the customer can
// see what to delete or what to buy. Deletes are never refused here: they are
// how a full repository gets back under its limit.
//
// Cost: an upload that FITS is answered from the last reading plus what was
// admitted since ([`Quota`]'s cache, re-walked every [`REWALK`]); only a
// refusal, or a stale reading, walks the repository's storage places.

/// **How long after `paid_until` holger still takes writes on its own: none.**
/// The owner's non-payment ruling (2026-09-29): a customer who stops paying is
/// READ-ONLY AT ONCE — monetize mails and deletes after the grace the ONE box decides
/// (`[nonpay] delete_after_days`, see [`purgeable`]), and no grace is granted in between. Past `paid_until` by holger's own
/// clock, a `Paid` or `Grace` fact nobody replaced is LAPSED. (It was 14 days,
/// the grace monetize's `order::Policy` once granted before suspending.)
pub const LAPSE_GRACE_MS: u64 = 0;

/// `YYYY-MM-DD` (UTC) of a unix-ms instant — the civil-from-days arithmetic,
/// so a refusal names the day and not a number.
pub fn civil_date(ms: u64) -> String {
    let z = (ms / 86_400_000) as i64 + 719_468;
    let era = z.div_euclid(146_097);
    let doe = z - era * 146_097;
    let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
    let mp = (5 * doy + 2) / 153;
    let d = doy - (153 * mp + 2) / 5 + 1;
    let m = if mp < 10 { mp + 3 } else { mp - 9 };
    let y = yoe + era * 400 + i64::from(m <= 2);
    format!("{y:04}-{m:02}-{d:02}")
}

/// Pure: **a lapsed paid period** (g-holger-expiry, audit 2026-09-28; read-only at
/// once since the non-payment ruling of 2026-09-29). A `Paid` or `Grace` fact whose
/// `paid_until_unix_ms` (plus [`LAPSE_GRACE_MS`], which is zero) lies before
/// `now_ms` (holger's OWN clock, [`crate::clock`], which follows the estate's
/// signed time) was never renewed and never moved on by the front — so the
/// repository is READ-ONLY by holger's own reading: uploads refused, reads and
/// deletes go on. A front that stops pushing no longer keeps writes open.
pub fn lapsed(fact: &SignedFact, now_ms: i64) -> Option<String> {
    if !matches!(fact.state, State::Paid | State::Grace) {
        return None;
    }
    let until = fact.paid_until_unix_ms.filter(|t| *t > 0)?;
    let closes = until.saturating_add(LAPSE_GRACE_MS);
    if now_ms <= 0 || (now_ms as u64) <= closes {
        return None;
    }
    Some(format!(
        "repository `{}` was paid until {} (order {}) and no renewal has reached this server since, \
         so it is read-only by its own clock: uploads are refused until it is paid again, reads go on",
        fact.tenant.0,
        civil_date(until),
        fact.plan,
    ))
}

/// Pure: **may this repository be purged now?** — holger's own guard on a purge the ONE box
/// ordered. The ONE box decides WHEN (its nonpay grace, `[nonpay] delete_after_days`, 10 days
/// on the ONE box): holger keeps no day count of its own. It refuses only a repository that is
/// paid for by its stored fact: `Free` (or no fact), or a `Paid`/`Grace` fact whose period has
/// not ended by holger's own clock. A `Suspended` or `Retention` fact, or a `Paid`/`Grace` one
/// past its `paid_until` ([`lapsed`]: monetize stopped renewing it), is purged.
pub fn purgeable(repo: &str, fact: Option<&SignedFact>, now_ms: i64) -> Result<(), String> {
    let Some(f) = fact else {
        return Err(format!("repository `{repo}` is Free (no fact stored): a repository nobody sold is never purged, and nothing was deleted"));
    };
    if matches!(f.state, State::Suspended | State::Retention) || lapsed(f, now_ms).is_some() {
        return Ok(());
    }
    Err(format!("repository `{repo}` is {:?} (order {}) and paid for by its own clock: only a lapsed, Suspended or Retention repository is purged, and nothing was deleted", f.state, f.plan))
}

/// Pure: the verdict for one upload. `None` ⇒ allowed; `Some(why)` ⇒ refused.
pub fn over_cap(fact: &SignedFact, current: u64, incoming: u64, where_: &str) -> Option<String> {
    if matches!(fact.state, State::Suspended | State::Retention) {
        return Some(format!(
            "repository `{}` is {:?} (order {}): uploads are refused until it is paid again",
            fact.tenant.0, fact.state, fact.plan
        ));
    }
    let limit = *fact.caps.get(crate::metering::METER_DISK_BYTES)?;
    if limit == 0 {
        return None;
    }
    let after = current.saturating_add(incoming);
    if current >= limit || after > limit {
        // The customer never sees a cap (the owner's rule, A10 row 18): the refusal says the
        // room is full and where more is bought; the numbers are the operator's
        // ([`operator_line`], logged by the write door).
        let _ = where_;
        return Some(format!(
            "repository `{}` is full: delete something, or buy more room for it",
            fact.tenant.0
        ));
    }
    None
}

/// **What the operator is told about a refused upload**: the numbers the customer is never
/// shown — what the repository would occupy, its cap, and where its bytes are.
pub fn operator_line(fact: &SignedFact, current: u64, incoming: u64, where_: &str) -> String {
    let limit = fact
        .caps
        .get(crate::metering::METER_DISK_BYTES)
        .copied()
        .unwrap_or(0);
    format!(
        "repository `{}` would occupy {} bytes on disk (now {current} + {incoming} incoming); its paid disk_bytes cap is {limit} — where the bytes are: {where_}",
        fact.tenant.0,
        current.saturating_add(incoming)
    )
}

/// The write-door check: the paid state plus where each repository keeps its bytes.
pub struct Quota {
    store: Arc<EntitlementStore>,
    /// Swapped (never edited in place) when a repository is made at run time (lane M4b).
    places: std::sync::RwLock<Arc<crate::metering::StoragePlaces>>,
    /// The last reading per repository, plus every upload admitted since.
    readings: std::sync::Mutex<std::collections::HashMap<String, Reading>>,
    /// The estate's pay page a paid refusal names (lane M4; `{tenant}` the repository).
    /// Replaced live when a later feed names another ([`Quota::set_pay_page`]).
    pay_page: std::sync::RwLock<Option<String>>,
}

/// What a repository occupied at `at`, plus what was admitted since — an
/// OVER-estimate as long as every write is admitted here (a failed or
/// deleted write only makes it larger than the truth, never smaller).
struct Reading {
    bytes: u64,
    at: std::time::Instant,
}

/// How long a reading may answer before the disk is walked again — the bound on
/// drift from bytes no admission saw (index sidecars, archive overhead) and on
/// how long a delete takes to be credited when nothing else forces a walk.
pub const REWALK: std::time::Duration = std::time::Duration::from_secs(30);

impl Quota {
    pub fn new(store: Arc<EntitlementStore>, places: Arc<crate::metering::StoragePlaces>) -> Self {
        Self {
            store,
            places: std::sync::RwLock::new(places),
            readings: Default::default(),
            pay_page: std::sync::RwLock::new(None),
        }
    }

    /// Where a repository made at run time keeps its bytes (lane M4b), so its paid limit
    /// measures it from its first upload.
    pub fn add_places(&self, repository: &str, places: Vec<crate::repository::StoragePlace>) {
        let mut cell = self.places.write_or_recover();
        let mut next = (**cell).clone();
        next.insert(repository.to_string(), places);
        *cell = Arc::new(next);
    }

    /// The quota whose paid refusals name `pay_page` (an empty one: none).
    pub fn with_pay_page(self, pay_page: Option<String>) -> Self {
        self.set_pay_page(pay_page);
        self
    }

    /// **The pay page, applied in place** when a later feed names it (A7 row 8).
    pub fn set_pay_page(&self, pay_page: Option<String>) {
        *self.pay_page.write_or_recover() = pay_page
            .map(|p| p.trim().to_string())
            .filter(|p| !p.is_empty());
    }

    /// The paid-state store this limit reads.
    pub fn store(&self) -> &Arc<EntitlementStore> {
        &self.store
    }

    /// A paid refusal, with where it is paid.
    fn paid(&self, repository: &str, why: String) -> String {
        match &*self.pay_page.read_or_recover() {
            Some(p) => format!("{why}; it is paid at {}", p.replace("{tenant}", repository)),
            None => why,
        }
    }

    /// `Ok(())` ⇒ take the upload; `Err(line)` ⇒ refuse it with
    /// [`holger_errcode::METERED_OVER_CAP`]. A store or disk that cannot be read
    /// refuses too (fail closed): a paid limit nobody can check is not a limit.
    pub fn admit(&self, repository: &str, incoming: u64) -> Result<(), String> {
        // A repository being PURGED takes nothing, whatever its fact says now — a
        // payment landing mid-walk must not open writes the walk would delete.
        if crate::purge::is_closed(self.store.root(), repository) {
            return Err(crate::purge::gated_refusal(repository));
        }
        // No monetize key has reached this box yet: nothing is paid for, nothing is policed.
        if !self.store.keyed() {
            return Ok(());
        }
        let fact = match self.store.get(repository) {
            Ok(Some(f)) => f,
            Ok(None) => return Ok(()),
            Err(e) => {
                return Err(format!(
                    "repository `{repository}`: the paid state cannot be read: {e}"
                ))
            }
        };
        // A paid period nobody renewed closes writes by holger's own clock.
        if let Some(why) = lapsed(&fact, crate::clock::now_unix_ms()) {
            return Err(self.paid(repository, why));
        }
        if fact
            .caps
            .get(crate::metering::METER_DISK_BYTES)
            .copied()
            .unwrap_or(0)
            == 0
            && !matches!(fact.state, State::Suspended | State::Retention)
        {
            return Ok(());
        }
        // A suspended plan needs no measurement to refuse.
        if matches!(fact.state, State::Suspended | State::Retention) {
            return Err(self.paid(
                repository,
                over_cap(&fact, 0, incoming, "").unwrap_or_default(),
            ));
        }
        let limit = fact
            .caps
            .get(crate::metering::METER_DISK_BYTES)
            .copied()
            .unwrap_or(0);
        // FAST ACCEPT: a fresh reading that says it fits takes the upload with no
        // walk, and counts it. A REFUSAL is never decided from the cache — only
        // from the disk, walked now, so a delete is credited before anybody is
        // told no.
        {
            let mut readings = self.readings.lock_or_recover();
            if let Some(r) = readings.get_mut(repository) {
                if r.at.elapsed() < REWALK
                    && r.bytes < limit
                    && r.bytes.saturating_add(incoming) <= limit
                {
                    r.bytes += incoming;
                    return Ok(());
                }
            }
        }
        let all = Arc::clone(&self.places.read_or_recover());
        let places = all.get(repository).map(Vec::as_slice).unwrap_or(&[]);
        let (current, where_) = crate::metering::disk_bytes(places).map_err(|e| {
            format!("repository `{repository}`: what it occupies cannot be measured: {e}")
        })?;
        let verdict = over_cap(&fact, current, incoming, &where_);
        if verdict.is_some() {
            log::info!(
                "metered: upload refused, {}",
                operator_line(&fact, current, incoming, &where_)
            );
        }
        let counted = if verdict.is_none() {
            current.saturating_add(incoming)
        } else {
            current
        };
        self.readings.lock_or_recover().insert(
            repository.to_string(),
            Reading {
                bytes: counted,
                at: std::time::Instant::now(),
            },
        );
        match verdict {
            None => Ok(()),
            Some(why) => Err(self.paid(repository, why)),
        }
    }
}

/// **The paid limit on EVERY programmatic write** — a repository's backend,
/// wrapped once at wiring (metered build only), so the slow paths that write
/// with `put` and never pass a door are policed by the same [`Quota`]: a mirror
/// pull, a replication copy, a job's finalized upload, a proxy's cache fill (the
/// proxy's primary IS this wrapped backend) and a gRPC `PutArtifact`. What each
/// caller does with the refusal is its own, already-written failure path: the
/// mirror logs the put as failed, replication reports the row `Failed`, the job
/// fails, and a proxy serves the bytes it fetched WITHOUT caching them.
///
/// **A twin is never policed.** It holds the primary's bytes because the primary
/// took them under the primary's limit, and it carries the primary's facts
/// (the estate copies `holger-entitlements/`); refusing its follow would make a
/// twin that silently stops being a copy. A box configured as a twin is never
/// wrapped; one ELECTED later is recognised on every write by its role record
/// (`twin::recorded_role`), so the exemption holds without a restart.
///
/// A [`traits::Decorator`]: every trait method reaches the inner backend by
/// construction; only the two writes that carry bytes are overridden.
pub struct QuotaGuard {
    inner: std::sync::Arc<dyn traits::RepositoryBackendTrait>,
    repository: String,
    quota: std::sync::Arc<Quota>,
    data_root: PathBuf,
}

impl QuotaGuard {
    pub fn wrap(
        inner: std::sync::Arc<dyn traits::RepositoryBackendTrait>,
        repository: impl Into<String>,
        quota: std::sync::Arc<Quota>,
        data_root: PathBuf,
    ) -> std::sync::Arc<dyn traits::RepositoryBackendTrait> {
        std::sync::Arc::new(Self {
            inner,
            repository: repository.into(),
            quota,
            data_root,
        })
    }

    fn admit(&self, incoming: u64) -> anyhow::Result<()> {
        if unpoliced(&self.data_root).is_some() {
            return Ok(());
        }
        self.quota
            .admit(&self.repository, incoming)
            .map_err(|why| anyhow::anyhow!(holger_errcode::METERED_OVER_CAP.tag(why)))
    }
}

/// **The writes the paid limit never polices on this box, and why** — `None`: every
/// write is policed.
///
/// * a TWIN: it holds the primary's bytes, taken under the primary's limit;
/// * a box being RECHARGED from its twin (`twin_follow::RECHARGING`): it serves
///   nothing until the recharge is whole (`holger-twin-15`), so every write it takes
///   is the recharge's own — the twin's copy of bytes the dead primary took under its
///   limit. MEASURED by the private-holger-ops estate-box gate (2026-10-04, R1): a
///   lapsed repository (Suspended: read-only, its data still the customer's) lost
///   every artifact to a recharge — each restore put was refused `[holger-metered-6]`
///   and the new main served 404 for what the old one held.
pub fn unpoliced(data_root: &std::path::Path) -> Option<&'static str> {
    if matches!(crate::twin::recorded_role(data_root), Ok(Some(ref r)) if r.role.is_twin()) {
        return Some("a twin holds the primary's bytes");
    }
    if data_root.join(crate::twin_follow::RECHARGING).exists() {
        return Some("a recharge restores what the primary held");
    }
    None
}

impl traits::Decorator for QuotaGuard {
    fn name(&self) -> &str {
        self.inner.name()
    }
    fn format(&self) -> traits::ArtifactFormat {
        self.inner.format()
    }
    fn is_writable(&self) -> bool {
        self.inner.is_writable()
    }
    fn caps(&self) -> traits::Caps {
        self.inner.caps()
    }
    fn around<T>(
        &self,
        _: traits::Access,
        call: impl FnOnce(&dyn traits::RepositoryBackendTrait) -> anyhow::Result<T>,
    ) -> anyhow::Result<T> {
        call(self.inner.as_ref())
    }
    fn put(&self, id: &traits::ArtifactId, data: &[u8]) -> anyhow::Result<()> {
        self.admit(data.len() as u64)?;
        self.inner.put(id, data)
    }
    fn put_from_reader(
        &self,
        id: &traits::ArtifactId,
        body: &mut dyn std::io::Read,
        size_hint: Option<u64>,
    ) -> anyhow::Result<()> {
        if self.inner.caps().streaming_uploads {
            self.admit(size_hint.unwrap_or(0))?;
        }
        self.inner.put_from_reader(id, body, size_hint)
    }
    /// Reads are not policed: a read in pieces is the inner backend's (lane HOL-RSS).
    fn stream_get(
        &self,
        suburl: &str,
        piece_bytes: usize,
    ) -> anyhow::Result<Option<traits::StreamedGet>> {
        self.inner.stream_get(suburl, piece_bytes)
    }
    // A delete is never refused (it is how a full repository gets back under),
    // and the two HTTP entries are not checked here: the HTTP door asked the
    // same Quota before it read the body (`GatewaySettings::admit_upload`).
}

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

    /// ★ **A recharge restores a lapsed repository whole** (the estate-box gate's R1,
    /// 2026-10-04): while the box is being recharged (`holger-recharging`), no write is
    /// policed; before and after it, every write is.
    #[test]
    fn a_box_being_recharged_is_not_policed_and_one_serving_is() {
        let d = tempfile::tempdir().unwrap();
        assert_eq!(unpoliced(d.path()), None, "a serving primary is policed");
        std::fs::write(
            d.path().join(crate::twin_follow::RECHARGING),
            "recharging from 10.6.0.13:50052\n",
        )
        .unwrap();
        assert!(unpoliced(d.path()).is_some_and(|w| w.contains("recharge")));
        std::fs::remove_file(d.path().join(crate::twin_follow::RECHARGING)).unwrap();
        assert_eq!(
            unpoliced(d.path()),
            None,
            "the recharge whole, the limit is back"
        );
    }

    fn fact(state: State, cap: Option<u64>) -> SignedFact {
        let mut caps = BTreeMap::new();
        if let Some(c) = cap {
            caps.insert(crate::metering::METER_DISK_BYTES.to_string(), c);
        }
        SignedFact {
            tenant: TenantId("crates".into()),
            plan: "holger/crates/2026-09-27+disk_bytes".into(),
            state,
            paid_until_unix_ms: None,
            caps,
            source: "operator".into(),
            signature: vec![0; 64],
            issued_unix_ms: None,
            issued_signature: Vec::new(),
        }
    }

    /// ★ **The customer never sees a cap** (A10 row 18, the owner's rule): over its room an
    /// upload is refused with words that say the room is full and that more is bought, and no
    /// number or disk path; the numbers and where the bytes are go to the operator's log line.
    #[test]
    fn under_the_limit_is_taken_and_over_it_is_refused_without_the_cap() {
        let f = fact(State::Paid, Some(1000));
        assert_eq!(
            over_cap(&f, 400, 600, "archive /d/crates.znippy (400 B)"),
            None,
            "exactly the limit fits"
        );
        let why = over_cap(&f, 400, 601, "archive /d/crates.znippy (400 B)").unwrap();
        assert!(
            why.contains("`crates`") && why.contains("full") && why.contains("more room"),
            "{why}"
        );
        for secret in [
            "1000",
            "1001",
            "601",
            "400",
            "/d/crates.znippy",
            "limit",
            "bytes",
        ] {
            assert!(
                !why.contains(secret),
                "the refusal names no cap ({secret}): {why}"
            );
        }
        let said = operator_line(&f, 400, 601, "archive /d/crates.znippy (400 B)");
        assert!(
            said.contains("1001") && said.contains("1000") && said.contains("/d/crates.znippy"),
            "the operator is told: {said}"
        );
        assert!(
            over_cap(&f, 1000, 0, "x").is_some(),
            "a full repository takes no upload of unknown size"
        );
    }

    #[test]
    fn an_upload_that_fits_needs_no_walk_and_a_refusal_always_reads_the_disk() {
        use ed25519_dalek::Signer;
        let d = tempfile::tempdir().unwrap();
        let files = d.path().join("crates");
        std::fs::create_dir_all(&files).unwrap();
        std::fs::write(files.join("a.tar"), vec![1u8; 1000]).unwrap();
        let key = ed25519_dalek::SigningKey::from_bytes(&[5u8; 32]);
        let roster = Arc::new(BTreeSet::from(["crates".to_string()]));
        let store = Arc::new(
            EntitlementStore::new(d.path().join("e"), roster, &key.verifying_key().to_bytes())
                .unwrap(),
        );
        let mut f = fact(State::Paid, Some(1 << 20));
        f.signature = key
            .sign(&monetize_embed::signing::fact_message(&f))
            .to_bytes()
            .to_vec();
        store.put(&f).unwrap();
        let places: crate::metering::StoragePlaces = [(
            "crates".to_string(),
            vec![crate::repository::StoragePlace {
                kind: "files",
                path: files.clone(),
            }],
        )]
        .into_iter()
        .collect();
        let q = Quota::new(store, Arc::new(places));
        let on_disk = crate::metering::disk_bytes(&q.places.read().unwrap()["crates"])
            .unwrap()
            .0;

        q.admit("crates", 1000).unwrap();
        // Something grows the disk behind the cache's back: the next FITTING upload
        // is still answered from the reading (no walk) …
        std::fs::write(files.join("b.tar"), vec![1u8; 900 << 10]).unwrap();
        q.admit("crates", 1000).unwrap();
        assert_eq!(
            q.readings.lock().unwrap()["crates"].bytes,
            on_disk + 2000,
            "counted, not walked"
        );
        // … but one the reading says would NOT fit is decided by the disk, walked
        // now — and a delete is credited before anybody is told no.
        std::fs::remove_file(files.join("b.tar")).unwrap();
        // The reading (on_disk + 2000) says this is 500 over; the disk says it fits.
        q.admit("crates", (1 << 20) - on_disk - 1500)
            .expect("the delete was credited by a fresh walk");
        assert_eq!(
            q.readings.lock().unwrap()["crates"].bytes,
            (1 << 20) - 1500,
            "re-read from the disk"
        );
        let why = q.admit("crates", 1 << 20).unwrap_err();
        assert!(
            why.contains("is full")
                && !why.contains("1048576")
                && !why.contains(&files.display().to_string()),
            "refused, and no cap or path shown to the customer (A10 row 18): {why}"
        );
    }

    /// ★ g-holger-expiry (audit 2026-09-28): a Paid fact whose period ended long
    /// ago, and that nobody replaced (the front stopped pushing), must NOT keep
    /// write access. holger locks writes by its OWN clock once the paid period
    /// is over — READ-ONLY: uploads refused, reads go on.
    #[test]
    fn a_lapsed_paid_fact_closes_writes_by_holgers_own_clock() {
        use ed25519_dalek::Signer;
        let d = tempfile::tempdir().unwrap();
        let key = ed25519_dalek::SigningKey::from_bytes(&[7u8; 32]);
        let roster = Arc::new(BTreeSet::from(["crates".to_string()]));
        let store = Arc::new(
            EntitlementStore::new(d.path().join("e"), roster, &key.verifying_key().to_bytes())
                .unwrap(),
        );
        let q = Quota::new(
            store.clone(),
            Arc::new(crate::metering::StoragePlaces::default()),
        );
        let now = crate::clock::now_unix_ms().max(0) as u64;
        // Paid until 60 days ago, never renewed: past any grace.
        let mut f = fact(State::Paid, None);
        f.paid_until_unix_ms = Some(now - 60 * 86_400_000);
        f.signature = key
            .sign(&monetize_embed::signing::fact_message(&f))
            .to_bytes()
            .to_vec();
        store.put(&f).unwrap();
        let why = q
            .admit("crates", 1)
            .expect_err("a paid period that ended 60 days ago still took an upload");
        assert!(
            why.contains("read-only") && why.contains("paid until"),
            "{why}"
        );
        // Paid until yesterday: no grace since the non-payment ruling — refused.
        f.paid_until_unix_ms = Some(now - 86_400_000);
        f.signature = key
            .sign(&monetize_embed::signing::fact_message(&f))
            .to_bytes()
            .to_vec();
        store.put(&f).unwrap();
        q.admit("crates", 1)
            .expect_err("a period that ended yesterday is read-only at once");
        // Paid into the future: taken.
        f.paid_until_unix_ms = Some(now + 30 * 86_400_000);
        f.signature = key
            .sign(&monetize_embed::signing::fact_message(&f))
            .to_bytes()
            .to_vec();
        store.put(&f).unwrap();
        q.admit("crates", 1)
            .expect("a running period takes an upload");
    }

    /// ★ The owner's non-payment ruling (2026-09-29): a customer who stops paying
    /// is READ-ONLY AT ONCE — one millisecond past `paid_until` by holger's own
    /// clock, not fourteen days later. monetize mails, and deletes after the ONE box's
    /// grace; holger's own reading must not hand out two free weeks of writes.
    #[test]
    fn a_paid_fact_one_ms_past_paid_until_is_lapsed() {
        let until = 1_790_553_600_000u64;
        let mut f = fact(State::Paid, None);
        f.paid_until_unix_ms = Some(until);
        assert_eq!(
            lapsed(&f, until as i64),
            None,
            "AT paid_until the period still runs"
        );
        let why = lapsed(&f, until as i64 + 1).expect("one ms past paid_until is read-only");
        assert!(
            why.contains("read-only") && why.contains("2026-09-28"),
            "{why}"
        );
        assert!(
            !why.contains("grace"),
            "no grace is granted, so none is named: {why}"
        );
    }

    #[test]
    fn lapsed_is_paid_or_grace_past_the_period_only() {
        let day = 86_400_000i64;
        let mut f = fact(State::Paid, None);
        assert_eq!(
            lapsed(&f, 100 * day),
            None,
            "no paid_until: nothing to lapse"
        );
        f.paid_until_unix_ms = Some((50 * day) as u64);
        assert_eq!(lapsed(&f, 49 * day), None, "before the end: running");
        assert!(
            lapsed(&f, 60 * day).unwrap().contains("read-only"),
            "10 days past: lapsed, no grace"
        );
        assert!(
            lapsed(&f, 65 * day).unwrap().contains("read-only"),
            "15 days past: lapsed"
        );
        f.state = State::Grace;
        assert!(lapsed(&f, 65 * day).is_some());
        f.state = State::Free;
        assert_eq!(lapsed(&f, 65 * day), None, "a free plan has no period");
        f.state = State::Suspended;
        assert_eq!(
            lapsed(&f, 65 * day),
            None,
            "Suspended is refused by over_cap already, with its own sentence"
        );
        f.state = State::Paid;
        assert_eq!(
            lapsed(&f, 0),
            None,
            "no clock reading: never lock on a guess"
        );
        assert_eq!(civil_date(1_790_553_600_000), "2026-09-28");
        assert_eq!(civil_date(0), "1970-01-01");
    }

    /// ★ While a purge walks a repository, EVERY upload is refused by name — even
    /// one to a repository whose fact now says Paid (a payment landing mid-walk)
    /// or that has no fact at all — and the gate opens when the walk ends.
    #[test]
    fn a_repository_being_purged_takes_no_upload_whatever_its_fact_says() {
        use ed25519_dalek::Signer;
        let d = tempfile::tempdir().unwrap();
        let key = ed25519_dalek::SigningKey::from_bytes(&[8u8; 32]);
        let roster = Arc::new(BTreeSet::from(["crates".to_string(), "free".to_string()]));
        let store = Arc::new(
            EntitlementStore::new(d.path().join("e"), roster, &key.verifying_key().to_bytes())
                .unwrap(),
        );
        let mut f = fact(State::Paid, None);
        f.signature = key
            .sign(&monetize_embed::signing::fact_message(&f))
            .to_bytes()
            .to_vec();
        store.put(&f).unwrap();
        let q = Quota::new(
            store.clone(),
            Arc::new(crate::metering::StoragePlaces::default()),
        );
        q.admit("crates", 1).expect("paid, not purged: taken");
        let gate = crate::purge::close(store.root(), "crates").unwrap();
        let why = q
            .admit("crates", 1)
            .expect_err("a repository being purged took an upload");
        assert!(
            why.contains("being purged") && why.contains("crates"),
            "{why}"
        );
        let other = crate::purge::close(store.root(), "free").unwrap();
        assert!(
            q.admit("free", 1).is_err(),
            "no fact is no exemption from the gate"
        );
        drop((gate, other));
        q.admit("crates", 1)
            .expect("the gate opens when the walk ends");
        q.admit("free", 1).expect("and for the factless one");
    }

    #[test]
    fn no_cap_is_not_policed_and_a_suspended_plan_takes_nothing() {
        assert_eq!(
            over_cap(&fact(State::Paid, None), u64::MAX / 2, 1 << 40, "x"),
            None
        );
        assert_eq!(over_cap(&fact(State::Paid, Some(0)), 5, 5, "x"), None);
        assert!(over_cap(&fact(State::Suspended, None), 0, 1, "x")
            .unwrap()
            .contains("Suspended"));
        assert!(over_cap(&fact(State::Retention, Some(1 << 40)), 0, 1, "x").is_some());
    }
}

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

    /// Every trait method reaches the wrapped backend, by construction: around
    /// a backend with every cap the guard answers every method, and around a
    /// core-only one it claims and answers none.
    #[test]
    fn every_method_reaches_the_wrapped_backend() {
        let d = tempfile::tempdir().unwrap();
        // A recharging box polices nothing, so the probes meet the backend.
        std::fs::write(d.path().join(crate::twin_follow::RECHARGING), "probe\n").unwrap();
        let key = ed25519_dalek::SigningKey::from_bytes(&[8u8; 32]);
        let store = Arc::new(
            EntitlementStore::new(
                d.path().join("e"),
                Arc::new(BTreeSet::new()),
                &key.verifying_key().to_bytes(),
            )
            .unwrap(),
        );
        let quota = Arc::new(Quota::new(
            store,
            Arc::new(crate::metering::StoragePlaces::default()),
        ));
        let all: Arc<dyn traits::RepositoryBackendTrait> =
            Arc::new(traits::backend::conformance::Everything::new("all"));
        let core: Arc<dyn traits::RepositoryBackendTrait> =
            Arc::new(traits::backend::conformance::Everything::core_only("core"));
        for inner in [all, core] {
            let g = QuotaGuard::wrap(inner, "r", Arc::clone(&quota), d.path().to_path_buf());
            traits::backend::conformance::check(g.as_ref()).unwrap();
        }
    }
}