Skip to main content

monetize_product/
lib.rs

1//! **`Product` — the plugin that makes monetize speak one product's language.**
2//!
3//! monetize handles MANY products, and each product's plugin lives in this
4//! repository. So monetize is
5//! one service for gunnar.rs, holger.rs, njord, …, and each product is a crate under
6//! `products/<name>` implementing this trait. The product itself carries only the thin
7//! `monetize-embed` hook behind a cargo feature; it never learns what capacity costs.
8//!
9//! ```text
10//!   product box ──Usage (gRPC, the product's own API)──▶ Product plugin ──▶ monetize core
11//!   product box ◀──Entitlement (product's own API)──── Product plugin ◀── monetize core
12//! ```
13//!
14//! # Two directions, both facts
15//!
16//! * [`Product::read_usage`] — what a tenant is consuming, in the product's own units
17//!   (gunnar: pack bytes, read-cache bytes, LFS bytes, tombstoned bytes, open-store RAM;
18//!   holger: whatever holger meters). The plugin maps them to [`Usage`], a flat bag of
19//!   named meters, so core never has product-specific fields.
20//! * [`Product::push_entitlement`] — the verdict, written back over the product's own
21//!   control plane (gunnar: `Entitlement.Set` with `source = payment:<vendor>:<ref>`).
22//!   Signed by monetize's key; the product appends it to its attestation log.
23//!
24//! # A plugin never blocks the product
25//!
26//! Counting is done **inside the product, always, open source** — a self-hoster wants the
27//! numbers too. The plugin only *reads* them. If monetize is down the product keeps
28//! serving on its cached entitlement; the plugin's job on reconnect is to catch up, not
29//! to have been in the request path.
30
31use std::collections::BTreeMap;
32
33/// **Reading a daily `*_today` counter out of a series of readings** — the fold
34/// behind the console's SILENT and FRICTION lists. Pure; see the module doc for
35/// why summing the readings is the wrong arithmetic and why it lives here.
36pub mod flow;
37
38/// A tenant, as the product names it. gunnar: the `Namespace` name (`team/sub`).
39#[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Debug, serde::Serialize, serde::Deserialize)]
40pub struct TenantId(pub String);
41
42/// **Whether the product will actually POLICE a cap on this meter.**
43///
44/// CPU and RAM exist as quota even where gunnar cannot cap them, and that gap is
45/// narrowed without redoing the engine.
46///
47/// An order may cap any declared meter. But a cap on a meter nothing checks is a
48/// promise nobody keeps, and the tenant cannot tell the difference from the
49/// invoice. So the product says, per meter, which kind of promise it is — and
50/// the console shows it beside the number rather than letting every cap look
51/// like a guarantee.
52///
53/// This is the smallest honest way to let CPU and RAM be sold: they ARE bought,
54/// with real money, and they DO change what the box can do — they are simply not
55/// something a git server polices per namespace. Saying so is better than either
56/// refusing to sell them or pretending they are enforced.
57#[derive(Clone, Copy, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
58pub enum Enforcement {
59    /// **The product refuses when over.** A cap here is a wall the tenant hits.
60    /// gunnar: `pack_bytes` (receive-pack refused before the bytes),
61    /// `cache_bytes` (the explode budget).
62    Enforced,
63    /// **Counted and reported, not policed.** The number is true and monetize
64    /// can bill on it or a human can act on it, but nothing refuses. A cap here
65    /// is a threshold, not a wall.
66    Measured,
67    /// **Neither counted nor policed BY THE PRODUCT — it is iron monetize
68    /// bought.** CPU and RAM: a bigger plan at the provider, real money, real
69    /// effect, and no per-namespace check anywhere in the product. The
70    /// provider's invoice is the enforcement.
71    Provisioned,
72}
73
74impl Enforcement {
75    /// One line for the console, beside the cap.
76    pub fn describe(self) -> &'static str {
77        match self {
78            Enforcement::Enforced => "the product refuses when over",
79            Enforcement::Measured => "counted and reported; nothing refuses",
80            Enforcement::Provisioned => "provisioned at the cloud provider; not policed by the product",
81        }
82    }
83
84    /// Is a cap on this meter a wall the tenant will actually hit?
85    pub fn is_a_wall(self) -> bool {
86        matches!(self, Enforcement::Enforced)
87    }
88}
89
90/// **What a cap on this meter is MADE OF**, for the meters where a cap is a
91/// promise of CAPACITY rather than a count — and therefore what UNIT the price
92/// list prices it in and what iron the transaction buys for it.
93///
94/// A plan that sells bytes it never buys is the one bug here that silently
95/// converts money into nothing. Since the plan catalogue went (2026-09-05), an
96/// ORDER names caps and nothing else, and this is the field that turns a cap
97/// into a purchase: `monetize::order::resources_for` translates the DELTA on a
98/// disk-backed meter into a `Resource::Disk`, and `monetize::PriceList` prices
99/// each backing's delta by its own unit.
100///
101/// It lives on the meter and not on the order because only the PRODUCT knows
102/// what its meter is made of — that `pack_bytes` is block storage and `pushes`
103/// is a count of events that costs nothing to promise.
104#[derive(Clone, Copy, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
105pub enum Backing {
106    /// **Bytes of block storage.** A delta here becomes a `Resource::Disk` of
107    /// `ceil(bytes / 2^30)` GiB, and is priced per GiB per month
108    /// (`disk_gib_month` on the price list).
109    DiskBytes,
110    /// **CPU, in millicores (1000 = one core).** Priced per core per month
111    /// (`cpu_core_month`). monetize does NOT translate millicores into a
112    /// provider's plan name (`2xCPU-4GB` is a name, and a table of names goes
113    /// stale every time a provider renames a size), so a delta here buys no
114    /// cloud resource of its own: the ceiling is the product's own measurement
115    /// (`Product::can_absorb`) and the box's declared limit
116    /// (`CloudProvider::ceilings`).
117    CpuMillicores,
118    /// **RAM, in bytes.** Priced per GiB per month (`ram_gib_month`); the same
119    /// ceiling rule as [`Backing::CpuMillicores`].
120    RamBytes,
121}
122
123impl Backing {
124    /// The price-list key this backing is priced under, and the unit it means:
125    /// `disk_gib_month` | `cpu_core_month` | `ram_gib_month`.
126    pub fn unit(self) -> &'static str {
127        match self {
128            Backing::DiskBytes => "disk_gib_month",
129            Backing::CpuMillicores => "cpu_core_month",
130            Backing::RamBytes => "ram_gib_month",
131        }
132    }
133
134    /// Every backing, in the order the price list is written in.
135    pub const ALL: [Backing; 3] = [Backing::DiskBytes, Backing::CpuMillicores, Backing::RamBytes];
136
137    /// The backing a price-list key names, if any.
138    pub fn from_unit(unit: &str) -> Option<Backing> {
139        Backing::ALL.into_iter().find(|b| b.unit() == unit)
140    }
141
142    /// **Price `delta` of this backing at `minor_per_unit_month`, for one
143    /// month**, in minor units. Integer arithmetic, rounded UP to the unit for
144    /// bytes (a customer who asks for one byte over a GiB is sold the next GiB,
145    /// which is what the provider sells us) and exact per millicore for CPU
146    /// (`minor × millicores / 1000`).
147    pub fn price_month(self, delta: u64, minor_per_unit_month: u64) -> u64 {
148        const GIB: u64 = 1 << 30;
149        match self {
150            Backing::DiskBytes | Backing::RamBytes => delta.div_ceil(GIB).saturating_mul(minor_per_unit_month),
151            Backing::CpuMillicores => u64::try_from(u128::from(delta) * u128::from(minor_per_unit_month) / 1000).unwrap_or(u64::MAX),
152        }
153    }
154}
155
156/// **WHERE a meter's number is even meaningful: per tenant, or per product.**
157///
158/// Disk is the whole game, and it is per tenant. CPU and RAM stay in the model,
159/// but for gunnar they are not interesting per user — they are interesting per
160/// product: a tenant does not own cores, the box does.
161///
162/// Orthogonal to [`Enforcement`], and the pair is what makes a fleet total
163/// honest. `Enforcement` says whether a cap is a WALL. Scope says whether adding
164/// one tenant's figure to another's produces anything at all:
165///
166/// * `pack_bytes` is per tenant, so nine tenants' pack bytes sum to the fleet's
167///   pack bytes;
168/// * `cpu_millicores` is per product, so nine tenants' CPU does not sum to
169///   anything — nobody measured a tenant's cores, and adding nine numbers that
170///   were never measured is arithmetic on nothing.
171///
172/// **It is the PRODUCT's call, not a global rule.** gunnar meters disk per tenant
173/// and CPU per product; another product may legitimately meter CPU per user, or
174/// disk only in total. [`Product::meters`] is already that seam, and nothing
175/// outside a product decides which of its meters are per-tenant. `monetize::fleet`
176/// sums only [`Scope::Tenant`], whatever a product declares.
177#[derive(Clone, Copy, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
178pub enum Scope {
179    /// **Measured, or sold, per TENANT.** A fleet figure is the sum over tenants.
180    /// The default, because it is what a meter usually is.
181    Tenant,
182    /// **Measured, or bought, for the WHOLE PRODUCT.** It appears ONCE, on the
183    /// product screen, and never in a per-tenant sum. A cap on it may still be
184    /// sold — CPU and RAM are bought with real money — it simply is not a
185    /// quantity any one tenant holds.
186    Product,
187}
188
189impl Scope {
190    /// May a figure for this meter be summed across tenants? Only [`Scope::Tenant`].
191    pub fn sums_across_tenants(self) -> bool {
192        matches!(self, Scope::Tenant)
193    }
194
195    /// `tenant` | `product` — the wire and JSON spelling, one writer.
196    pub fn name(self) -> &'static str {
197        match self {
198            Scope::Tenant => "tenant",
199            Scope::Product => "product",
200        }
201    }
202}
203
204/// **One meter a product declares**: its name, what it means, and whether a cap
205/// on it is policed. An order may only cap a declared meter, and the console
206/// shows [`Enforcement`] beside the number.
207#[derive(Clone, Copy, PartialEq, Eq, Debug)]
208pub struct Meter {
209    /// The wire name, and the key in [`EntitlementFact::caps`] and [`Usage::meters`].
210    pub name: &'static str,
211    /// One line, the plugin's own words.
212    pub meaning: &'static str,
213    pub enforcement: Enforcement,
214    /// **Does [`Product::read_usage`] carry a reading for this meter?**
215    ///
216    /// A meter exists to do one or both of two jobs: carry a usage READING, and
217    /// accept a CAP. Most do both. Two kinds do not, and conflating them was
218    /// caught by a test rather than by thinking:
219    ///
220    /// * `cpu_millicores`, `ram_bytes` — the product has no idea what was
221    ///   bought; monetize does, from the order. Reporting `0` would read as "you
222    ///   have none", which is worse than saying nothing.
223    /// * `concurrent_transfers` — a cap the engine enforces instantly, with no
224    ///   stored reading to bill from. The limit is real; the gauge does not exist.
225    ///
226    /// So a plugin's `read_usage` must report EXACTLY the meters with this set,
227    /// which is an equality a test can hold in both directions: an undeclared
228    /// reading is a leak, and a declared reading that stopped arriving is a
229    /// silently emptied bill.
230    pub reports_usage: bool,
231    /// **What a cap on this meter is made of**, or `None` when a cap here costs
232    /// nothing to grant (a count of pushes, a number of repositories the
233    /// existing box already holds). `Some` is what makes an order on this meter
234    /// a PURCHASE: the delta is priced by the backing's unit and, for disk,
235    /// bought as iron. See [`Backing`].
236    pub backing: Option<Backing>,
237    /// **Whether this meter's number is per tenant or per product.** See
238    /// [`Scope`]; it is what keeps a fleet total from adding up figures nobody
239    /// measured per tenant.
240    pub scope: Scope,
241}
242
243impl Meter {
244    /// A meter that both reports a reading and may be capped — the common case.
245    pub const fn new(name: &'static str, meaning: &'static str, enforcement: Enforcement) -> Meter {
246        Meter { name, meaning, enforcement, reports_usage: true, backing: None, scope: Scope::Tenant }
247    }
248
249    /// A meter that may be CAPPED but carries no reading. See
250    /// [`Meter::reports_usage`] for the two kinds and why `0` is not a truthful
251    /// substitute.
252    pub const fn cap_only(name: &'static str, meaning: &'static str, enforcement: Enforcement) -> Meter {
253        Meter { name, meaning, enforcement, reports_usage: false, backing: None, scope: Scope::Tenant }
254    }
255
256    /// The same meter, declaring what a cap on it is MADE OF — and so what an
257    /// order on it buys and what the price list prices it by. A
258    /// [`Enforcement::Provisioned`] meter without one is a cap monetize can
259    /// neither price nor buy; gunnar's own test holds its list to that.
260    pub const fn with_backing(self, backing: Backing) -> Meter {
261        Meter { backing: Some(backing), ..self }
262    }
263
264    /// The same meter, measured for the WHOLE PRODUCT rather than per tenant —
265    /// so a fleet total leaves it out instead of adding up figures nobody took.
266    /// See [`Scope`].
267    pub const fn per_product(self) -> Meter {
268        Meter { scope: Scope::Product, ..self }
269    }
270}
271
272/// **What a product says it could still serve, or why it could not say.**
273///
274/// The other half of the oversell number: `sum(what has been sold) − servable`.
275/// Without it monetize could sell ten tenants 10 GiB each on a box with 55 GiB
276/// servable and nothing would object until the sixth push.
277///
278/// The two arms are the distinction `BASE-MODEL.md` rule 3 turns on and the same
279/// one [`Product::can_absorb`] already makes: **an unanswered capacity question
280/// is a measurement that did not happen, not a full disk and not an empty one.**
281/// The live gunnar.rs appliance predates gunnar's `Admin.Capacity` RPC and
282/// answers [`Servable::Unmeasured`] today; a fleet total that turned that into a
283/// zero would report every deployment as catastrophically oversold, and one that
284/// turned it into infinity would report every deployment as fine. Neither is a
285/// measurement.
286/// **Can this product's store grow while it runs?** Carried on
287/// [`Servable::Measured`] beside the bytes, because the two answer different
288/// questions and an operator acts differently on each: a full box that can grow
289/// wants a disk; a full box that is SEALED wants its set enlarged by an operator
290/// maintenance operation (the volumes it already has, grown in place) or a fresh
291/// install onto a larger one, and a disk bought for it is billed and invisible.
292///
293/// Each arm carries the PRODUCT's own remediation sentence (gunnar's
294/// `remediation()`), so the console renders what the product said rather than a
295/// paraphrase of it — `UI.md` screen 2's `⚠ cannot grow` badge holds exactly
296/// that paragraph.
297#[derive(Clone, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
298pub enum Growth {
299    /// Cannot gain capacity while it runs, and nothing on the machine can give
300    /// it any. `GROWTH_SEALED` on gunnar's wire.
301    Sealed(String),
302    /// The product will not grow the set itself, but an operator can, out of
303    /// band. `GROWTH_OPERATOR_ONLY`.
304    OperatorOnly(String),
305    /// The product could not tell. NOT a promise that it can grow.
306    Unknown(String),
307    /// **Grows at its next restart** once every member volume has been
308    /// enlarged to one equal size (gunnar's `GROWTH_AT_RESTART`, 2026-09-14).
309    /// Still no hot-plug: a disk ATTACHED to it is as invisible as under
310    /// `Sealed`; what helps is the outside actor enlarging the members
311    /// ([`ApplianceGrow`] + `monetize_cloud::grow`).
312    AtRestart(String),
313    /// **Grows without a restart**: the product drains, unmounts and grows into
314    /// enlarged members while its control plane stays up (gunnar's
315    /// `GROWTH_AT_RUNTIME`). The shape [`ApplianceGrow`] drives.
316    AtRuntime(String),
317}
318
319impl Growth {
320    /// The wire word: `sealed` | `operator_only` | `unknown` | `at_restart` |
321    /// `at_runtime`.
322    pub fn name(&self) -> &'static str {
323        match self {
324            Growth::Sealed(_) => "sealed",
325            Growth::OperatorOnly(_) => "operator_only",
326            Growth::Unknown(_) => "unknown",
327            Growth::AtRestart(_) => "at_restart",
328            Growth::AtRuntime(_) => "at_runtime",
329        }
330    }
331
332    /// The product's own sentence about what to do.
333    pub fn detail(&self) -> &str {
334        match self {
335            Growth::Sealed(s) | Growth::OperatorOnly(s) | Growth::Unknown(s) | Growth::AtRestart(s) | Growth::AtRuntime(s) => s,
336        }
337    }
338}
339
340#[derive(Clone, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
341pub enum Servable {
342    /// The product measured its own store.
343    Measured {
344        /// **Bytes that may still be handed to tenants**, beyond what they already
345        /// hold, after whatever reserve the product keeps for itself. gunnar's
346        /// `servable_bytes` (`free − disk floor`) — never its `free_bytes`, which
347        /// includes a reserve monetize must not sell.
348        servable_bytes: u64,
349        /// The whole store, reserve and all. `used + servable_bytes` is what the
350        /// fleet could grow to; `total_bytes` is larger than that by the reserve
351        /// and by anything on the filesystem that is not tenant data. 0 when the
352        /// product measures what is left but not what there is in total.
353        total_bytes: u64,
354        /// **Whether the store can EVER hold more than `total_bytes`.** A
355        /// measurement of the box, independent of the bytes: gunnar's
356        /// `Admin.Capacity` answers it as `growth`, and it is the field that
357        /// stops "buy another disk" being the reflex answer to a full fleet.
358        growth: Growth,
359        /// **The box's MEASURED ceilings on product-scope meters**, by the
360        /// product's own meter name: gunnar's `cpu_millicores_total` and
361        /// `ram_bytes_total` (Admin.Capacity, since gunnar 744bf72d) land here
362        /// as `cpu_millicores` / `ram_bytes`. A meter ABSENT here was not
363        /// measured — the wire spells that as an absent field, never as 0, and
364        /// a reader must never take a 0 as a ceiling. An order that would
365        /// raise a product-scope cap past a number here is refused by name;
366        /// the box's DECLARED ceilings (`CloudProvider::ceilings`) carry only
367        /// for a meter this map does not hold.
368        ceilings: BTreeMap<String, u64>,
369    },
370    /// **It could not be asked, or would not say — which is not zero.** The
371    /// string names WHICH: an RPC the deployed build predates, a control plane
372    /// that is down, a plugin that was never wired.
373    Unmeasured(String),
374}
375
376/// One named meter. Names are the product's, documented in its plugin crate, e.g.
377/// `pack_bytes`, `cache_bytes`, `lfs_bytes`, `tombstoned_bytes`, `open_store_ram_bytes`,
378/// `pushes`, `anonymous_reads`. Values are the product's units, usually bytes or counts.
379#[derive(Clone, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
380pub struct Usage {
381    pub meters: BTreeMap<String, u64>,
382    pub measured_at_unix_ms: u64,
383}
384
385/// **One tenant's FLOW for one UTC day** — what MOVED, as against what is held.
386///
387/// Every number in [`Usage`] is a STOCK reading: it answers *how much is on the
388/// product's disk*, and it reads exactly the same for a customer who worked
389/// this morning and one who last touched the product in March. monetize could
390/// therefore bill nine tenants correctly and not know that six of them had
391/// stopped. This is the other kind, and it is what the SILENT and FRICTION
392/// lists are made of.
393///
394/// **The meters are the product's own names**, exactly as [`Usage::meters`] is,
395/// and for the same reason: core has never held a product-specific field and
396/// must not gain one here. gunnar fills `bytes_in`, `bytes_out`, `pushes`,
397/// `fetches`, `lfs_bytes_in`, `lfs_bytes_out`, `refused`, its three
398/// `refused_*` reasons, `auth_failures`, `distinct_principals` and the two
399/// `last_*_unix_ms` timestamps. Another product will fill something else, and
400/// nothing here needs to know.
401///
402/// **A day with no traffic has NO ROW.** Not a row of zeroes: the caller knows
403/// which days it asked for, and zero-filling would put a bar of height zero
404/// beside a bar that means *not counted*.
405#[derive(Clone, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
406pub struct ActivityDay {
407    /// Midnight UTC of the day, in Unix milliseconds.
408    pub day_unix_ms: u64,
409    /// The product's own meter names. See the type's doc.
410    pub meters: BTreeMap<String, u64>,
411}
412
413/// **A tenant's kept activity, and how far back the product remembers.**
414///
415/// The second half is not decoration. An empty `days` means one of two opposite
416/// things — *nothing happened* or *you asked about a time the product no longer
417/// remembers* — and only `retained_days` can tell them apart. A console that
418/// drew an empty series as "silent" without it would report every tenant on a
419/// freshly installed box as a lapsed customer.
420#[derive(Clone, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
421pub struct Activity {
422    /// Oldest first. Days with no traffic are absent.
423    pub days: Vec<ActivityDay>,
424    /// How many days the product keeps. `0` when it would not say.
425    pub retained_days: u64,
426}
427
428/// The verdict monetize pushes back. Product-agnostic; the plugin maps it to the
429/// product's own enum (gunnar: `EntitlementState`).
430#[derive(Clone, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
431pub struct EntitlementFact {
432    pub tenant: TenantId,
433    /// **The ORDER this verdict came from** — the ledger reference
434    /// (`<product>/<tenant>/<date>+<caps>`) for a purchase, an operator's own
435    /// label (or nothing) for a hand-set fact. The field is still called `plan`
436    /// on the wire and in the signed canonical form (`monetize_embed::signing`),
437    /// because gunnar verifies that form and a renamed field would invalidate
438    /// every signature a deployed gunnar checks; the CONTENT is an order
439    /// reference since the plan catalogue went on 2026-09-05.
440    pub plan: String,
441    pub state: State,
442    pub paid_until_unix_ms: Option<u64>,
443    /// Per-meter caps the product enforces itself (gunnar: `pack_quota_bytes`,
444    /// `explode_budget_bytes`). Absent = product default.
445    pub caps: BTreeMap<String, u64>,
446    /// `operator` | `payment:<vendor>:<reference>` — lands in the product's attestation log.
447    pub source: String,
448    /// Ed25519 over the canonical JSON of the fields above, by monetize-server's key.
449    /// **The V1 form** — it does NOT cover [`EntitlementFact::issued_unix_ms`], and
450    /// that is deliberate: it is the signature an appliance built before
451    /// 2026-09-17 computes, and it must keep verifying there for ever. See
452    /// [`Self::issued_signature`].
453    pub signature: Vec<u8>,
454    /// **When monetize issued this verdict — the field that makes a fact good
455    /// ONCE.**
456    ///
457    /// Without it an `EntitlementFact` is replayable for ever: capture a `Paid`
458    /// fact, wait for the tenant to lapse, push the captured bytes back, and the
459    /// signature still verifies because it is a real signature. `paid_until`
460    /// cannot tell the two apart — a lapse keeps the date and moves the ladder —
461    /// so the only thing that can is something MONOTONIC inside the signed form.
462    /// A product refuses a fact that is not newer than the one it holds
463    /// (gunnar: `EntitlementSlot::set`).
464    ///
465    /// Unix milliseconds, and not a sequence number, for the same reason
466    /// [`crate`]'s sibling [`monetize_embed::signing::Snapshot`] chose one: a
467    /// clock needs no durable per-tenant counter on monetize's side, so a
468    /// restored ledger cannot rewind one and mint facts every appliance in the
469    /// field then refuses for ever. One idea in this system, not two.
470    ///
471    /// `None` is a fact signed before this field existed. It stays legal, and a
472    /// product accepts it — until that product has seen ONE stamped fact for the
473    /// tenant, after which the unstamped form is a downgrade and is refused.
474    #[serde(default, skip_serializing_if = "Option::is_none")]
475    pub issued_unix_ms: Option<u64>,
476    /// **The V2 signature**: Ed25519 over the canonical JSON of every field
477    /// above INCLUDING `issued_unix_ms`
478    /// (`monetize_embed::signing::fact_message_issued`).
479    ///
480    /// Two signatures and not one, because a fact has to be readable by two
481    /// generations of appliance at once. An appliance that predates this field
482    /// reads only fields 1-7 off the wire, computes the V1 form, and checks
483    /// [`Self::signature`] — so it accepts a stamped fact unchanged, and a
484    /// paying customer on an un-upgraded box loses nothing. An appliance that
485    /// knows the field checks BOTH, so the issue time is signed and cannot be
486    /// added, moved or bumped by whoever relays the fact.
487    ///
488    /// Empty exactly when `issued_unix_ms` is `None`; neither is legal without
489    /// the other.
490    #[serde(default, skip_serializing_if = "Vec::is_empty")]
491    pub issued_signature: Vec<u8>,
492}
493
494/// The ladder. Numbers (grace/retention days) are the deployment's policy
495/// (`monetize::Policy`), not the enum's.
496#[derive(Clone, Copy, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
497pub enum State {
498    Free,
499    Paid,
500    Grace,
501    Suspended,
502    Retention,
503}
504
505#[derive(Clone, Debug)]
506pub enum ProductError {
507    /// The product's control plane refused (auth, unknown tenant).
508    Refused(String),
509    /// Product unreachable. Retryable; nothing was written.
510    Unavailable(String),
511}
512
513impl std::fmt::Display for ProductError {
514    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
515        match self {
516            ProductError::Refused(r) => write!(f, "product refused: {r}"),
517            ProductError::Unavailable(r) => write!(f, "product unavailable: {r}"),
518        }
519    }
520}
521impl std::error::Error for ProductError {}
522
523/// **Where the room for a target comes from, as the product measured it** —
524/// what [`Product::can_absorb`] answers when it can serve.
525///
526/// A yes is two different facts, and the difference is money. gunnar's
527/// `Admin.Capacity` answers a `verdict` and a `growth`, and the three readings
528/// of them a seller can act on are: there is room already (buy nothing), there
529/// is no room but a bought disk will be used (buy, and somebody has to extend a
530/// filesystem), and there is no room and a bought disk will NOT be used (refuse).
531/// The third is a refusal and never reaches this type. Before this type existed
532/// a yes was `Ok(())` and the cloud was asked to `ensure` a disk regardless. With
533/// the front's configuration (`pool_reserve_gib` covering the whole data set)
534/// UpCloud's pool arithmetic answers "short" for every order, so every yes
535/// bought and attached a disk to an appliance that had just said it had room —
536/// read from the code and the config, not from a bill.
537///
538/// The transaction turns it into `monetize_cloud::Room` for the provider, so
539/// the cloud is told what the product measured and never has to know which
540/// product that was.
541#[derive(Clone, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
542pub enum Absorb {
543    /// **Served out of what the product already has.** Nothing may be bought
544    /// for it: a disk bought on top of this answer is billed and not needed.
545    OnHand,
546    /// **Short today, and bought iron becomes usable** — by the hand `note`
547    /// names, in the product's own words (gunnar: an operator extends the
548    /// filesystem out of band, and gunnar measures the larger one on its next
549    /// look). Carried onto the purchase row so the ledger records that
550    /// somebody still has a job to do after the disk is attached.
551    WithIron { note: String },
552}
553
554/// **What a metered verb answered** — see [`Product::metered_door`]. Three
555/// arms, because "nobody could ask" is neither a paid build nor a community
556/// one, and a deployment that sells against any of the last two is selling
557/// something it cannot deliver.
558#[derive(Clone, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
559pub enum MeteredDoor {
560    /// **The box answered a metered verb.** It has the paid services compiled
561    /// in; the string is whatever the plugin wants to record about the proof
562    /// (the verb it called, the build it saw).
563    Enforcing(String),
564    /// **The box answered `UNIMPLEMENTED`.** A community build. Every paid
565    /// signup against it will take the money and fail to grant the
566    /// entitlement, so a deployment that sells is misconfigured, loudly.
567    Absent(String),
568    /// **Nobody could ask** — the box is down, the plugin has no probe, nothing
569    /// has run yet. NOT a yes and NOT a no; carries which.
570    Unproven(String),
571}
572
573impl MeteredDoor {
574    /// `enforcing` | `absent` | `unproven` — the wire, JSON and log spelling,
575    /// one writer.
576    pub fn name(&self) -> &'static str {
577        match self {
578            MeteredDoor::Enforcing(_) => "enforcing",
579            MeteredDoor::Absent(_) => "absent",
580            MeteredDoor::Unproven(_) => "unproven",
581        }
582    }
583    /// The plugin's own words about the answer.
584    pub fn detail(&self) -> &str {
585        match self {
586            MeteredDoor::Enforcing(s) | MeteredDoor::Absent(s) | MeteredDoor::Unproven(s) => s,
587        }
588    }
589    /// **Has the paid door been PROVEN to be there?** False for `Unproven`,
590    /// which is the whole point of the arm: a deployment may not call itself
591    /// healthy on the strength of a question nobody answered.
592    pub fn proven(&self) -> bool {
593        matches!(self, MeteredDoor::Enforcing(_))
594    }
595}
596
597/// The plugin seam. Sync for the same reason as the vendor traits.
598pub trait Product: Send + Sync {
599    /// `gunnar`, `holger`, `njord`. Also the first segment of every payment reference.
600    fn id(&self) -> &'static str;
601    /// The meters this product declares — the UI and the order form read this,
602    /// so an order can only cap a meter that exists, and the console can say
603    /// whether a cap on it is policed ([`Enforcement`]).
604    fn meters(&self) -> &[Meter];
605
606    fn list_tenants(&self) -> Result<Vec<TenantId>, ProductError>;
607    fn read_usage(&self, tenant: &TenantId) -> Result<Usage, ProductError>;
608
609    /// **What the tenant has been DOING, day by day** — the flow series behind
610    /// the console's SILENT and FRICTION lists.
611    ///
612    /// [`Product::read_usage`] answers *how much is held*. Only this answers
613    /// *is this customer using the product, has it gone quiet, and is it being
614    /// refused* — and no stock meter has ever been able to.
615    ///
616    /// The window is Unix milliseconds, inclusive, resolved by the PRODUCT to
617    /// whole UTC days: `0` for `since` means the whole kept window, `0` for
618    /// `until` means now. The product is the half that knows what it keeps, and
619    /// a second arithmetic here would be a second answer to one question.
620    ///
621    /// # The default REFUSES BY NAME
622    ///
623    /// [`Product::purge_tenant`]'s rule, for the same shape of danger. The
624    /// permissive answer here is an empty series, and an empty series is
625    /// rendered as *this tenant has gone quiet* — so a product that never
626    /// implemented this would put every one of its customers on the SILENT list
627    /// and an operator would go and ask them why they had stopped. A refusal
628    /// naming the product is the safe wrong answer, and it says which plugin
629    /// owes the work.
630    fn read_activity(
631        &self,
632        tenant: &TenantId,
633        since_unix_ms: u64,
634        until_unix_ms: u64,
635    ) -> Result<Activity, ProductError> {
636        let _ = (tenant, since_unix_ms, until_unix_ms);
637        Err(ProductError::Refused(format!(
638            "the {} plugin cannot report a tenant's activity: it has no flow series, so an empty              answer here would read as a customer who had gone quiet rather than as a question              nobody asked the product.",
639            self.id()
640        )))
641    }
642    fn push_entitlement(&self, fact: &EntitlementFact) -> Result<(), ProductError>;
643
644    /// **Can this product actually DELIVER these caps to this tenant, today —
645    /// and out of what?** `Ok` says where the room comes from ([`Absorb`]);
646    /// `Err(reason)` is a refusal that names what is missing, in words for an
647    /// OPERATOR (the transaction gives a customer its own sentence instead).
648    ///
649    /// # Why it exists
650    ///
651    /// Everything else in monetize checks whether the money can be taken and
652    /// whether the iron can be bought. Nothing asked the third question, and it
653    /// is the one that decides whether the sale is honest: **once the disk is
654    /// attached, can the product use it?**
655    ///
656    /// For gunnar today the answer is often NO. The appliance's `/data` is a
657    /// 4x75 GB raid0 xfs set laid once at install (`InstallMode::Fresh`), there
658    /// is no runtime data-set growth, and gunnar is PID 1 with no shell — so a
659    /// disk monetize buys and attaches is **billed and invisible**. Selling a
660    /// bigger quota there does not fail; it succeeds, charges the customer, signs
661    /// a fact promising capacity, and the capacity is not there. That is the
662    /// worst failure shape this system has: money moved, everything green, the
663    /// promise hollow.
664    ///
665    /// # The contract
666    ///
667    /// * `caps` is the FULL target cap set, not a delta — the product is asked
668    ///   about the world it would have to serve, not about the change.
669    /// * It is asked **before the reserve**, so a refusal costs nothing and
670    ///   nothing has to be unwound. See
671    ///   `monetize::transaction::Transaction::increase`.
672    /// * `Ok` is a claim, not a shrug. A plugin that cannot tell must say so
673    ///   in an `Err`, because a plugin that guesses yes is indistinguishable
674    ///   from one that knows, right up until a tenant is charged for nothing.
675    /// * `Ok(Absorb::OnHand)` forbids a purchase and `Ok(Absorb::WithIron)`
676    ///   permits one. A product whose store cannot use bought iron must never
677    ///   answer `WithIron`: it refuses instead, and says what would help.
678    /// * It has **no default implementation**, deliberately. A default `Ok(())`
679    ///   would let a product that never thought about this answer yes forever,
680    ///   which is exactly the silence this method exists to break.
681    ///
682    /// It may talk to the product (it is the plugin's own control plane), so it
683    /// may fail for the usual reasons; report those as a refusal with the reason
684    /// in it rather than inventing a yes.
685    fn can_absorb(&self, tenant: &TenantId, caps: &BTreeMap<String, u64>) -> Result<Absorb, String>;
686
687    /// **How much this product could still serve, across the whole product.**
688    ///
689    /// The denominator of `UI.md`'s `184 / 300 GiB` and the servable half of the
690    /// oversell number (`monetize::fleet`). [`Product::can_absorb`] asks the same
691    /// box a narrower question — *can you take THIS tenant to THIS cap* — and
692    /// answers yes or no; this one asks for the figure, because a fleet total
693    /// needs a number and not a verdict.
694    ///
695    /// **It has no default implementation, for [`Product::can_absorb`]'s reason.**
696    /// A default would let a product that never thought about capacity answer
697    /// forever, and whichever constant it returned would be a lie: 0 reads as a
698    /// full disk, `u64::MAX` reads as an empty one, and the truth for a product
699    /// that cannot measure is [`Servable::Unmeasured`] — which the plugin must
700    /// say in its own words, naming what is missing.
701    ///
702    /// It may talk to the product, so it may fail for the usual reasons; report
703    /// those as `Unmeasured` with the reason in them rather than inventing a
704    /// number.
705    fn servable(&self) -> Servable;
706
707    /// **Is the PAID DOOR actually there?** One call to a verb only a metered
708    /// build answers, so a deployment that sells can prove the box it sells
709    /// against can enforce what it sold.
710    ///
711    /// # Why this is not covered by anything else
712    ///
713    /// Everything the poller does — `list_tenants`, `read_usage` — is an OPEN
714    /// build's service. gunnar's community ISO answers both perfectly, and
715    /// `poll tenants=11 failures=0` therefore proves connectivity and nothing
716    /// whatever about entitlements. The first thing that finds out is a paying
717    /// customer: the sink creates the principal, pushes the entitlement,
718    /// `Entitlement.Set` answers `UNIMPLEMENTED`, and the signup is refused
719    /// with the money already taken (`monetize_server::signup::Owed`). The last
720    /// proof this estate's appliance is a metered build is a journal line from
721    /// 2026-09-15.
722    ///
723    /// So: a READ-ONLY metered verb, asked on a schedule. It must not create,
724    /// change or delete anything — it is called on every poll tick, against a
725    /// live box, forever.
726    ///
727    /// # The default is UNPROVEN, and unproven is not "yes"
728    ///
729    /// Unlike [`Product::can_absorb`] and [`Product::servable`], which have no
730    /// default because every constant they could return is a lie about
731    /// capacity, this one has a safe answer that is not a claim:
732    /// [`MeteredDoor::Unproven`], naming the plugin that owes the work. A
733    /// default of `Enforcing` would let every plugin that never thought about
734    /// it assert the one thing this exists to check.
735    fn metered_door(&self) -> MeteredDoor {
736        MeteredDoor::Unproven(format!(
737            "the {} plugin has no metered-door probe, so nothing has asked this box whether it can enforce an entitlement at all",
738            self.id()
739        ))
740    }
741
742    /// **Delete everything this tenant has in the product, and keep them locked
743    /// out while it happens.**
744    ///
745    /// The product half of a super purge. `monetize` can give the IRON back on
746    /// its own — `transaction::release_tenant` and
747    /// `CloudProvider::release_for` — and it cannot delete a byte of what is on
748    /// it, because only the product knows what a tenant's data IS. In gunnar it
749    /// is every repository in the account plus two classes of bytes that are
750    /// not in the catalog at all; nothing on this side of the seam could
751    /// enumerate that.
752    ///
753    /// # It must LOCK, and the lock is the product's
754    ///
755    /// A purge is not atomic — it walks and reclaims over minutes while the
756    /// product goes on serving — so anything the tenant does during the walk
757    /// lands behind it. The lockout that prevents that can only live where the
758    /// requests arrive, which is the product. gunnar's is `Accounts.Gate`, and
759    /// it is total rather than read-only because in gunnar a read creates
760    /// things: a fetch explodes objects into a cache the purge just unlinked.
761    ///
762    /// [`Purged::still_locked`] carries the outcome back, because a product
763    /// that emptied a tenant and could not let them back in has left an account
764    /// nobody can use, and that must not read as success anywhere above here.
765    ///
766    /// # The default REFUSES BY NAME
767    ///
768    /// Unlike [`Product::can_absorb`] and [`Product::servable`], which have no
769    /// default at all because every constant they could return is a lie about
770    /// capacity, this one's dangerous answer is a permissive `Ok` — a product
771    /// that never implemented it reporting a tenant's data gone when it is
772    /// still there, after which a super purge would cheerfully destroy the
773    /// disks it is on. A refusal naming the product is the safe wrong answer
774    /// and it says which product owes the work.
775    fn purge_tenant(&self, tenant: &TenantId, reason: &str) -> Result<Purged, ProductError> {
776        let _ = (tenant, reason);
777        Err(ProductError::Refused(format!(
778            "the {} plugin cannot purge a tenant's data: it has no purge verb, so nothing here \
779             can promise the tenant's bytes are gone. Empty the tenant in the product itself \
780             before releasing its resources.",
781            self.id()
782        )))
783    }
784
785    /// **The product's own half of a data-set growth**, if it has one. `None`
786    /// — the default — is a product whose set cannot be grown at runtime from
787    /// outside; monetize then refuses a growth by name before touching a
788    /// volume. See [`ApplianceGrow`].
789    fn grow(&self) -> Option<&dyn ApplianceGrow> {
790        None
791    }
792
793    /// **The TWIN's own half of ITS data-set growth**, if the product has a twin whose
794    /// control plane monetize can reach. Owner ruling 2026-09-22 (T21): the twin, built
795    /// from the same medium, grows EXACTLY like main — the same [`ApplianceGrow`] verbs on
796    /// the twin's own control plane (drain, unmount, wait, grow, mount; `maintenance`
797    /// meanwhile) around the cloud's same `grow_set`, and it is NEVER stopped or
798    /// restarted. `None` — the default — is a product whose twin cannot be grown that
799    /// way; its twin-growth lane then holds by name rather than stop the box.
800    fn twin_grow(&self) -> Option<&dyn ApplianceGrow> {
801        None
802    }
803
804    /// **The product's own half of a restore from its twin**, if it has one.
805    /// `None` — the default — is a product that cannot be re-laid from
806    /// outside; a failed growth then parks in `failed` and an operator's
807    /// `cargo xtask restore main` is what is left. See [`ApplianceRestore`].
808    /// Named `restore_lane`, not `restore`: a product implements BOTH this
809    /// trait and [`ApplianceRestore`], whose own verb is `restore`, and two
810    /// methods of that name on one type make every call site ambiguous
811    /// (E0034). The lane accessor is the one that moves.
812    fn restore_lane(&self) -> Option<&dyn ApplianceRestore> {
813        None
814    }
815
816    /// **The product's half of replacing a dead twin**, if it has one. `None`
817    /// — the default — is a product that cannot adopt a standby from
818    /// outside; a lost twin then stays lost until an operator acts. See
819    /// [`ApplianceTwinship`].
820    fn twinship(&self) -> Option<&dyn ApplianceTwinship> {
821        None
822    }
823
824    /// **The product's twins, as its primary hears them** — one row per twin:
825    /// the heartbeat state, and the fill of the twin's data volume the twin
826    /// reported on its last poll. What the batcher's twin-fill trigger reads.
827    /// The default is no twins: a product without a twin has nothing to grow.
828    fn twins(&self) -> Result<Vec<TwinFill>, ProductError> {
829        Ok(Vec::new())
830    }
831
832    /// **Whether a tenant-presented actor ticket can be verified at all by this
833    /// product.** False means a console must not offer self-service renewal —
834    /// there is nothing here that could tell a customer from a stranger, so the
835    /// route must not exist rather than existing and refusing.
836    ///
837    /// Reported on the wire as `ProductInfo.tenant_renewal`.
838    fn tenant_renewal(&self) -> bool {
839        false
840    }
841
842    /// **May the bearer of this ticket act for `tenant`? Default DENY.**
843    ///
844    /// The ticket is the product's own appliance vouching that the human
845    /// driving a console holds the tenant they are paying for
846    /// (`monetize_embed::ticket`). monetize's gRPC surface authenticates one
847    /// shared bearer — an empty one means open — so this is the ONLY thing that
848    /// can tell "alice renewing alice" from "somebody renewing alice".
849    ///
850    /// # Why the argument is raw bytes
851    ///
852    /// `monetize-embed` — which owns `ActorTicket` and `verify_ticket` —
853    /// depends on THIS crate, so this crate cannot name that type without a
854    /// dependency cycle. The verification therefore happens in the PLUGIN,
855    /// which may depend on `monetize-embed`, and the seam carries the opaque
856    /// bytes it was handed. That is not a compromise: the plugin is also the
857    /// only half that holds the appliance's key, so it is where the check
858    /// belongs whichever way the crates pointed.
859    ///
860    /// # Why the default refuses
861    ///
862    /// A plugin is out of tree and nobody edits it when a path like this ships.
863    /// A default of `Allowed` would silently turn every such plugin into one
864    /// that honours signed bytes it holds no key for; a default that REFUSES
865    /// makes an unaudited product safe by omission rather than by somebody
866    /// remembering. The cost of the wrong default here is a customer who cannot
867    /// renew themselves; the cost of the other one is a stranger who can.
868    /// `purpose` and `caps` are what the REQUEST says, and they are arguments
869    /// rather than things the plugin reads off the ticket, because a verifier
870    /// cannot compare a claim against itself. A ticket names the verb it
871    /// authorises and — for an order — the amount it covers; the plugin's job
872    /// is to check both against the call that arrived. Passing them in is what
873    /// makes "this ticket is for a different order" a refusal that no
874    /// implementation can forget to make.
875    ///
876    /// `caps` is empty for a renewal, and a plugin must refuse a renewal ticket
877    /// that carries any.
878    fn may_act_for(
879        &self,
880        _tenant: &TenantId,
881        _purpose: &str,
882        _caps: &std::collections::BTreeMap<String, u64>,
883        _ticket: &[u8],
884    ) -> Result<ActorVerdict, ProductError> {
885        Ok(ActorVerdict::Refused("this product cannot verify a tenant-presented ticket".into()))
886    }
887}
888
889/// **What a product says about a ticket somebody presented.**
890///
891/// Not a `bool` and not a `Result<(), ProductError>`: a refusal is a normal,
892/// expected answer that carries a SENTENCE — the words a caller may show — and
893/// a `ProductError` means something else entirely (the product's control plane
894/// is down, the question could not be asked). Conflating the two would let an
895/// unreachable appliance read as a refused ticket, or worse, the reverse.
896#[derive(Clone, PartialEq, Eq, Debug)]
897pub enum ActorVerdict {
898    /// The ticket verifies, names this tenant, this product and this verb, is
899    /// inside its life, and has not been spent before.
900    Allowed,
901    /// Refused, and why — in words a customer surface may show. A verifier must
902    /// not name the ticket's own tenant back to whoever asked about another
903    /// one; see `monetize_embed::verify_ticket`.
904    Refused(String),
905}
906
907/// One twin as its primary last heard from it (gunnar's `TwinHeartbeat`).
908#[derive(Clone, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
909pub struct TwinFill {
910    pub principal: String,
911    pub address: String,
912    /// `ok` | `behind` | `silent`.
913    pub state: String,
914    /// Wall clock of the twin's last poll; 0 = never heard.
915    pub last_seen_unix_ms: i64,
916    pub lag_entries: u64,
917    pub disk_total_bytes: u64,
918    pub disk_used_bytes: u64,
919    /// used × 1000 / total; 0 when total is unknown.
920    pub fill_permille: u32,
921}
922
923impl TwinFill {
924    /// The primary can still hear it: `ok` or `behind`, never `silent` or a word we do not know.
925    pub fn visible(&self) -> bool {
926        matches!(self.state.as_str(), "ok" | "behind")
927    }
928}
929
930// ── the appliance's half of a data-set growth ───────────────────────────────
931
932/// **What the appliance does INSIDE the box while the outside actor grows its
933/// volumes** (`DATA-SET-GROWTH-FLOW.md` §0 ruling 1, T6): drain, unmount, wait
934/// for the members to come back larger, run its engine, mount, reopen. monetize
935/// drives it — flush → start → wait `unmounted` → cloud steps → resume → wait
936/// `done|failed` — and every verb here is one call on the product's control
937/// plane. Vendor-neutral: the words are the phases the flow document names,
938/// and the go-ahead is opaque bytes the product verifies on its own terms.
939pub trait ApplianceGrow: Send + Sync {
940    /// **Prove the twin is caught up** (T11, step 0) — flush replication to
941    /// every standby, bounded by `timeout_secs` (0 = the product's own ceiling),
942    /// and report. Changes no role and no roster.
943    fn flush(&self, timeout_secs: u32) -> Result<FlushReport, ProductError>;
944    /// Begin the growth: drain, unmount, wait for the members. `go_ahead` is
945    /// the signed approval (see `monetize_embed::signing::go_ahead_message`);
946    /// `target_sectors_per_member` is in 512-byte sectors, 0 = "whatever every
947    /// member comes back larger at". Answers the status after the call.
948    fn start(&self, target_sectors_per_member: u64, go_ahead: &[u8]) -> Result<GrowStatus, ProductError>;
949    /// The phase and the members, as the product sees them. **Must answer
950    /// while the set is unmounted and, where the product offers it, without a
951    /// credential** — the outside actor decides whether to re-attach from it.
952    fn status(&self) -> Result<GrowStatus, ProductError>;
953    /// "Look again": the members are back. With `give_up`, stop waiting and
954    /// serve whatever is there at whatever size it is.
955    fn resume(&self, give_up: bool) -> Result<GrowStatus, ProductError>;
956}
957
958// ── the appliance's half of a RESTORE from its twin ────────────────────────
959
960/// **What the appliance does when the data set is gone and the twin still has
961/// it** — `DATA-SET-GROWTH-FLOW.md` ruling 2: *"If the grow fails, gunnar
962/// re-lays (reformats) the data set and pulls EVERYTHING back from the twin.
963/// The reformat must never run unless the twin is proven caught up first."*
964///
965/// Gap **G3** is this verb on the appliance's side; what is here is the seam
966/// monetize drives it through, shaped exactly like [`ApplianceGrow`]: one call
967/// to start, one to poll, and the phase words are the product's own. monetize
968/// owns no second driver — the sequence `cargo xtask restore main` performs by
969/// hand (prove the twin → re-lay → replay → verify) is the sequence the
970/// product performs here, and the difference is only who said go.
971///
972/// A product without this lane answers `None` from [`Product::restore`], and a
973/// growth that fails then parks in `failed` exactly as it always has.
974pub trait ApplianceRestore: Send + Sync {
975    /// **Re-lay the set and pull everything back from the twin.** Answers the
976    /// status after the call, never when the restore has finished — a replay
977    /// is long, and the poll is [`ApplianceRestore::restore_status`].
978    ///
979    /// `accept_lost` is the operator's acceptance of
980    /// `DATA-SET-GROWTH-FLOW.md` §2.1: every ref outside `refs/heads/*` and
981    /// `refs/tags/*`, every LFS object, the control database, every
982    /// repository's visibility, every tombstone. Without it a restore that
983    /// would lose anything REFUSES, and says what.
984    fn restore(&self, accept_lost: bool) -> Result<RestoreStatus, ProductError>;
985    /// The restore's phase, as the product sees it. Must answer while the set
986    /// is unmounted, for the same reason [`ApplianceGrow::status`] must.
987    fn restore_status(&self) -> Result<RestoreStatus, ProductError>;
988}
989
990/// What a product reports about a restore in progress.
991#[derive(Clone, Debug, Default, PartialEq, Eq)]
992pub struct RestoreStatus {
993    /// `idle` | `laying` | `replaying` | `verifying` | `done` | `failed`.
994    pub phase: String,
995    /// `failed`'s reason; empty otherwise.
996    pub why: String,
997    /// The twin's head the replay is against.
998    pub head_seq: u64,
999    /// Entries applied so far.
1000    pub applied: u64,
1001    /// **Entries the twin could not give back.** Non-zero is the §2.1 loss,
1002    /// and a restore started without `accept_lost` refuses rather than
1003    /// reaching this with a number in it.
1004    pub lost: u64,
1005    /// The box needs a restart before it serves again — the ONE place in this
1006    /// design that restarts the primary.
1007    pub needs_restart: bool,
1008    pub since_unix_ms: i64,
1009    pub detail: String,
1010}
1011
1012impl RestoreStatus {
1013    /// The restore ended, either way.
1014    pub fn is_terminal(&self) -> bool {
1015        matches!(self.phase.as_str(), "done" | "failed")
1016    }
1017    pub fn is_done(&self) -> bool {
1018        self.phase == "done"
1019    }
1020}
1021
1022// ── the other direction: the TWIN is the one that died ────────────────
1023
1024/// **Rebuilding a twin that is gone**, which is the mirror image of
1025/// [`ApplianceRestore`] and just as necessary: an estate that can recover its
1026/// primary from its twin but cannot replace a dead twin is an estate that
1027/// works exactly once.
1028///
1029/// Every door this needs already exists on gunnar and none of them is new:
1030///
1031/// * `Bootstrap.BecomeTwin{primary}` — *"Make this FRESH box a twin of
1032///   `primary`. Unauthenticated, once … The box mints its own key, gives up
1033///   its own log numbers, freezes, and starts following. The answer is the
1034///   public key to enrol on the primary as a member of group `twin`."*
1035/// * `Mirror.SetReplication` — *"Add or replace one standby, BY NAME, and
1036///   start replicating to it now. Idempotent: the same name twice replaces
1037///   rather than duplicates."*
1038/// * `Mirror.SnapshotFetch` for a twin that comes up behind a retention hole.
1039///
1040/// What was missing is an ACTOR, and for the same reason as on the other
1041/// side: the twin cannot notice its own death, and the primary has no cloud
1042/// credential with which to build it a new box. Only the front has both.
1043///
1044/// The implementation dials TWO boxes — the fresh candidate's bootstrap door
1045/// and this primary's mirror door — which is why it is one verb here rather
1046/// than two: enrolling a key on the primary for a twin that never answered
1047/// would leave a grant for a box that does not exist.
1048pub trait ApplianceTwinship: Send + Sync {
1049    /// **Is there a twin, and is the primary hearing it?** Folded from the
1050    /// same `Admin.Health` rows [`Product::twins`] serves, so there is one
1051    /// description of the twin and not two.
1052    fn twin_health(&self) -> Result<TwinHealth, ProductError>;
1053
1054    /// **Turn a FRESH box at `twin_addr` into this primary's twin**:
1055    /// `BecomeTwin` on it, enrol the key it mints on this primary in group
1056    /// `twin`, then `SetReplication` so it starts following now.
1057    ///
1058    /// Refuses by name when the candidate is not fresh — `BecomeTwin` is
1059    /// *"refused (FAILED_PRECONDITION) unless nothing is enrolled and no
1060    /// repository exists"* — because making a box fresh is a cloud action and
1061    /// no product can do it to another box.
1062    fn adopt_twin(&self, twin_addr: &str) -> Result<TwinAdopted, ProductError>;
1063}
1064
1065/// What the primary hears from its standbys right now.
1066#[derive(Clone, Debug, Default, PartialEq, Eq)]
1067pub struct TwinHealth {
1068    /// Every standby row, as [`Product::twins`] reports them.
1069    pub rows: Vec<TwinFill>,
1070    /// The primary believes it should have a twin — a standby is configured
1071    /// in its `replication.json`, whether or not it is answering. Absent this,
1072    /// "no rows" means "no twin was ever set up", which is a config choice and
1073    /// not a fault.
1074    pub configured: bool,
1075}
1076
1077impl TwinHealth {
1078    /// **No standby is answering, and one should be.** The condition a rebuild
1079    /// keys on. Rows that read `ok` or `behind` are heard;
1080    /// [`TwinFill::visible`] is the one reader of those words.
1081    #[must_use]
1082    pub fn lost(&self) -> bool {
1083        self.configured && !self.rows.iter().any(TwinFill::visible)
1084    }
1085    /// The longest any standby has been unheard, in unix ms, or `None` when
1086    /// one is answering now or none was ever heard.
1087    #[must_use]
1088    pub fn quietest_since(&self) -> Option<i64> {
1089        if !self.lost() {
1090            return None;
1091        }
1092        self.rows.iter().map(|r| r.last_seen_unix_ms).filter(|t| *t > 0).max()
1093    }
1094}
1095
1096/// What [`ApplianceTwinship::adopt_twin`] did.
1097#[derive(Clone, Debug, Default, PartialEq, Eq)]
1098pub struct TwinAdopted {
1099    /// The name the standby is registered under on the primary.
1100    pub name: String,
1101    /// The key `BecomeTwin` minted, as enrolled. Public, so it is safe here.
1102    pub public_key: String,
1103    /// It is following, as against merely registered.
1104    pub following: bool,
1105    /// It came up behind a retention hole and needs `SnapshotFetch`. There is
1106    /// no shipped driver for that (`bench estate` row B8 is UNRUN by
1107    /// construction for exactly this), so the flag is how an operator learns
1108    /// they must run it by hand.
1109    pub needs_snapshot: bool,
1110    pub detail: String,
1111}
1112
1113/// The phases a product reports, as `DATA-SET-GROWTH-FLOW.md` names them.
1114/// Kept as words on the wire (`GrowStatus::phase`); this is the reader.
1115#[derive(Clone, Copy, PartialEq, Eq, Debug)]
1116pub enum GrowPhase {
1117    Idle,
1118    Draining,
1119    Unmounted,
1120    WaitingMembers,
1121    Growing,
1122    Mounting,
1123    Done,
1124    Failed,
1125    /// A word this monetize does not know — a newer product. Shown, never
1126    /// acted on.
1127    Other,
1128}
1129
1130impl GrowPhase {
1131    pub fn parse(word: &str) -> GrowPhase {
1132        match word {
1133            "idle" => GrowPhase::Idle,
1134            "draining" => GrowPhase::Draining,
1135            "unmounted" => GrowPhase::Unmounted,
1136            "waiting-members" => GrowPhase::WaitingMembers,
1137            "growing" => GrowPhase::Growing,
1138            "mounting" => GrowPhase::Mounting,
1139            "done" => GrowPhase::Done,
1140            "failed" => GrowPhase::Failed,
1141            _ => GrowPhase::Other,
1142        }
1143    }
1144    /// The set is off the stripe: the outside actor may touch the volumes.
1145    pub fn volumes_free(self) -> bool {
1146        matches!(self, GrowPhase::Unmounted | GrowPhase::WaitingMembers)
1147    }
1148    pub fn is_terminal(self) -> bool {
1149        matches!(self, GrowPhase::Done | GrowPhase::Failed | GrowPhase::Idle)
1150    }
1151}
1152
1153/// One member of the set as the product last surveyed it.
1154#[derive(Clone, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
1155pub struct GrowMember {
1156    pub index: u32,
1157    pub device: String,
1158    pub disk_bytes: u64,
1159    pub set_bytes: u64,
1160    pub larger: bool,
1161}
1162
1163/// The product's answer to [`ApplianceGrow::status`].
1164#[derive(Clone, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
1165pub struct GrowStatus {
1166    /// `idle` | `draining` | `unmounted` | `waiting-members` | `growing` |
1167    /// `mounting` | `done` | `failed`. Read with [`GrowPhase::parse`].
1168    pub phase: String,
1169    /// `failed`'s reason; empty otherwise.
1170    pub why: String,
1171    pub epoch: u64,
1172    pub target_sectors_per_member: u64,
1173    pub members: Vec<GrowMember>,
1174    pub members_verdict: String,
1175    pub engine_present: bool,
1176    pub serving: bool,
1177    pub in_flight: u64,
1178    pub since_unix_ms: i64,
1179    pub detail: String,
1180    pub go_ahead_signer: String,
1181    pub set_uuid: String,
1182    pub total_bytes: u64,
1183}
1184
1185impl GrowStatus {
1186    pub fn phase(&self) -> GrowPhase {
1187        GrowPhase::parse(&self.phase)
1188    }
1189}
1190
1191/// The product's answer to [`ApplianceGrow::flush`]: is the twin caught up,
1192/// and what does that prove.
1193#[derive(Clone, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
1194pub struct FlushReport {
1195    pub caught_up: bool,
1196    pub in_flight: u64,
1197    pub pending: u64,
1198    pub bus_gaps: u64,
1199    pub needs_full_resync: bool,
1200    pub last_success_unix_ms: i64,
1201    pub flushed_at_unix_ms: i64,
1202    pub waited_ms: u64,
1203    pub catalog_repos: u64,
1204    pub standbys: u64,
1205    pub verdict: String,
1206}
1207
1208/// What [`Product::purge_tenant`] did.
1209#[derive(Clone, PartialEq, Eq, Debug)]
1210pub struct Purged {
1211    pub tenant: TenantId,
1212    /// What the product removed, in its own words and its own units — "5 of 5
1213    /// stores, 1.2 GiB". Free text because every product counts different
1214    /// things and a shared schema would force each of them to lie a little.
1215    pub detail: String,
1216    /// Bytes the product says it reclaimed. `None` means it does not count
1217    /// them, which is not the same as zero and must not be rendered as it.
1218    pub bytes_reclaimed: Option<u64>,
1219    /// **Is the tenant still locked out of the product?**
1220    ///
1221    /// `true` is a real and expected outcome — an operator may have suspended
1222    /// the account separately, and the purge correctly refuses to lift a
1223    /// lockout it did not take — but it is also what a purge that could not
1224    /// unlock reports, and either way the tenant cannot use what is left.
1225    /// Carried so a super purge can say so rather than infer it.
1226    pub still_locked: bool,
1227    /// One line per thing the product could not remove. Non-empty means the
1228    /// tenant's data is PARTIALLY there, and the caller must not go on to
1229    /// destroy the iron it is sitting on.
1230    pub failures: Vec<String>,
1231}
1232
1233/// **Bytes in the largest unit that still leaves a digit before the decimal
1234/// point**, by integer arithmetic: `10.0 GiB`, `64.0 MiB`, `999 B`.
1235///
1236/// One writer, because there used to be three and every one of them carried the
1237/// same bug. Each divided by 1 GiB unconditionally and printed one decimal, so
1238/// anything under a gibibyte rendered `0.0 GiB` — and `0.0` beside a quantity is
1239/// read as *there is none*:
1240///
1241/// * the console (then a Plans page) showed a real 64 MiB
1242///   `pack_bytes` cap as `0.0 GiB`, which an operator reads as "no quota";
1243/// * the old catalogue validator said *"sells pack_bytes = 10.0 GiB but
1244///   its resources buy only 10.0 GiB — short 0.0 GiB"*, a sentence in which
1245///   every number is wrong in the direction of "nothing is the matter";
1246/// * `products/gunnar`'s capacity refusals said "short 0.0 GiB" for the same
1247///   reason.
1248///
1249/// It lives here because this is the crate that already owns the meter
1250/// vocabulary ([`Meter`], [`Backing::DiskBytes`]) and the only one every side
1251/// can depend on: core, the product plugins, and the browser console alike. It
1252/// pulls in nothing (this crate is `serde` and nothing else), so the wasm
1253/// bundle pays a few hundred bytes for a formatter it was carrying anyway.
1254///
1255/// Truncates, never rounds up: a reading must not appear to cross a cap it has
1256/// not crossed.
1257pub fn bytes(n: u64) -> String {
1258    const KIB: u64 = 1 << 10;
1259    const MIB: u64 = 1 << 20;
1260    const GIB: u64 = 1 << 30;
1261    const TIB: u64 = 1 << 40;
1262    let (unit, per) = match n {
1263        n if n >= TIB => ("TiB", TIB),
1264        n if n >= GIB => ("GiB", GIB),
1265        n if n >= MIB => ("MiB", MIB),
1266        n if n >= KIB => ("KiB", KIB),
1267        // Under a kibibyte there is nothing to scale to, and `0.0 KiB` would be
1268        // the same lie one unit down. Bytes are exact and short.
1269        n => return format!("{n} B"),
1270    };
1271    format!("{}.{} {unit}", n / per, ((n % per) * 10) / per)
1272}
1273
1274#[cfg(test)]
1275mod backing_tests {
1276    use super::Backing;
1277
1278    /// **A byte over the GiB is sold the next GiB, and a millicore is priced
1279    /// as a thousandth of a core.** Distinct non-zero numbers: 2200 öre per
1280    /// GiB-month, 90 GiB + 1 byte, 1500 millicores at 30 000.
1281    #[test]
1282    fn prices_round_bytes_up_to_the_unit_and_cpu_by_the_millicore() {
1283        const GIB: u64 = 1 << 30;
1284        assert_eq!(Backing::DiskBytes.price_month(90 * GIB, 2200), 198_000, "90 GiB × 22.00 SEK");
1285        assert_eq!(Backing::DiskBytes.price_month(90 * GIB + 1, 2200), 200_200, "one byte over is the 91st GiB");
1286        assert_eq!(Backing::DiskBytes.price_month(0, 2200), 0);
1287        assert_eq!(Backing::RamBytes.price_month(3 * GIB, 700), 2100);
1288        assert_eq!(Backing::CpuMillicores.price_month(1500, 30_000), 45_000, "1.5 cores at 300.00");
1289        assert_eq!(Backing::CpuMillicores.price_month(1, 30_000), 30, "one millicore is not free and not a core");
1290        // A zero unit price is FREE, whatever the delta: the open-source list.
1291        for b in Backing::ALL {
1292            assert_eq!(b.price_month(u64::MAX / 4, 0), 0, "{b:?}");
1293        }
1294        // The unit names are the price list's keys, both ways.
1295        for b in Backing::ALL {
1296            assert_eq!(Backing::from_unit(b.unit()), Some(b));
1297        }
1298        assert_eq!(Backing::from_unit("moon_month"), None);
1299    }
1300}
1301
1302#[cfg(test)]
1303mod bytes_tests {
1304    use super::bytes;
1305
1306    /// **A quantity that exists never renders as zero.** RED before this
1307    /// function existed: `0.0 GiB` for every value under 2^30, in three separate
1308    /// copies of the same six lines.
1309    #[test]
1310    fn only_a_genuine_zero_reads_as_zero() {
1311        for n in [1u64, 512, 1 << 20, 67_108_864, (1 << 30) - 1] {
1312            let s = bytes(n);
1313            assert!(!s.starts_with("0.0 ") && !s.starts_with("0 "), "{n} bytes rendered as {s:?}, which reads as nothing");
1314        }
1315        assert_eq!(bytes(0), "0 B");
1316    }
1317
1318    #[test]
1319    fn the_unit_scales_and_the_value_truncates() {
1320        assert_eq!(bytes(999), "999 B");
1321        assert_eq!(bytes(1536), "1.5 KiB");
1322        assert_eq!(bytes(67_108_864), "64.0 MiB");
1323        assert_eq!(bytes((1 << 30) - 1), "1023.9 MiB", "truncates, never rounds up past the cap");
1324        assert_eq!(bytes(10 << 30), "10.0 GiB");
1325        assert_eq!(bytes(3 << 40), "3.0 TiB");
1326        assert_eq!(bytes(u64::MAX), "16777215.9 TiB", "no overflow at the top of the range");
1327    }
1328}
1329
1330#[cfg(test)]
1331mod actor_tests {
1332    use super::*;
1333
1334    /// A plugin written before the tenant path existed: it implements every
1335    /// method the trait REQUIRES and knows nothing about tickets.
1336    struct OldPlugin;
1337
1338    impl Product for OldPlugin {
1339        fn id(&self) -> &'static str {
1340            "old"
1341        }
1342        fn meters(&self) -> &[Meter] {
1343            &[]
1344        }
1345        fn list_tenants(&self) -> Result<Vec<TenantId>, ProductError> {
1346            Ok(Vec::new())
1347        }
1348        fn read_usage(&self, _tenant: &TenantId) -> Result<Usage, ProductError> {
1349            Err(ProductError::Refused("no".into()))
1350        }
1351        fn push_entitlement(&self, _fact: &EntitlementFact) -> Result<(), ProductError> {
1352            Ok(())
1353        }
1354        fn can_absorb(&self, _tenant: &TenantId, _caps: &BTreeMap<String, u64>) -> Result<Absorb, String> {
1355            Ok(Absorb::OnHand)
1356        }
1357        fn servable(&self) -> Servable {
1358            Servable::Unmeasured("a test double measures nothing".to_owned())
1359        }
1360    }
1361
1362    /// ★ **Safe by OMISSION, not by remembering.**
1363    ///
1364    /// The out-of-tree plugin nobody will edit when this path ships must not
1365    /// start honouring signed bytes it has no key for. Both halves of the
1366    /// default say no: it does not advertise the capability, and it refuses
1367    /// every ticket rather than falling through to an allow.
1368    #[test]
1369    fn a_product_that_never_heard_of_a_ticket_refuses_every_one() {
1370        let p = OldPlugin;
1371        assert!(!p.tenant_renewal(), "a console must not be told to offer self-service here");
1372        let verdict = // The literal and not a constant: this crate cannot depend on
1373        // `monetize-embed` (that would be a cycle), which is exactly why the
1374        // trait takes the purpose as a `&str`. The default refuses whatever it
1375        // is handed, so the word decides nothing here.
1376        p.may_act_for(&TenantId("alice".into()), "renew", &Default::default(), b"anything at all").expect("the default answers rather than erroring");
1377        let ActorVerdict::Refused(why) = verdict else { panic!("the default must DENY") };
1378        assert!(!why.trim().is_empty(), "a refusal says why");
1379    }
1380
1381    /// ★ **"NO TWIN" AND "A TWIN THAT STOPPED ANSWERING" ARE DIFFERENT
1382    /// ANSWERS, AND ONLY ONE OF THEM IS A FAULT.**
1383    ///
1384    /// An estate that never had a standby is a choice somebody made in the
1385    /// config; building one unasked would be monetize inventing infrastructure.
1386    /// An estate that HAS one configured and cannot hear it has lost
1387    /// something, and that is the only case a rebuild may key on. This is the
1388    /// same distinction `grow::twin_sizing` makes for a missing twin lane and
1389    /// `replication_flush` makes with FailedPrecondition.
1390    #[test]
1391    fn a_twin_never_configured_is_not_a_lost_twin() {
1392        let silent = TwinFill { principal: "twin".into(), state: "silent".into(), last_seen_unix_ms: 1_900_000_000_000, ..TwinFill::default() };
1393
1394        // Configured and silent: lost.
1395        let h = TwinHealth { rows: vec![silent.clone()], configured: true };
1396        assert!(h.lost());
1397        assert_eq!(h.quietest_since(), Some(1_900_000_000_000));
1398
1399        // The same silence, but nobody ever asked for a twin: not a fault.
1400        let h = TwinHealth { rows: vec![silent.clone()], configured: false };
1401        assert!(!h.lost(), "an estate with no standby configured has lost nothing");
1402        assert_eq!(h.quietest_since(), None);
1403
1404        // Configured, and no rows at all — the twin is not merely quiet, it is
1405        // absent from the primary's own health. Still lost.
1406        let h = TwinHealth { rows: vec![], configured: true };
1407        assert!(h.lost());
1408        assert_eq!(h.quietest_since(), None, "never heard is not a time");
1409    }
1410
1411    /// **`behind` is heard.** A twin that is catching up is answering, and
1412    /// replacing it would throw away the catching-up it has already done. Only
1413    /// `silent`, or a word this build does not know, counts as unheard.
1414    #[test]
1415    fn a_twin_that_is_merely_behind_is_still_heard_and_is_never_rebuilt() {
1416        for word in ["ok", "behind"] {
1417            let h = TwinHealth { rows: vec![TwinFill { state: word.into(), last_seen_unix_ms: 5, ..TwinFill::default() }], configured: true };
1418            assert!(!h.lost(), "{word} is heard");
1419        }
1420        for word in ["silent", "", "who-knows"] {
1421            let h = TwinHealth { rows: vec![TwinFill { state: word.into(), last_seen_unix_ms: 5, ..TwinFill::default() }], configured: true };
1422            assert!(h.lost(), "{word:?} is not heard");
1423        }
1424    }
1425
1426    /// **One standby answering is enough.** A deployment may have two; the
1427    /// primary has not lost its standby until none of them is heard.
1428    #[test]
1429    fn one_standby_answering_is_enough_to_not_be_lost() {
1430        let h = TwinHealth {
1431            rows: vec![
1432                TwinFill { principal: "a".into(), state: "silent".into(), ..TwinFill::default() },
1433                TwinFill { principal: "b".into(), state: "ok".into(), ..TwinFill::default() },
1434            ],
1435            configured: true,
1436        };
1437        assert!(!h.lost());
1438    }
1439}