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, …, 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/// The registration seam: how a product gets into a server without the server
34/// knowing its name. A build registers what it links; the server looks ids up.
35pub mod factory;
36pub use factory::{body_secret_placeholder, GuestSpec, HttpAuth, ProductBuild, ProductFactory, PULLED_NEVER_DIALLED};
37
38/// **Reading a daily `*_today` counter out of a series of readings** — the fold
39/// behind the console's SILENT and FRICTION lists. Pure; see the module doc for
40/// why summing the readings is the wrong arithmetic and why it lives here.
41pub mod flow;
42pub mod guardian;
43pub mod pull;
44
45
46/// A tenant, as the product names it. gunnar: the `Namespace` name (`team/sub`).
47#[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Debug, serde::Serialize, serde::Deserialize)]
48#[derive(Default)]
49pub struct TenantId(pub String);
50
51/// **Whether the product will actually POLICE a cap on this meter.**
52///
53/// CPU and RAM exist as quota even where gunnar cannot cap them, and that gap is
54/// narrowed without redoing the engine.
55///
56/// An order may cap any declared meter. But a cap on a meter nothing checks is a
57/// promise nobody keeps, and the tenant cannot tell the difference from the
58/// invoice. So the product says, per meter, which kind of promise it is — and
59/// the console shows it beside the number rather than letting every cap look
60/// like a guarantee.
61///
62/// This is the smallest honest way to let CPU and RAM be sold: they ARE bought,
63/// with real money, and they DO change what the box can do — they are simply not
64/// something a git server polices per namespace. Saying so is better than either
65/// refusing to sell them or pretending they are enforced.
66#[derive(Clone, Copy, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
67pub enum Enforcement {
68 /// **The product refuses when over.** A cap here is a wall the tenant hits.
69 /// gunnar: `pack_bytes` (receive-pack refused before the bytes),
70 /// `cache_bytes` (the explode budget).
71 Enforced,
72 /// **Counted and reported, not policed.** The number is true and monetize
73 /// can bill on it or a human can act on it, but nothing refuses. A cap here
74 /// is a threshold, not a wall.
75 Measured,
76 /// **Neither counted nor policed BY THE PRODUCT — it is iron monetize
77 /// bought.** CPU and RAM: a bigger plan at the provider, real money, real
78 /// effect, and no per-namespace check anywhere in the product. The
79 /// provider's invoice is the enforcement.
80 Provisioned,
81}
82
83impl Enforcement {
84 /// One line for the console, beside the cap.
85 pub fn describe(self) -> &'static str {
86 match self {
87 Enforcement::Enforced => "the product refuses when over",
88 Enforcement::Measured => "counted and reported; nothing refuses",
89 Enforcement::Provisioned => "provisioned at the cloud provider; not policed by the product",
90 }
91 }
92
93 /// Is a cap on this meter a wall the tenant will actually hit?
94 pub fn is_a_wall(self) -> bool {
95 matches!(self, Enforcement::Enforced)
96 }
97}
98
99/// **What a cap on this meter is MADE OF** — and therefore what UNIT the price
100/// list prices it in and what iron, if any, the transaction buys for it.
101///
102/// Three of the four are capacity: bytes and cores, priced per GiB or core
103/// per month, and (for disk) bought from a cloud. The fourth, [`Backing::Units`],
104/// is a COUNT that is nevertheless SOLD — a property, a seat, a
105/// mailbox — priced flat per unit per month and buying nothing. It exists
106/// because a meter with no backing at all costs nothing to promise, and a
107/// product whose whole price is a count (a per-property price) would
108/// otherwise open a checkout for zero.
109///
110/// A plan that sells bytes it never buys is the one bug here that silently
111/// converts money into nothing. Since the plan catalogue went (2026-09-05), an
112/// ORDER names caps and nothing else, and this is the field that turns a cap
113/// into a purchase: `monetize::order::resources_for` translates the DELTA on a
114/// disk-backed meter into a `Resource::Disk`, and `monetize::PriceList` prices
115/// each backing's delta by its own unit.
116///
117/// It lives on the meter and not on the order because only the PRODUCT knows
118/// what its meter is made of — that `pack_bytes` is block storage and `pushes`
119/// is a count of events that costs nothing to promise.
120#[derive(Clone, Copy, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
121pub enum Backing {
122 /// **Bytes of block storage.** A delta here becomes a `Resource::Disk` of
123 /// `ceil(bytes / 2^30)` GiB, and is priced per GiB per month
124 /// (`disk_gib_month` on the price list).
125 DiskBytes,
126 /// **CPU, in millicores (1000 = one core).** Priced per core per month
127 /// (`cpu_core_month`). monetize does NOT translate millicores into a
128 /// provider's plan name (`2xCPU-4GB` is a name, and a table of names goes
129 /// stale every time a provider renames a size), so a delta here buys no
130 /// cloud resource of its own: the ceiling is the product's own measurement
131 /// (`Product::can_absorb`) and the box's declared limit
132 /// (`RoomLeft::ceilings`).
133 CpuMillicores,
134 /// **RAM, in bytes.** Priced per GiB per month (`ram_gib_month`); the same
135 /// ceiling rule as [`Backing::CpuMillicores`].
136 RamBytes,
137 /// **A counted, sold unit that is not iron** — a property register's
138 /// `fastigheter`, a seat, a mailbox. Priced flat per unit per month (`unit_month`, the
139 /// delta times the price, no rounding), and it buys NO cloud resource:
140 /// `resources_for` ignores it exactly as it ignores CPU and RAM. The
141 /// ceiling is the product's own answer (`Product::can_absorb`).
142 ///
143 /// Without this a product priced per count had no way to be priced at all:
144 /// a meter with no backing "costs nothing to promise", which is right for
145 /// `pushes` and wrong for a property that is the whole subscription.
146 Units,
147}
148
149impl Backing {
150 /// The price-list key this backing is priced under, and the unit it means:
151 /// `disk_gib_month` | `cpu_core_month` | `ram_gib_month` | `unit_month`.
152 pub fn unit(self) -> &'static str {
153 match self {
154 Backing::DiskBytes => "disk_gib_month",
155 Backing::CpuMillicores => "cpu_core_month",
156 Backing::RamBytes => "ram_gib_month",
157 Backing::Units => "unit_month",
158 }
159 }
160
161 /// Every backing, in the order the price list is written in.
162 pub const ALL: [Backing; 4] = [Backing::DiskBytes, Backing::CpuMillicores, Backing::RamBytes, Backing::Units];
163
164 /// The backing a price-list key names, if any.
165 pub fn from_unit(unit: &str) -> Option<Backing> {
166 Backing::ALL.into_iter().find(|b| b.unit() == unit)
167 }
168
169 /// **Price `delta` of this backing at `minor_per_unit_month`, for one
170 /// month**, in minor units. Integer arithmetic, rounded UP to the unit for
171 /// bytes (a customer who asks for one byte over a GiB is sold the next GiB,
172 /// which is what the provider sells us), exact per millicore for CPU
173 /// (`minor × millicores / 1000`), and flat for a counted unit (`delta ×
174 /// minor` — five properties cost five times one).
175 pub fn price_month(self, delta: u64, minor_per_unit_month: u64) -> u64 {
176 const GIB: u64 = 1 << 30;
177 match self {
178 Backing::DiskBytes | Backing::RamBytes => delta.div_ceil(GIB).saturating_mul(minor_per_unit_month),
179 Backing::CpuMillicores => u64::try_from(u128::from(delta) * u128::from(minor_per_unit_month) / 1000).unwrap_or(u64::MAX),
180 Backing::Units => delta.saturating_mul(minor_per_unit_month),
181 }
182 }
183}
184
185/// **WHERE a meter's number is even meaningful: per tenant, or per product.**
186///
187/// Disk is the whole game, and it is per tenant. CPU and RAM stay in the model,
188/// but for gunnar they are not interesting per user — they are interesting per
189/// product: a tenant does not own cores, the box does.
190///
191/// Orthogonal to [`Enforcement`], and the pair is what makes a fleet total
192/// honest. `Enforcement` says whether a cap is a WALL. Scope says whether adding
193/// one tenant's figure to another's produces anything at all:
194///
195/// * `pack_bytes` is per tenant, so nine tenants' pack bytes sum to the fleet's
196/// pack bytes;
197/// * `cpu_millicores` is per product, so nine tenants' CPU does not sum to
198/// anything — nobody measured a tenant's cores, and adding nine numbers that
199/// were never measured is arithmetic on nothing.
200///
201/// **It is the PRODUCT's call, not a global rule.** gunnar meters disk per tenant
202/// and CPU per product; another product may legitimately meter CPU per user, or
203/// disk only in total. [`Product::meters`] is already that seam, and nothing
204/// outside a product decides which of its meters are per-tenant. `monetize::fleet`
205/// sums only [`Scope::Tenant`], whatever a product declares.
206#[derive(Clone, Copy, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
207pub enum Scope {
208 /// **Measured, or sold, per TENANT.** A fleet figure is the sum over tenants.
209 /// The default, because it is what a meter usually is.
210 Tenant,
211 /// **Measured, or bought, for the WHOLE PRODUCT.** It appears ONCE, on the
212 /// product screen, and never in a per-tenant sum. A cap on it may still be
213 /// sold — CPU and RAM are bought with real money — it simply is not a
214 /// quantity any one tenant holds.
215 Product,
216}
217
218impl Scope {
219 /// May a figure for this meter be summed across tenants? Only [`Scope::Tenant`].
220 pub fn sums_across_tenants(self) -> bool {
221 matches!(self, Scope::Tenant)
222 }
223
224 /// `tenant` | `product` — the wire and JSON spelling, one writer.
225 pub fn name(self) -> &'static str {
226 match self {
227 Scope::Tenant => "tenant",
228 Scope::Product => "product",
229 }
230 }
231}
232
233/// **One meter a product declares**: its name, what it means, and whether a cap
234/// on it is policed. An order may only cap a declared meter, and the console
235/// shows [`Enforcement`] beside the number.
236#[derive(Clone, Copy, PartialEq, Eq, Debug)]
237pub struct Meter {
238 /// The wire name, and the key in [`EntitlementFact::caps`] and [`Usage::meters`].
239 pub name: &'static str,
240 /// One line, the plugin's own words.
241 pub meaning: &'static str,
242 pub enforcement: Enforcement,
243 /// **Does [`Product::read_usage`] carry a reading for this meter?**
244 ///
245 /// A meter exists to do one or both of two jobs: carry a usage READING, and
246 /// accept a CAP. Most do both. Two kinds do not, and conflating them was
247 /// caught by a test rather than by thinking:
248 ///
249 /// * `cpu_millicores`, `ram_bytes` — the product has no idea what was
250 /// bought; monetize does, from the order. Reporting `0` would read as "you
251 /// have none", which is worse than saying nothing.
252 /// * `concurrent_transfers` — a cap the engine enforces instantly, with no
253 /// stored reading to bill from. The limit is real; the gauge does not exist.
254 ///
255 /// So a plugin's `read_usage` must report EXACTLY the meters with this set,
256 /// which is an equality a test can hold in both directions: an undeclared
257 /// reading is a leak, and a declared reading that stopped arriving is a
258 /// silently emptied bill.
259 pub reports_usage: bool,
260 /// **What a cap on this meter is made of**, or `None` when a cap here costs
261 /// nothing to grant (a count of pushes, a number of repositories the
262 /// existing box already holds). `Some` is what makes an order on this meter
263 /// a PURCHASE: the delta is priced by the backing's unit and, for disk,
264 /// bought as iron. See [`Backing`].
265 pub backing: Option<Backing>,
266 /// **Whether this meter's number is per tenant or per product.** See
267 /// [`Scope`]; it is what keeps a fleet total from adding up figures nobody
268 /// measured per tenant.
269 pub scope: Scope,
270}
271
272impl Meter {
273 /// A meter that both reports a reading and may be capped — the common case.
274 pub const fn new(name: &'static str, meaning: &'static str, enforcement: Enforcement) -> Meter {
275 Meter { name, meaning, enforcement, reports_usage: true, backing: None, scope: Scope::Tenant }
276 }
277
278 /// A meter that may be CAPPED but carries no reading. See
279 /// [`Meter::reports_usage`] for the two kinds and why `0` is not a truthful
280 /// substitute.
281 pub const fn cap_only(name: &'static str, meaning: &'static str, enforcement: Enforcement) -> Meter {
282 Meter { name, meaning, enforcement, reports_usage: false, backing: None, scope: Scope::Tenant }
283 }
284
285 /// The same meter, declaring what a cap on it is MADE OF — and so what an
286 /// order on it buys and what the price list prices it by. A
287 /// [`Enforcement::Provisioned`] meter without one is a cap monetize can
288 /// neither price nor buy; gunnar's own test holds its list to that.
289 pub const fn with_backing(self, backing: Backing) -> Meter {
290 Meter { backing: Some(backing), ..self }
291 }
292
293 /// The same meter, measured for the WHOLE PRODUCT rather than per tenant —
294 /// so a fleet total leaves it out instead of adding up figures nobody took.
295 /// See [`Scope`].
296 pub const fn per_product(self) -> Meter {
297 Meter { scope: Scope::Product, ..self }
298 }
299}
300
301/// **What a product says it could still serve, or why it could not say.**
302///
303/// The other half of the oversell number: `sum(what has been sold) − servable`.
304/// Without it monetize could sell ten tenants 10 GiB each on a box with 55 GiB
305/// servable and nothing would object until the sixth push.
306///
307/// The two arms are the distinction `BASE-MODEL.md` rule 3 turns on and the same
308/// one [`Product::can_absorb`] already makes: **an unanswered capacity question
309/// is a measurement that did not happen, not a full disk and not an empty one.**
310/// The live gunnar.rs appliance predates gunnar's `Admin.Capacity` RPC and
311/// answers [`Servable::Unmeasured`] today; a fleet total that turned that into a
312/// zero would report every deployment as catastrophically oversold, and one that
313/// turned it into infinity would report every deployment as fine. Neither is a
314/// measurement.
315/// **Can this product's store grow while it runs?** Carried on
316/// [`Servable::Measured`] beside the bytes, because the two answer different
317/// questions and an operator acts differently on each: a full box that can grow
318/// wants a disk; a full box that is SEALED wants its set enlarged by an operator
319/// maintenance operation (the volumes it already has, grown in place) or a fresh
320/// install onto a larger one, and a disk bought for it is billed and invisible.
321///
322/// Each arm carries the PRODUCT's own remediation sentence (gunnar's
323/// `remediation()`), so the console renders what the product said rather than a
324/// paraphrase of it — `UI.md` screen 2's `⚠ cannot grow` badge holds exactly
325/// that paragraph.
326#[derive(Clone, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
327pub enum Growth {
328 /// Cannot gain capacity while it runs, and nothing on the machine can give
329 /// it any. `GROWTH_SEALED` on gunnar's wire.
330 Sealed(String),
331 /// The product will not grow the set itself, but an operator can, out of
332 /// band. `GROWTH_OPERATOR_ONLY`.
333 OperatorOnly(String),
334 /// The product could not tell. NOT a promise that it can grow.
335 Unknown(String),
336 /// **Grows at its next restart** once every member volume has been
337 /// enlarged to one equal size (gunnar's `GROWTH_AT_RESTART`, 2026-09-14).
338 /// Still no hot-plug: a disk ATTACHED to it is as invisible as under
339 /// `Sealed`; what helps is the outside actor enlarging the members
340 /// ([`ApplianceGrow`] + `monetize_cloud::grow`).
341 AtRestart(String),
342 /// **Grows without a restart**: the product drains, unmounts and grows into
343 /// enlarged members while its control plane stays up (gunnar's
344 /// `GROWTH_AT_RUNTIME`). The shape [`ApplianceGrow`] drives.
345 AtRuntime(String),
346}
347
348impl Growth {
349 /// The wire word: `sealed` | `operator_only` | `unknown` | `at_restart` |
350 /// `at_runtime`.
351 pub fn name(&self) -> &'static str {
352 match self {
353 Growth::Sealed(_) => "sealed",
354 Growth::OperatorOnly(_) => "operator_only",
355 Growth::Unknown(_) => "unknown",
356 Growth::AtRestart(_) => "at_restart",
357 Growth::AtRuntime(_) => "at_runtime",
358 }
359 }
360
361 /// The product's own sentence about what to do.
362 pub fn detail(&self) -> &str {
363 match self {
364 Growth::Sealed(s) | Growth::OperatorOnly(s) | Growth::Unknown(s) | Growth::AtRestart(s) | Growth::AtRuntime(s) => s,
365 }
366 }
367}
368
369#[derive(Clone, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
370pub enum Servable {
371 /// The product measured its own store.
372 Measured {
373 /// **Bytes that may still be handed to tenants**, beyond what they already
374 /// hold, after whatever reserve the product keeps for itself. gunnar's
375 /// `servable_bytes` (`free − disk floor`) — never its `free_bytes`, which
376 /// includes a reserve monetize must not sell.
377 servable_bytes: u64,
378 /// The whole store, reserve and all. `used + servable_bytes` is what the
379 /// fleet could grow to; `total_bytes` is larger than that by the reserve
380 /// and by anything on the filesystem that is not tenant data. 0 when the
381 /// product measures what is left but not what there is in total.
382 total_bytes: u64,
383 /// **Whether the store can EVER hold more than `total_bytes`.** A
384 /// measurement of the box, independent of the bytes: gunnar's
385 /// `Admin.Capacity` answers it as `growth`, and it is the field that
386 /// stops "buy another disk" being the reflex answer to a full fleet.
387 growth: Growth,
388 /// **The box's MEASURED ceilings on product-scope meters**, by the
389 /// product's own meter name: gunnar's `cpu_millicores_total` and
390 /// `ram_bytes_total` (Admin.Capacity, since gunnar 744bf72d) land here
391 /// as `cpu_millicores` / `ram_bytes`. A meter ABSENT here was not
392 /// measured — the wire spells that as an absent field, never as 0, and
393 /// a reader must never take a 0 as a ceiling. An order that would
394 /// raise a product-scope cap past a number here is refused by name;
395 /// the box's DECLARED ceilings (`RoomLeft::ceilings`) carry only
396 /// for a meter this map does not hold.
397 ceilings: BTreeMap<String, u64>,
398 },
399 /// **It could not be asked, or would not say — which is not zero.** The
400 /// string names WHICH: an RPC the deployed build predates, a control plane
401 /// that is down, a plugin that was never wired.
402 Unmeasured(String),
403}
404
405/// One named meter. Names are the product's, documented in its plugin crate, e.g.
406/// `pack_bytes`, `cache_bytes`, `lfs_bytes`, `tombstoned_bytes`, `open_store_ram_bytes`,
407/// `pushes`, `anonymous_reads`. Values are the product's units, usually bytes or counts.
408#[derive(Clone, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
409pub struct Usage {
410 pub meters: BTreeMap<String, u64>,
411 pub measured_at_unix_ms: u64,
412}
413
414/// **One page of a BATCHED usage read** — [`Product::read_usage_page`]'s
415/// question (2026-10-06).
416///
417/// The poller used to ask [`Product::read_usage`] once per tenant per tick: at
418/// 1,000 tenants that was ~25–30 s of 1.4 cores on gunnar's main every five
419/// minutes, linear in tenants, and at ~10,000 the whole period. A page answers
420/// many tenants in ONE call, and `changed_since` lets the product answer only
421/// the tenants whose usage moved — so a quiet estate costs a few calls, not one
422/// per tenant.
423///
424/// **Who a listed tenant IS** ([`Product::tenant_owners`], 2026-10-08): the product's own
425/// word for the namespace's owner, so a SELLING shop can give every PERSON's account a fact
426/// (no account there is "no fact = uncapped") and leave the SELLER's own alone.
427///
428/// Every field defaults, and neither side denies unknown fields.
429#[derive(Clone, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
430#[serde(default)]
431pub struct TenantOwner {
432 pub tenant: TenantId,
433 /// The tenant is a PERSON's own account (gunnar: `owner_kind` USER). False for a group,
434 /// a clan, a machine's, or a namespace of no principal.
435 pub person: bool,
436 /// The owner holds a SERVER-scope grant at the product (the operator — the seller): the
437 /// shop's own, never a customer. Only the product says so; nothing a customer does sets it.
438 pub seller: bool,
439}
440
441/// Every field defaults, and neither side denies unknown fields: an older and a
442/// newer build of either side read each other's JSON.
443#[derive(Clone, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
444pub struct UsagePageReq {
445 /// `""` asks for the first page; otherwise the previous page's
446 /// [`UsagePage::next_page_token`], unchanged.
447 #[serde(default)]
448 pub page_token: String,
449 /// At most this many rows; 0 = the product's own default.
450 #[serde(default)]
451 pub page_size: u32,
452 /// `""` = every tenant. Otherwise a [`UsagePage::cursor`] an earlier read
453 /// answered: only the tenants whose usage CHANGED since. A product that can
454 /// no longer honour it (it restarted, the day turned) answers every tenant
455 /// and says so with [`UsagePage::changed_only`] = false.
456 #[serde(default)]
457 pub changed_since: String,
458}
459
460/// One tenant's row in a [`UsagePage`].
461#[derive(Clone, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
462pub struct TenantUsage {
463 pub tenant: TenantId,
464 pub usage: Usage,
465}
466
467/// **How long a whole keep boot may take** (audit 7, 2026-10-18; lane G9, A7 row 9): the keep
468/// medium imported and cloned, the box stopped, the medium's run (the provider's own budget,
469/// 45 min at UpCloud), the clone given back and the box started. The shop holds its Upgrade
470/// call this long (`monetize_cloud::lay::KEEP_BOOT_WITHIN`), and every product's front waits on
471/// it exactly as long (`monetize_embed::fleet::KEEP_BOOT_WITHIN`, Gunnar b74ca801): a front
472/// that gave up first would report a failure while the shop still works. ONE constant, here,
473/// because the shop's cloud crate and the fronts' embed crate both stand on this one.
474pub const KEEP_BOOT_WITHIN: std::time::Duration = std::time::Duration::from_secs(120 * 60);
475
476/// **The mark of a sold estate's tenant on the ONE ledger** (5.4, ONE monetize, G16): a
477/// private Gunnar runs no monetize of its own, so its tenants are accounts of product `gunnar`
478/// in the ONE ledger as `@<estate>/<namespace>`. `@` is no character of a gunnar namespace
479/// (`gunnar-core` sandbox rules), so no tenant of the product's own estate reads as one.
480pub const ESTATE_MARK: char = '@';
481
482impl TenantId {
483 /// `namespace` on the sold estate `estate`, as the ONE ledger keys it: `@<estate>/<namespace>`.
484 pub fn on_estate(estate: &str, namespace: &str) -> Self {
485 TenantId(format!("{ESTATE_MARK}{estate}/{namespace}"))
486 }
487
488 /// `(estate, namespace)` when this is a sold estate's tenant ([`Self::on_estate`]); `None`
489 /// for a tenant of the product's own estate.
490 pub fn estate(&self) -> Option<(&str, &str)> {
491 let (estate, namespace) = self.0.strip_prefix(ESTATE_MARK)?.split_once('/')?;
492 (!estate.is_empty() && !namespace.is_empty()).then_some((estate, namespace))
493 }
494}
495
496/// **One page of a batched usage read** — [`Product::read_usage_page`]'s
497/// answer. See [`UsagePageReq`].
498#[derive(Clone, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
499pub struct UsagePage {
500 /// The readings, in the product's order. MAY include names that
501 /// [`Product::list_tenants`] does not list (a product's own machine
502 /// accounts); a caller keys by tenant and ignores the rest.
503 #[serde(default)]
504 pub rows: Vec<TenantUsage>,
505 /// `""` = this was the last page.
506 #[serde(default)]
507 pub next_page_token: String,
508 /// What to pass as [`UsagePageReq::changed_since`] next tick. The same on
509 /// every page of one read: it is taken when the read BEGAN, so a change that
510 /// lands while the pages are fetched is answered again next time rather than
511 /// lost.
512 #[serde(default)]
513 pub cursor: String,
514 /// True when `changed_since` was honoured and `rows` holds only the tenants
515 /// that changed; false when every tenant is in the read.
516 #[serde(default)]
517 pub changed_only: bool,
518 /// When the product took the reading this page answers from.
519 #[serde(default)]
520 pub as_of_unix_ms: u64,
521}
522
523/// **One tenant's FLOW for one UTC day** — what MOVED, as against what is held.
524///
525/// Every number in [`Usage`] is a STOCK reading: it answers *how much is on the
526/// product's disk*, and it reads exactly the same for a customer who worked
527/// this morning and one who last touched the product in March. monetize could
528/// therefore bill nine tenants correctly and not know that six of them had
529/// stopped. This is the other kind, and it is what the SILENT and FRICTION
530/// lists are made of.
531///
532/// **The meters are the product's own names**, exactly as [`Usage::meters`] is,
533/// and for the same reason: core has never held a product-specific field and
534/// must not gain one here. gunnar fills `bytes_in`, `bytes_out`, `pushes`,
535/// `fetches`, `lfs_bytes_in`, `lfs_bytes_out`, `refused`, its three
536/// `refused_*` reasons, `auth_failures`, `distinct_principals` and the two
537/// `last_*_unix_ms` timestamps. Another product will fill something else, and
538/// nothing here needs to know.
539///
540/// **A day with no traffic has NO ROW.** Not a row of zeroes: the caller knows
541/// which days it asked for, and zero-filling would put a bar of height zero
542/// beside a bar that means *not counted*.
543#[derive(Clone, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
544pub struct ActivityDay {
545 /// Midnight UTC of the day, in Unix milliseconds.
546 pub day_unix_ms: u64,
547 /// The product's own meter names. See the type's doc.
548 pub meters: BTreeMap<String, u64>,
549}
550
551/// **A tenant's kept activity, and how far back the product remembers.**
552///
553/// The second half is not decoration. An empty `days` means one of two opposite
554/// things — *nothing happened* or *you asked about a time the product no longer
555/// remembers* — and only `retained_days` can tell them apart. A console that
556/// drew an empty series as "silent" without it would report every tenant on a
557/// freshly installed box as a lapsed customer.
558#[derive(Clone, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
559pub struct Activity {
560 /// Oldest first. Days with no traffic are absent.
561 pub days: Vec<ActivityDay>,
562 /// How many days the product keeps. `0` when it would not say.
563 pub retained_days: u64,
564}
565
566/// The verdict monetize pushes back. Product-agnostic; the plugin maps it to the
567/// product's own enum (gunnar: `EntitlementState`).
568#[derive(Clone, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
569pub struct EntitlementFact {
570 pub tenant: TenantId,
571 /// **The ORDER this verdict came from** — the ledger reference
572 /// (`<product>/<tenant>/<date>+<caps>`) for a purchase, an operator's own
573 /// label (or nothing) for a hand-set fact. The field is still called `plan`
574 /// on the wire and in the signed canonical form (`monetize_embed::signing`),
575 /// because gunnar verifies that form and a renamed field would invalidate
576 /// every signature a deployed gunnar checks; the CONTENT is an order
577 /// reference since the plan catalogue went on 2026-09-05.
578 pub plan: String,
579 pub state: State,
580 pub paid_until_unix_ms: Option<u64>,
581 /// Per-meter caps the product enforces itself (gunnar: `pack_quota_bytes`,
582 /// `explode_budget_bytes`). Absent = product default.
583 pub caps: BTreeMap<String, u64>,
584 /// `operator` | `payment:<vendor>:<reference>` — lands in the product's attestation log.
585 pub source: String,
586 /// Ed25519 over the canonical JSON of the fields above, by monetize-server's key.
587 /// **The V1 form** — it does NOT cover [`EntitlementFact::issued_unix_ms`], and
588 /// that is deliberate: it is the signature an appliance built before
589 /// 2026-09-17 computes, and it must keep verifying there for ever. See
590 /// [`Self::issued_signature`].
591 pub signature: Vec<u8>,
592 /// **When monetize issued this verdict — the field that makes a fact good
593 /// ONCE.**
594 ///
595 /// Without it an `EntitlementFact` is replayable for ever: capture a `Paid`
596 /// fact, wait for the tenant to lapse, push the captured bytes back, and the
597 /// signature still verifies because it is a real signature. `paid_until`
598 /// cannot tell the two apart — a lapse keeps the date and moves the ladder —
599 /// so the only thing that can is something MONOTONIC inside the signed form.
600 /// A product refuses a fact that is not newer than the one it holds
601 /// (gunnar: `EntitlementSlot::set`).
602 ///
603 /// Unix milliseconds, and not a sequence number, for the same reason
604 /// [`crate`]'s sibling [`monetize_embed::signing::Snapshot`] chose one: a
605 /// clock needs no durable per-tenant counter on monetize's side, so a
606 /// restored ledger cannot rewind one and mint facts every appliance in the
607 /// field then refuses for ever. One idea in this system, not two.
608 ///
609 /// `None` is a fact signed before this field existed. It stays legal, and a
610 /// product accepts it — until that product has seen ONE stamped fact for the
611 /// tenant, after which the unstamped form is a downgrade and is refused.
612 #[serde(default, skip_serializing_if = "Option::is_none")]
613 pub issued_unix_ms: Option<u64>,
614 /// **The V2 signature**: Ed25519 over the canonical JSON of every field
615 /// above INCLUDING `issued_unix_ms`
616 /// (`monetize_embed::signing::fact_message_issued`).
617 ///
618 /// Two signatures and not one, because a fact has to be readable by two
619 /// generations of appliance at once. An appliance that predates this field
620 /// reads only fields 1-7 off the wire, computes the V1 form, and checks
621 /// [`Self::signature`] — so it accepts a stamped fact unchanged, and a
622 /// paying customer on an un-upgraded box loses nothing. An appliance that
623 /// knows the field checks BOTH, so the issue time is signed and cannot be
624 /// added, moved or bumped by whoever relays the fact.
625 ///
626 /// Empty exactly when `issued_unix_ms` is `None`; neither is legal without
627 /// the other.
628 #[serde(default, skip_serializing_if = "Vec::is_empty")]
629 pub issued_signature: Vec<u8>,
630}
631
632/// The ladder. Numbers (grace/retention days) are the deployment's policy
633/// (`monetize::Policy`), not the enum's.
634#[derive(Clone, Copy, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
635pub enum State {
636 Free,
637 Paid,
638 Grace,
639 Suspended,
640 Retention,
641}
642
643#[derive(Clone, Debug)]
644pub enum ProductError {
645 /// The product's control plane refused (auth, unknown tenant).
646 Refused(String),
647 /// Product unreachable. Retryable; nothing was written.
648 Unavailable(String),
649}
650
651impl std::fmt::Display for ProductError {
652 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
653 match self {
654 ProductError::Refused(r) => write!(f, "product refused: {r}"),
655 ProductError::Unavailable(r) => write!(f, "product unavailable: {r}"),
656 }
657 }
658}
659impl std::error::Error for ProductError {}
660
661/// **Where the room for a target comes from, as the product measured it** —
662/// what [`Product::can_absorb`] answers when it can serve.
663///
664/// A yes is two different facts, and the difference is money. gunnar's
665/// `Admin.Capacity` answers a `verdict` and a `growth`, and the three readings
666/// of them a seller can act on are: there is room already (buy nothing), there
667/// is no room but a bought disk will be used (buy, and somebody has to extend a
668/// filesystem), and there is no room and a bought disk will NOT be used (refuse).
669/// The third is a refusal and never reaches this type. Before this type existed
670/// a yes was `Ok(())` and the cloud was asked to `ensure` a disk regardless. With
671/// the front's configuration (`pool_reserve_gib` covering the whole data set)
672/// UpCloud's pool arithmetic answers "short" for every order, so every yes
673/// bought and attached a disk to an appliance that had just said it had room —
674/// read from the code and the config, not from a bill.
675///
676/// The transaction turns it into `monetize_cloud::Room` for the provider, so
677/// the cloud is told what the product measured and never has to know which
678/// product that was.
679#[derive(Clone, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
680pub enum Absorb {
681 /// **Served out of what the product already has.** Nothing may be bought
682 /// for it: a disk bought on top of this answer is billed and not needed.
683 OnHand,
684 /// **Short today, and bought iron becomes usable** — by the hand `note`
685 /// names, in the product's own words (gunnar: an operator extends the
686 /// filesystem out of band, and gunnar measures the larger one on its next
687 /// look). Carried onto the purchase row so the ledger records that
688 /// somebody still has a job to do after the disk is attached.
689 WithIron { note: String },
690}
691
692/// **What a metered verb answered** — see [`Product::metered_door`]. Three
693/// arms, because "nobody could ask" is neither a paid build nor a community
694/// one, and a deployment that sells against any of the last two is selling
695/// something it cannot deliver.
696#[derive(Clone, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
697pub enum MeteredDoor {
698 /// **The box answered a metered verb.** It has the paid services compiled
699 /// in; the string is whatever the plugin wants to record about the proof
700 /// (the verb it called, the build it saw).
701 Enforcing(String),
702 /// **The box answered `UNIMPLEMENTED`.** A community build. Every paid
703 /// signup against it will take the money and fail to grant the
704 /// entitlement, so a deployment that sells is misconfigured, loudly.
705 Absent(String),
706 /// **Nobody could ask** — the box is down, the plugin has no probe, nothing
707 /// has run yet. NOT a yes and NOT a no; carries which.
708 Unproven(String),
709}
710
711impl MeteredDoor {
712 /// `enforcing` | `absent` | `unproven` — the wire, JSON and log spelling,
713 /// one writer.
714 pub fn name(&self) -> &'static str {
715 match self {
716 MeteredDoor::Enforcing(_) => "enforcing",
717 MeteredDoor::Absent(_) => "absent",
718 MeteredDoor::Unproven(_) => "unproven",
719 }
720 }
721 /// The plugin's own words about the answer.
722 pub fn detail(&self) -> &str {
723 match self {
724 MeteredDoor::Enforcing(s) | MeteredDoor::Absent(s) | MeteredDoor::Unproven(s) => s,
725 }
726 }
727 /// **Has the paid door been PROVEN to be there?** False for `Unproven`,
728 /// which is the whole point of the arm: a deployment may not call itself
729 /// healthy on the strength of a question nobody answered.
730 pub fn proven(&self) -> bool {
731 matches!(self, MeteredDoor::Enforcing(_))
732 }
733}
734
735/// **One TEST BUTTON a product declares** (owner 2026-10-11): the operator's
736/// console renders one button per declared action and monetize runs it through
737/// the SAME sign-up / estate-order path a customer takes, on a vendor in test
738/// mode only. Data, never markup: the console stays a thin renderer and the
739/// product never ships HTML into it.
740#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
741pub struct TestAction {
742 /// Stable within the product (`hosted-10gb`); what the console sends back.
743 pub id: String,
744 /// What the button reads (`Test: 10 GB hosted`).
745 pub label: String,
746 pub kind: TestActionKind,
747}
748
749/// **What a test button buys** — the two things a stranger can buy today.
750#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
751#[serde(rename_all = "snake_case")]
752pub enum TestActionKind {
753 /// A hosted account with this much room, at the plan the sign-up page
754 /// offers for it NOW (`Signup.Plans`) — the product names the room, never a
755 /// priced id, because the price is the shop's.
756 HostedPlan { cap_gib: u64 },
757 /// A private instance from the estate-order ladder, at these sizes (empty:
758 /// the ladder's own default for that field).
759 PrivateInstance {
760 #[serde(default)]
761 front_plan: String,
762 #[serde(default)]
763 main_plan: String,
764 #[serde(default)]
765 main_data_gib: u64,
766 },
767}
768
769/// **What is fixed of a product, as DATA** (A32-13): its manifest's id and meters and its facts'
770/// tickets, test buttons and guardian. The host reads it ONCE ([`Product::facts`]) and holds it
771/// in the product's slot, as the Cloud and Payment slots hold their manifests; every reader reads
772/// the slot ([`Served`]), never the product.
773#[derive(Clone, Debug, PartialEq, Eq, Default)]
774pub struct ProductFacts {
775 /// `gunnar`, `holger`: the block's id. Also the first segment of every payment reference.
776 pub id: &'static str,
777 /// The meters this product declares — the UI and the order form read this, so an order
778 /// can only cap a meter that exists, and the console can say whether a cap on it is
779 /// policed ([`Enforcement`]).
780 pub meters: &'static [Meter],
781 /// **Its main mints actor tickets** (the facts' `tickets`): a console may offer
782 /// self-service renewal. False means there is nothing that could tell a customer from a
783 /// stranger, so the route must not exist rather than existing and refusing. The HOST checks
784 /// each ticket. Reported on the wire as `ProductInfo.tenant_renewal`.
785 pub tickets: bool,
786 /// **The test buttons this product offers its operator** ([`TestAction`]). Empty: the
787 /// console shows none for it.
788 pub test_actions: Vec<TestAction>,
789 /// **It brings an estate guardian** ([`guardian::EstateGuardian`], 5.4): how its estates are
790 /// diagnosed and repaired. The host holds the guardian beside the slot.
791 pub guardian: bool,
792 /// `Some(why)`: the facts could not be read yet (a guest that could not answer); its
793 /// tickets, test buttons and guardian are off, and the host asks again on the next look.
794 pub unread: Option<String>,
795}
796
797impl ProductFacts {
798 /// A product `id` with `meters` and nothing else: no tickets, no buttons, no guardian.
799 pub fn new(id: &'static str, meters: &'static [Meter]) -> ProductFacts {
800 ProductFacts { id, meters, ..Default::default() }
801 }
802
803 /// **These facts, interned**: one `'static` copy per distinct answer (the meter tables are
804 /// interned the same way), so a slot hands out `&'static` facts that outlive a replaced
805 /// guest, and reading the same answer again leaks nothing.
806 pub fn leak(self) -> &'static ProductFacts {
807 static HELD: std::sync::Mutex<Vec<&'static ProductFacts>> = std::sync::Mutex::new(Vec::new());
808 let mut held = HELD.lock().unwrap_or_else(|e| e.into_inner());
809 if let Some(f) = held.iter().find(|f| ***f == self) {
810 return f;
811 }
812 let f: &'static ProductFacts = Box::leak(Box::new(self));
813 held.push(f);
814 f
815 }
816}
817
818/// **A product as the host serves it**: its facts, read once, beside the product's estate verbs.
819/// Copy, like `monetize_pay::Vendor`: the slot's facts (`&'static`) and the product. Every
820/// reader of an id, a meter table, the ticket fact or the test buttons reads them here; the
821/// estate verbs are the product's ([`std::ops::Deref`]).
822#[derive(Clone, Copy)]
823pub struct Served<'a> {
824 facts: &'a ProductFacts,
825 product: &'a dyn Product,
826 guardian: Option<&'a dyn guardian::EstateGuardian>,
827}
828
829impl<'a> Served<'a> {
830 /// `product` with the facts the host holds for it, and no guardian.
831 pub fn new(facts: &'a ProductFacts, product: &'a dyn Product) -> Served<'a> {
832 Served { facts, product, guardian: None }
833 }
834 /// The same, with the estate guardian the host holds beside it.
835 pub fn with_guardian(mut self, guardian: Option<&'a dyn guardian::EstateGuardian>) -> Served<'a> {
836 self.guardian = guardian;
837 self
838 }
839 /// **The estate guardian that answers for it** (5.4): how its estates are diagnosed and
840 /// repaired. `None`: monetize repairs none of its estates.
841 pub fn guardian(&self) -> Option<&'a dyn guardian::EstateGuardian> {
842 self.guardian
843 }
844 /// The block's id.
845 pub fn id(&self) -> &'static str {
846 self.facts.id
847 }
848 /// The declared meters.
849 pub fn meters(&self) -> &'static [Meter] {
850 self.facts.meters
851 }
852 /// Its main mints actor tickets ([`ProductFacts::tickets`]).
853 pub fn tenant_renewal(&self) -> bool {
854 self.facts.tickets
855 }
856 /// Its operator's test buttons.
857 pub fn test_actions(&self) -> &'a [TestAction] {
858 &self.facts.test_actions
859 }
860 /// Everything the host holds of it.
861 pub fn facts(&self) -> &'a ProductFacts {
862 self.facts
863 }
864 /// The product's estate verbs, without the facts.
865 pub fn product(&self) -> &'a dyn Product {
866 self.product
867 }
868}
869
870impl<'a> std::ops::Deref for Served<'a> {
871 type Target = dyn Product + 'a;
872 fn deref(&self) -> &Self::Target {
873 self.product
874 }
875}
876
877impl std::fmt::Debug for Served<'_> {
878 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
879 f.debug_struct("Served").field("id", &self.facts.id).finish()
880 }
881}
882
883/// The plugin seam. Sync for the same reason as the vendor traits.
884pub trait Product: Send + Sync {
885 /// **What is fixed of this product** ([`ProductFacts`]): asked ONCE by the host, when the
886 /// product is published (and again only while [`ProductFacts::unread`] says so).
887 fn facts(&self) -> ProductFacts;
888
889 fn list_tenants(&self) -> Result<Vec<TenantId>, ProductError>;
890 fn read_usage(&self, tenant: &TenantId) -> Result<Usage, ProductError>;
891
892 /// **Many tenants' usage in one call, optionally only what CHANGED** —
893 /// see [`UsagePageReq`] (2026-10-06).
894 ///
895 /// `Ok(None)` means *this product (or the box behind it) has no batched
896 /// read*: an older plugin, an older guest, an older appliance. The caller
897 /// then falls back to [`Product::read_usage`] per tenant, exactly as before
898 /// — which is why this has a default and why the default is `None` rather
899 /// than a refusal: a product that never heard of batching must keep being
900 /// polled, not stop being polled.
901 fn read_usage_page(&self, req: &UsagePageReq) -> Result<Option<UsagePage>, ProductError> {
902 let _ = req;
903 Ok(None)
904 }
905
906 /// **Who each tenant is** ([`TenantOwner`]) — what a SELLING shop's poller asks so every
907 /// person's account holds a fact (2026-10-08: an account an invite, an operator or a
908 /// failed sign-up made held none and was UNCAPPED). `Ok(None)`: this product (or its box)
909 /// cannot say — nothing is issued, exactly as before.
910 fn tenant_owners(&self) -> Result<Option<Vec<TenantOwner>>, ProductError> {
911 Ok(None)
912 }
913
914 /// **What the tenant has been DOING, day by day** — the flow series behind
915 /// the console's SILENT and FRICTION lists.
916 ///
917 /// [`Product::read_usage`] answers *how much is held*. Only this answers
918 /// *is this customer using the product, has it gone quiet, and is it being
919 /// refused* — and no stock meter has ever been able to.
920 ///
921 /// The window is Unix milliseconds, inclusive, resolved by the PRODUCT to
922 /// whole UTC days: `0` for `since` means the whole kept window, `0` for
923 /// `until` means now. The product is the half that knows what it keeps, and
924 /// a second arithmetic here would be a second answer to one question.
925 ///
926 /// # The default REFUSES BY NAME
927 ///
928 /// [`Product::purge_tenant`]'s rule, for the same shape of danger. The
929 /// permissive answer here is an empty series, and an empty series is
930 /// rendered as *this tenant has gone quiet* — so a product that never
931 /// implemented this would put every one of its customers on the SILENT list
932 /// and an operator would go and ask them why they had stopped. A refusal
933 /// naming the product is the safe wrong answer, and it says which plugin
934 /// owes the work.
935 fn read_activity(
936 &self,
937 tenant: &TenantId,
938 since_unix_ms: u64,
939 until_unix_ms: u64,
940 ) -> Result<Activity, ProductError> {
941 let _ = (tenant, since_unix_ms, until_unix_ms);
942 Err(ProductError::Refused(format!(
943 "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.",
944 self.facts().id
945 )))
946 }
947 fn push_entitlement(&self, fact: &EntitlementFact) -> Result<(), ProductError>;
948
949 /// **Can this product actually DELIVER these caps to this tenant, today —
950 /// and out of what?** `Ok` says where the room comes from ([`Absorb`]);
951 /// `Err(reason)` is a refusal that names what is missing, in words for an
952 /// OPERATOR (the transaction gives a customer its own sentence instead).
953 ///
954 /// # Why it exists
955 ///
956 /// Everything else in monetize checks whether the money can be taken and
957 /// whether the iron can be bought. Nothing asked the third question, and it
958 /// is the one that decides whether the sale is honest: **once the disk is
959 /// attached, can the product use it?**
960 ///
961 /// For gunnar today the answer is often NO. The appliance's `/data` is a
962 /// 4x75 GB raid0 xfs set laid once at install (`InstallMode::Fresh`), there
963 /// is no runtime data-set growth, and gunnar is PID 1 with no shell — so a
964 /// disk monetize buys and attaches is **billed and invisible**. Selling a
965 /// bigger quota there does not fail; it succeeds, charges the customer, signs
966 /// a fact promising capacity, and the capacity is not there. That is the
967 /// worst failure shape this system has: money moved, everything green, the
968 /// promise hollow.
969 ///
970 /// # The contract
971 ///
972 /// * `caps` is the FULL target cap set, not a delta — the product is asked
973 /// about the world it would have to serve, not about the change.
974 /// * It is asked **before the reserve**, so a refusal costs nothing and
975 /// nothing has to be unwound. See
976 /// `monetize::transaction::Transaction::increase`.
977 /// * `Ok` is a claim, not a shrug. A plugin that cannot tell must say so
978 /// in an `Err`, because a plugin that guesses yes is indistinguishable
979 /// from one that knows, right up until a tenant is charged for nothing.
980 /// * `Ok(Absorb::OnHand)` forbids a purchase and `Ok(Absorb::WithIron)`
981 /// permits one. A product whose store cannot use bought iron must never
982 /// answer `WithIron`: it refuses instead, and says what would help.
983 /// * It has **no default implementation**, deliberately. A default `Ok(())`
984 /// would let a product that never thought about this answer yes forever,
985 /// which is exactly the silence this method exists to break.
986 ///
987 /// It may talk to the product (it is the plugin's own control plane), so it
988 /// may fail for the usual reasons; report those as a refusal with the reason
989 /// in it rather than inventing a yes.
990 fn can_absorb(&self, tenant: &TenantId, caps: &BTreeMap<String, u64>) -> Result<Absorb, String>;
991
992 /// **How much this product could still serve, across the whole product.**
993 ///
994 /// The denominator of `UI.md`'s `184 / 300 GiB` and the servable half of the
995 /// oversell number (`monetize::fleet`). [`Product::can_absorb`] asks the same
996 /// box a narrower question — *can you take THIS tenant to THIS cap* — and
997 /// answers yes or no; this one asks for the figure, because a fleet total
998 /// needs a number and not a verdict.
999 ///
1000 /// **It has no default implementation, for [`Product::can_absorb`]'s reason.**
1001 /// A default would let a product that never thought about capacity answer
1002 /// forever, and whichever constant it returned would be a lie: 0 reads as a
1003 /// full disk, `u64::MAX` reads as an empty one, and the truth for a product
1004 /// that cannot measure is [`Servable::Unmeasured`] — which the plugin must
1005 /// say in its own words, naming what is missing.
1006 ///
1007 /// It may talk to the product, so it may fail for the usual reasons; report
1008 /// those as `Unmeasured` with the reason in them rather than inventing a
1009 /// number.
1010 fn servable(&self) -> Servable;
1011
1012 /// **What the box of ONE estate of this product can serve** (STINKY ST-11, ONE monetize
1013 /// selling several sites of one product): a pulled product answers from that estate's
1014 /// main's report. The default — a product with one box — is [`Product::servable`].
1015 fn servable_on(&self, estate: &str) -> Servable {
1016 let _ = estate;
1017 self.servable()
1018 }
1019
1020 /// **Is the PAID DOOR actually there?** One call to a verb only a metered
1021 /// build answers, so a deployment that sells can prove the box it sells
1022 /// against can enforce what it sold.
1023 ///
1024 /// # Why this is not covered by anything else
1025 ///
1026 /// Everything the poller does — `list_tenants`, `read_usage` — is an OPEN
1027 /// build's service. gunnar's community ISO answers both perfectly, and
1028 /// `poll tenants=11 failures=0` therefore proves connectivity and nothing
1029 /// whatever about entitlements. The first thing that finds out is a paying
1030 /// customer: the sink creates the principal, pushes the entitlement,
1031 /// `Entitlement.Set` answers `UNIMPLEMENTED`, and the signup is refused
1032 /// with the money already taken (`monetize_server::signup::Owed`). The last
1033 /// proof this estate's appliance is a metered build is a journal line from
1034 /// 2026-09-15.
1035 ///
1036 /// So: a READ-ONLY metered verb, asked on a schedule. It must not create,
1037 /// change or delete anything — it is called on every poll tick, against a
1038 /// live box, forever.
1039 ///
1040 /// # The default is UNPROVEN, and unproven is not "yes"
1041 ///
1042 /// Unlike [`Product::can_absorb`] and [`Product::servable`], which have no
1043 /// default because every constant they could return is a lie about
1044 /// capacity, this one has a safe answer that is not a claim:
1045 /// [`MeteredDoor::Unproven`], naming the plugin that owes the work. A
1046 /// default of `Enforcing` would let every plugin that never thought about
1047 /// it assert the one thing this exists to check.
1048 fn metered_door(&self) -> MeteredDoor {
1049 MeteredDoor::Unproven(format!(
1050 "the {} plugin has no metered-door probe, so nothing has asked this box whether it can enforce an entitlement at all",
1051 self.facts().id
1052 ))
1053 }
1054
1055 /// **Delete everything this tenant has in the product, and keep them locked
1056 /// out while it happens.**
1057 ///
1058 /// The product half of a super purge. `monetize` can give the IRON back on
1059 /// its own — `transaction::release_tenant` and
1060 /// `Give::References` — and it cannot delete a byte of what is on
1061 /// it, because only the product knows what a tenant's data IS. In gunnar it
1062 /// is every repository in the account plus two classes of bytes that are
1063 /// not in the catalog at all; nothing on this side of the seam could
1064 /// enumerate that.
1065 ///
1066 /// # It must LOCK, and the lock is the product's
1067 ///
1068 /// A purge is not atomic — it walks and reclaims over minutes while the
1069 /// product goes on serving — so anything the tenant does during the walk
1070 /// lands behind it. The lockout that prevents that can only live where the
1071 /// requests arrive, which is the product. gunnar's is `Accounts.Gate`, and
1072 /// it is total rather than read-only because in gunnar a read creates
1073 /// things: a fetch explodes objects into a cache the purge just unlinked.
1074 ///
1075 /// [`Purged::still_locked`] carries the outcome back, because a product
1076 /// that emptied a tenant and could not let them back in has left an account
1077 /// nobody can use, and that must not read as success anywhere above here.
1078 ///
1079 /// # The default REFUSES BY NAME
1080 ///
1081 /// Unlike [`Product::can_absorb`] and [`Product::servable`], which have no
1082 /// default at all because every constant they could return is a lie about
1083 /// capacity, this one's dangerous answer is a permissive `Ok` — a product
1084 /// that never implemented it reporting a tenant's data gone when it is
1085 /// still there, after which a super purge would cheerfully destroy the
1086 /// disks it is on. A refusal naming the product is the safe wrong answer
1087 /// and it says which product owes the work.
1088 /// **"Add users", asked before money moves** (owner, 2026-10-04): would
1089 /// `ask` fit in the room the tenant (an account) holds and has neither
1090 /// stored nor promised, and if not, how much is missing? The product
1091 /// answers from its own books — it knows what is stored and what it
1092 /// promised its users; monetize never measures — and writes nothing. A
1093 /// name the product would refuse (one that exists) is refused here too,
1094 /// so no money moves for a user that could never be created.
1095 ///
1096 /// The default refuses by name: a product with no users to add has no
1097 /// "yes" to give.
1098 fn users_fit(&self, tenant: &TenantId, ask: &UsersAsk) -> Result<UsersAnswer, ProductError> {
1099 let _ = (tenant, ask);
1100 Err(ProductError::Refused(format!("the {} plugin cannot add users to a tenant", self.facts().id)))
1101 }
1102
1103 /// **"Add users", granted**: ONE named user, or `ask.count` unnamed seats,
1104 /// each holding `ask.room_bytes` of the tenant's room. `order` is the
1105 /// reference the grant belongs to, so a retry answers the first grant
1106 /// (`UsersAnswer::unchanged`) instead of adding twice. Called when the
1107 /// room is there — at once from unused room, or once an order bought the
1108 /// rest — and the product checks the room again in the write.
1109 fn add_users(&self, tenant: &TenantId, ask: &UsersAsk, order: &str) -> Result<UsersAnswer, ProductError> {
1110 let _ = (tenant, ask, order);
1111 Err(ProductError::Refused(format!("the {} plugin cannot add users to a tenant", self.facts().id)))
1112 }
1113
1114 fn purge_tenant(&self, tenant: &TenantId, reason: &str) -> Result<Purged, ProductError> {
1115 let _ = (tenant, reason);
1116 Err(ProductError::Refused(format!(
1117 "the {} plugin cannot purge a tenant's data: it has no purge verb, so nothing here \
1118 can promise the tenant's bytes are gone. Empty the tenant in the product itself \
1119 before releasing its resources.",
1120 self.facts().id
1121 )))
1122 }
1123
1124 /// ★ UW-4: **Is room sold past the set QUEUED for a resize the product's own estate does**
1125 /// (the pull channel: monetize grows nothing it cannot reach)? `false` — the default —
1126 /// leaves the answer to the grow lane ([`Product::grow`]).
1127 fn queues_room(&self) -> bool {
1128 false
1129 }
1130
1131 /// ★ UW-4: **Tell the estate serving `tenant` it was sold `need_bytes` past its room**, to
1132 /// be resized later. Only asked of a product that [`Product::queues_room`]. The default
1133 /// refuses by name.
1134 fn queue_room(&self, tenant: &TenantId, need_bytes: u64, why: &str) -> Result<(), ProductError> {
1135 let _ = (tenant, need_bytes, why);
1136 Err(ProductError::Refused(format!("the {} plugin queues no room", self.facts().id)))
1137 }
1138
1139 /// **The product's own half of a data-set growth**, if it has one. `None`
1140 /// — the default — is a product whose set cannot be grown at runtime from
1141 /// outside; monetize then refuses a growth by name before touching a
1142 /// volume. See [`ApplianceGrow`].
1143 fn grow(&self) -> Option<&dyn ApplianceGrow> {
1144 None
1145 }
1146
1147 /// **The TWIN's own half of ITS data-set growth**, if the product has a twin whose
1148 /// control plane monetize can reach. Owner ruling 2026-09-22 (T21): the twin, built
1149 /// from the same medium, grows EXACTLY like main — the same [`ApplianceGrow`] verbs on
1150 /// the twin's own control plane (drain, unmount, wait, grow, mount; `maintenance`
1151 /// meanwhile) around the cloud's same `grow_set`, and it is NEVER stopped or
1152 /// restarted. `None` — the default — is a product whose twin cannot be grown that
1153 /// way; its twin-growth lane then holds by name rather than stop the box.
1154 fn twin_grow(&self) -> Option<&dyn ApplianceGrow> {
1155 None
1156 }
1157
1158 /// **The product's twins, as its primary hears them** — one row per twin:
1159 /// the heartbeat state, and the fill of the twin's data volume the twin
1160 /// reported on its last poll. What the batcher's twin-fill trigger reads.
1161 /// The default is no twins: a product without a twin has nothing to grow.
1162 fn twins(&self) -> Result<Vec<TwinFill>, ProductError> {
1163 Ok(Vec::new())
1164 }
1165}
1166
1167/// **A plugin's product served under another id** (G15): `[products.gunnar-test2]
1168/// plugin = "gunnar"` is the gunnar plugin, answering `gunnar-test2`. Everything
1169/// but its facts' id ([`ProductFacts::id`]) is the plugin's own; the id is the BLOCK's, which is what
1170/// the ledger, the prices, the vendor route and the signing key key on — so two
1171/// estates of one product never share one id.
1172pub struct ServedAs {
1173 id: &'static str,
1174 inner: Box<dyn Product>,
1175}
1176
1177impl ServedAs {
1178 pub fn new(id: &'static str, inner: Box<dyn Product>) -> Self {
1179 Self { id, inner }
1180 }
1181 /// The id the plugin itself answers.
1182 pub fn plugin_id(&self) -> &'static str {
1183 self.inner.facts().id
1184 }
1185}
1186
1187impl Product for ServedAs {
1188 fn facts(&self) -> ProductFacts {
1189 ProductFacts { id: self.id, ..self.inner.facts() }
1190 }
1191 fn list_tenants(&self) -> Result<Vec<TenantId>, ProductError> {
1192 self.inner.list_tenants()
1193 }
1194 fn read_usage(&self, tenant: &TenantId) -> Result<Usage, ProductError> {
1195 self.inner.read_usage(tenant)
1196 }
1197 fn read_usage_page(&self, req: &UsagePageReq) -> Result<Option<UsagePage>, ProductError> {
1198 self.inner.read_usage_page(req)
1199 }
1200 fn tenant_owners(&self) -> Result<Option<Vec<TenantOwner>>, ProductError> {
1201 self.inner.tenant_owners()
1202 }
1203 fn read_activity(&self, tenant: &TenantId, since_unix_ms: u64, until_unix_ms: u64) -> Result<Activity, ProductError> {
1204 self.inner.read_activity(tenant, since_unix_ms, until_unix_ms)
1205 }
1206 fn push_entitlement(&self, fact: &EntitlementFact) -> Result<(), ProductError> {
1207 self.inner.push_entitlement(fact)
1208 }
1209 fn can_absorb(&self, tenant: &TenantId, caps: &BTreeMap<String, u64>) -> Result<Absorb, String> {
1210 self.inner.can_absorb(tenant, caps)
1211 }
1212 fn servable(&self) -> Servable {
1213 self.inner.servable()
1214 }
1215 fn servable_on(&self, estate: &str) -> Servable {
1216 self.inner.servable_on(estate)
1217 }
1218 fn metered_door(&self) -> MeteredDoor {
1219 self.inner.metered_door()
1220 }
1221 fn users_fit(&self, tenant: &TenantId, ask: &UsersAsk) -> Result<UsersAnswer, ProductError> {
1222 self.inner.users_fit(tenant, ask)
1223 }
1224 fn add_users(&self, tenant: &TenantId, ask: &UsersAsk, order: &str) -> Result<UsersAnswer, ProductError> {
1225 self.inner.add_users(tenant, ask, order)
1226 }
1227 fn purge_tenant(&self, tenant: &TenantId, reason: &str) -> Result<Purged, ProductError> {
1228 self.inner.purge_tenant(tenant, reason)
1229 }
1230 fn grow(&self) -> Option<&dyn ApplianceGrow> {
1231 self.inner.grow()
1232 }
1233 fn twin_grow(&self) -> Option<&dyn ApplianceGrow> {
1234 self.inner.twin_grow()
1235 }
1236 fn queues_room(&self) -> bool {
1237 self.inner.queues_room()
1238 }
1239 fn queue_room(&self, tenant: &TenantId, need_bytes: u64, why: &str) -> Result<(), ProductError> {
1240 self.inner.queue_room(tenant, need_bytes, why)
1241 }
1242 fn twins(&self) -> Result<Vec<TwinFill>, ProductError> {
1243 self.inner.twins()
1244 }
1245}
1246
1247/// **What the host says about a ticket somebody presented** (`monetize_server::site_ticket`).
1248///
1249/// Not a `bool` and not a `Result<(), ProductError>`: a refusal is a normal,
1250/// expected answer that carries a SENTENCE — the words a caller may show — and
1251/// a `ProductError` means something else entirely (the product's control plane
1252/// is down, the question could not be asked). Conflating the two would let an
1253/// unreachable appliance read as a refused ticket, or worse, the reverse.
1254#[derive(Clone, PartialEq, Eq, Debug)]
1255pub enum ActorVerdict {
1256 /// The ticket verifies, names this tenant, this product and this verb, is
1257 /// inside its life, and has not been spent before.
1258 Allowed,
1259 /// Refused, and why — in words a customer surface may show. A verifier must
1260 /// not name the ticket's own tenant back to whoever asked about another
1261 /// one; see `monetize_embed::verify_ticket`.
1262 Refused(String),
1263}
1264
1265/// One twin as its primary last heard from it (gunnar's `TwinHeartbeat`).
1266#[derive(Clone, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
1267pub struct TwinFill {
1268 pub principal: String,
1269 pub address: String,
1270 /// `ok` | `behind` | `silent`.
1271 pub state: String,
1272 /// Wall clock of the twin's last poll; 0 = never heard.
1273 pub last_seen_unix_ms: i64,
1274 pub lag_entries: u64,
1275 pub disk_total_bytes: u64,
1276 pub disk_used_bytes: u64,
1277 /// used × 1000 / total; 0 when total is unknown.
1278 pub fill_permille: u32,
1279}
1280
1281impl TwinFill {
1282 /// The primary can still hear it: `ok` or `behind`, never `silent` or a word we do not know.
1283 pub fn visible(&self) -> bool {
1284 matches!(self.state.as_str(), "ok" | "behind")
1285 }
1286}
1287
1288// ── the appliance's half of a data-set growth ───────────────────────────────
1289
1290/// **What the appliance does INSIDE the box while the outside actor grows its
1291/// volumes** (`DATA-SET-GROWTH-FLOW.md` §0 ruling 1, T6): drain, unmount, wait
1292/// for the members to come back larger, run its engine, mount, reopen. monetize
1293/// drives it — flush → start → wait `unmounted` → cloud steps → resume → wait
1294/// `done|failed` — and every verb here is one call on the product's control
1295/// plane. Vendor-neutral: the words are the phases the flow document names,
1296/// and the go-ahead is opaque bytes the product verifies on its own terms.
1297pub trait ApplianceGrow: Send + Sync {
1298 /// **Prove the twin is caught up** (T11, step 0) — flush replication to
1299 /// every standby, bounded by `timeout_secs` (0 = the product's own ceiling),
1300 /// and report. Changes no role and no roster.
1301 fn flush(&self, timeout_secs: u32) -> Result<FlushReport, ProductError>;
1302 /// Begin the growth: drain, unmount, wait for the members. `go_ahead` is
1303 /// the signed approval (see `monetize_embed::signing::go_ahead_message`);
1304 /// `target_sectors_per_member` is in 512-byte sectors, 0 = "whatever every
1305 /// member comes back larger at". Answers the status after the call.
1306 fn start(&self, target_sectors_per_member: u64, go_ahead: &[u8]) -> Result<GrowStatus, ProductError>;
1307 /// The phase and the members, as the product sees them. **Must answer
1308 /// while the set is unmounted and, where the product offers it, without a
1309 /// credential** — the outside actor decides whether to re-attach from it.
1310 fn status(&self) -> Result<GrowStatus, ProductError>;
1311 /// "Look again": the members are back. With `give_up`, stop waiting and
1312 /// serve whatever is there at whatever size it is.
1313 fn resume(&self, give_up: bool) -> Result<GrowStatus, ProductError>;
1314}
1315
1316/// The phases a product reports, as `DATA-SET-GROWTH-FLOW.md` names them.
1317/// Kept as words on the wire (`GrowStatus::phase`); this is the reader.
1318#[derive(Clone, Copy, PartialEq, Eq, Debug)]
1319pub enum GrowPhase {
1320 Idle,
1321 Draining,
1322 Unmounted,
1323 WaitingMembers,
1324 Growing,
1325 Mounting,
1326 Done,
1327 Failed,
1328 /// A word this monetize does not know — a newer product. Shown, never
1329 /// acted on.
1330 Other,
1331}
1332
1333impl GrowPhase {
1334 pub fn parse(word: &str) -> GrowPhase {
1335 match word {
1336 "idle" => GrowPhase::Idle,
1337 "draining" => GrowPhase::Draining,
1338 "unmounted" => GrowPhase::Unmounted,
1339 "waiting-members" => GrowPhase::WaitingMembers,
1340 "growing" => GrowPhase::Growing,
1341 "mounting" => GrowPhase::Mounting,
1342 "done" => GrowPhase::Done,
1343 "failed" => GrowPhase::Failed,
1344 _ => GrowPhase::Other,
1345 }
1346 }
1347 /// The set is off the stripe: the outside actor may touch the volumes.
1348 pub fn volumes_free(self) -> bool {
1349 matches!(self, GrowPhase::Unmounted | GrowPhase::WaitingMembers)
1350 }
1351 pub fn is_terminal(self) -> bool {
1352 matches!(self, GrowPhase::Done | GrowPhase::Failed | GrowPhase::Idle)
1353 }
1354}
1355
1356/// One member of the set as the product last surveyed it.
1357#[derive(Clone, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
1358pub struct GrowMember {
1359 pub index: u32,
1360 pub device: String,
1361 pub disk_bytes: u64,
1362 pub set_bytes: u64,
1363 pub larger: bool,
1364}
1365
1366/// The product's answer to [`ApplianceGrow::status`].
1367#[derive(Clone, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
1368pub struct GrowStatus {
1369 /// `idle` | `draining` | `unmounted` | `waiting-members` | `growing` |
1370 /// `mounting` | `done` | `failed`. Read with [`GrowPhase::parse`].
1371 pub phase: String,
1372 /// `failed`'s reason; empty otherwise.
1373 pub why: String,
1374 pub epoch: u64,
1375 pub target_sectors_per_member: u64,
1376 pub members: Vec<GrowMember>,
1377 pub members_verdict: String,
1378 pub engine_present: bool,
1379 pub serving: bool,
1380 pub in_flight: u64,
1381 pub since_unix_ms: i64,
1382 pub detail: String,
1383 pub go_ahead_signer: String,
1384 pub set_uuid: String,
1385 pub total_bytes: u64,
1386}
1387
1388impl GrowStatus {
1389 pub fn phase(&self) -> GrowPhase {
1390 GrowPhase::parse(&self.phase)
1391 }
1392}
1393
1394/// The product's answer to [`ApplianceGrow::flush`]: is the twin caught up,
1395/// and what does that prove.
1396#[derive(Clone, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
1397pub struct FlushReport {
1398 pub caught_up: bool,
1399 pub in_flight: u64,
1400 pub pending: u64,
1401 pub bus_gaps: u64,
1402 pub needs_full_resync: bool,
1403 pub last_success_unix_ms: i64,
1404 pub flushed_at_unix_ms: i64,
1405 pub waited_ms: u64,
1406 pub catalog_repos: u64,
1407 pub standbys: u64,
1408 pub verdict: String,
1409}
1410
1411/// **"Add users"** (owner, 2026-10-04): `count` users, each holding
1412/// `room_bytes` of the tenant's room. `count` 1 is ONE named user (`name`);
1413/// more is that many SEATS with no names, filled later (an invite, an
1414/// import). `room_bytes` 0 is the product's own default. The rules a person
1415/// types against (the range, the names, the room's floor) are the PRODUCT's,
1416/// which every door shares.
1417#[derive(Clone, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
1418#[serde(default)]
1419pub struct UsersAsk {
1420 pub count: u32,
1421 pub room_bytes: u64,
1422 #[serde(skip_serializing_if = "String::is_empty")]
1423 pub name: String,
1424 /// Where the named user's invite is mailed; empty: nowhere. Not part of
1425 /// the ask's identity ([`UsersAsk::fingerprint`]).
1426 #[serde(skip_serializing_if = "String::is_empty")]
1427 pub email: String,
1428 /// The console the invite is redeemed at.
1429 #[serde(skip_serializing_if = "String::is_empty")]
1430 pub invite_origin: String,
1431 /// **The named user as the ledger knows them** (G13, 5.4): an opaque id
1432 /// (`monetize::ledger::principal_id`), never the name. Empty for seats.
1433 #[serde(skip_serializing_if = "String::is_empty")]
1434 pub principal_id: String,
1435}
1436
1437impl UsersAsk {
1438 /// **What a ticket for this ask covers** — the "caps" an `order` actor
1439 /// ticket is minted over and compared against, so a ticket vouches for
1440 /// exactly this number of users at exactly this room, and no other.
1441 pub fn ticket_caps(&self) -> BTreeMap<String, u64> {
1442 [("users".to_owned(), u64::from(self.count.max(1))), ("user_room_bytes".to_owned(), self.room_bytes)].into_iter().collect()
1443 }
1444
1445 /// A short, stable fingerprint for a reference: `2x10737418240` or
1446 /// `1x10737418240:p-<id>` — the named user by [`UsersAsk::principal_id`],
1447 /// never by name (G13).
1448 pub fn fingerprint(&self) -> String {
1449 match self.principal_id.as_str() {
1450 "" => format!("{}x{}", self.count.max(1), self.room_bytes),
1451 id => format!("{}x{}:{id}", self.count.max(1), self.room_bytes),
1452 }
1453 }
1454
1455 /// **The ask as the ledger may hold it**: no name, no address — the
1456 /// named user only by [`UsersAsk::principal_id`].
1457 pub fn without_names(&self) -> UsersAsk {
1458 UsersAsk { name: String::new(), email: String::new(), ..self.clone() }
1459 }
1460
1461 /// One named user (the name, or its id, is known).
1462 pub fn is_named(&self) -> bool {
1463 !self.name.is_empty() || !self.principal_id.is_empty()
1464 }
1465}
1466
1467/// The product's answer to [`Product::users_fit`] / [`Product::add_users`].
1468#[derive(Clone, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
1469#[serde(default)]
1470pub struct UsersAnswer {
1471 /// The named user, once created; empty for seats and for a dry run.
1472 pub principal: String,
1473 /// Bytes each user holds, as the product resolved it (its default applied).
1474 pub room_bytes: u64,
1475 /// What the ask needs of the tenant's room: count × room.
1476 pub need_bytes: u64,
1477 /// The tenant's room neither stored nor promised, before the ask. `None`:
1478 /// nothing caps the tenant there.
1479 pub unused_bytes: Option<u64>,
1480 /// How much is missing; 0 = it fits.
1481 pub shortfall_bytes: u64,
1482 /// The tenant's seats after this: every seat, and the open ones.
1483 pub seats: u32,
1484 pub seats_open: u32,
1485 /// Nothing was written: a dry run, or the retry of a granted order.
1486 pub unchanged: bool,
1487 /// The new named user's invite secret, on the answer that created the
1488 /// user (and never again): what a `<console>/login?invite=` link carries.
1489 #[serde(skip_serializing_if = "String::is_empty")]
1490 pub invite_secret: String,
1491}
1492
1493/// What [`Product::purge_tenant`] did.
1494#[derive(Clone, PartialEq, Eq, Debug)]
1495pub struct Purged {
1496 pub tenant: TenantId,
1497 /// What the product removed, in its own words and its own units — "5 of 5
1498 /// stores, 1.2 GiB". Free text because every product counts different
1499 /// things and a shared schema would force each of them to lie a little.
1500 pub detail: String,
1501 /// Bytes the product says it reclaimed. `None` means it does not count
1502 /// them, which is not the same as zero and must not be rendered as it.
1503 pub bytes_reclaimed: Option<u64>,
1504 /// **Is the tenant still locked out of the product?**
1505 ///
1506 /// `true` is a real and expected outcome — an operator may have suspended
1507 /// the account separately, and the purge correctly refuses to lift a
1508 /// lockout it did not take — but it is also what a purge that could not
1509 /// unlock reports, and either way the tenant cannot use what is left.
1510 /// Carried so a super purge can say so rather than infer it.
1511 pub still_locked: bool,
1512 /// One line per thing the product could not remove. Non-empty means the
1513 /// tenant's data is PARTIALLY there, and the caller must not go on to
1514 /// destroy the iron it is sitting on.
1515 pub failures: Vec<String>,
1516}
1517
1518/// **Bytes in the largest unit that still leaves a digit before the decimal
1519/// point**, by integer arithmetic: `10.0 GiB`, `64.0 MiB`, `999 B`.
1520///
1521/// One writer, because there used to be three and every one of them carried the
1522/// same bug. Each divided by 1 GiB unconditionally and printed one decimal, so
1523/// anything under a gibibyte rendered `0.0 GiB` — and `0.0` beside a quantity is
1524/// read as *there is none*:
1525///
1526/// * the console (then a Plans page) showed a real 64 MiB
1527/// `pack_bytes` cap as `0.0 GiB`, which an operator reads as "no quota";
1528/// * the old catalogue validator said *"sells pack_bytes = 10.0 GiB but
1529/// its resources buy only 10.0 GiB — short 0.0 GiB"*, a sentence in which
1530/// every number is wrong in the direction of "nothing is the matter";
1531/// * `products/gunnar`'s capacity refusals said "short 0.0 GiB" for the same
1532/// reason.
1533///
1534/// It lives here because this is the crate that already owns the meter
1535/// vocabulary ([`Meter`], [`Backing::DiskBytes`]) and the only one every side
1536/// can depend on: core, the product plugins, and the browser console alike. It
1537/// pulls in nothing (this crate is `serde` and nothing else), so the wasm
1538/// bundle pays a few hundred bytes for a formatter it was carrying anyway.
1539///
1540/// Truncates, never rounds up: a reading must not appear to cross a cap it has
1541/// not crossed.
1542pub fn bytes(n: u64) -> String {
1543 const KIB: u64 = 1 << 10;
1544 const MIB: u64 = 1 << 20;
1545 const GIB: u64 = 1 << 30;
1546 const TIB: u64 = 1 << 40;
1547 let (unit, per) = match n {
1548 n if n >= TIB => ("TiB", TIB),
1549 n if n >= GIB => ("GiB", GIB),
1550 n if n >= MIB => ("MiB", MIB),
1551 n if n >= KIB => ("KiB", KIB),
1552 // Under a kibibyte there is nothing to scale to, and `0.0 KiB` would be
1553 // the same lie one unit down. Bytes are exact and short.
1554 n => return format!("{n} B"),
1555 };
1556 format!("{}.{} {unit}", n / per, ((n % per) * 10) / per)
1557}
1558
1559#[cfg(test)]
1560mod backing_tests {
1561 use super::Backing;
1562
1563 /// **A byte over the GiB is sold the next GiB, and a millicore is priced
1564 /// as a thousandth of a core.** Distinct non-zero numbers: 2200 öre per
1565 /// GiB-month, 90 GiB + 1 byte, 1500 millicores at 30 000.
1566 #[test]
1567 fn prices_round_bytes_up_to_the_unit_and_cpu_by_the_millicore() {
1568 const GIB: u64 = 1 << 30;
1569 assert_eq!(Backing::DiskBytes.price_month(90 * GIB, 2200), 198_000, "90 GiB × 22.00 SEK");
1570 assert_eq!(Backing::DiskBytes.price_month(90 * GIB + 1, 2200), 200_200, "one byte over is the 91st GiB");
1571 assert_eq!(Backing::DiskBytes.price_month(0, 2200), 0);
1572 assert_eq!(Backing::RamBytes.price_month(3 * GIB, 700), 2100);
1573 assert_eq!(Backing::CpuMillicores.price_month(1500, 30_000), 45_000, "1.5 cores at 300.00");
1574 assert_eq!(Backing::CpuMillicores.price_month(1, 30_000), 30, "one millicore is not free and not a core");
1575 // A counted unit is flat: five properties at 200.00 SEK-month are 1000.00,
1576 // and one is 200.00 — no rounding, no thousandths. This is the arm a per-property
1577 // subscription rides on; before it existed a property priced to zero.
1578 assert_eq!(Backing::Units.price_month(5, 20_000), 100_000, "5 × 200.00");
1579 assert_eq!(Backing::Units.price_month(1, 20_000), 20_000);
1580 assert_eq!(Backing::Units.price_month(0, 20_000), 0);
1581 assert_eq!(Backing::Units.unit(), "unit_month");
1582 // A zero unit price is FREE, whatever the delta: the open-source list.
1583 for b in Backing::ALL {
1584 assert_eq!(b.price_month(u64::MAX / 4, 0), 0, "{b:?}");
1585 }
1586 // The unit names are the price list's keys, both ways.
1587 for b in Backing::ALL {
1588 assert_eq!(Backing::from_unit(b.unit()), Some(b));
1589 }
1590 assert_eq!(Backing::from_unit("moon_month"), None);
1591 }
1592}
1593
1594#[cfg(test)]
1595mod bytes_tests {
1596 use super::bytes;
1597
1598 /// **A quantity that exists never renders as zero.** RED before this
1599 /// function existed: `0.0 GiB` for every value under 2^30, in three separate
1600 /// copies of the same six lines.
1601 #[test]
1602 fn only_a_genuine_zero_reads_as_zero() {
1603 for n in [1u64, 512, 1 << 20, 67_108_864, (1 << 30) - 1] {
1604 let s = bytes(n);
1605 assert!(!s.starts_with("0.0 ") && !s.starts_with("0 "), "{n} bytes rendered as {s:?}, which reads as nothing");
1606 }
1607 assert_eq!(bytes(0), "0 B");
1608 }
1609
1610 #[test]
1611 fn the_unit_scales_and_the_value_truncates() {
1612 assert_eq!(bytes(999), "999 B");
1613 assert_eq!(bytes(1536), "1.5 KiB");
1614 assert_eq!(bytes(67_108_864), "64.0 MiB");
1615 assert_eq!(bytes((1 << 30) - 1), "1023.9 MiB", "truncates, never rounds up past the cap");
1616 assert_eq!(bytes(10 << 30), "10.0 GiB");
1617 assert_eq!(bytes(3 << 40), "3.0 TiB");
1618 assert_eq!(bytes(u64::MAX), "16777215.9 TiB", "no overflow at the top of the range");
1619 }
1620}
1621
1622#[cfg(test)]
1623mod actor_tests {
1624 use super::*;
1625
1626 /// A plugin written before the tenant path existed: it implements every
1627 /// method the trait REQUIRES and knows nothing about tickets.
1628 struct OldPlugin;
1629
1630 impl Product for OldPlugin {
1631 fn facts(&self) -> ProductFacts {
1632 ProductFacts::new("old", &[])
1633 }
1634 fn list_tenants(&self) -> Result<Vec<TenantId>, ProductError> {
1635 Ok(Vec::new())
1636 }
1637 fn read_usage(&self, _tenant: &TenantId) -> Result<Usage, ProductError> {
1638 Err(ProductError::Refused("no".into()))
1639 }
1640 fn push_entitlement(&self, _fact: &EntitlementFact) -> Result<(), ProductError> {
1641 Ok(())
1642 }
1643 fn can_absorb(&self, _tenant: &TenantId, _caps: &BTreeMap<String, u64>) -> Result<Absorb, String> {
1644 Ok(Absorb::OnHand)
1645 }
1646 fn servable(&self) -> Servable {
1647 Servable::Unmeasured("a test double measures nothing".to_owned())
1648 }
1649 }
1650
1651 /// ★ **Safe by OMISSION, not by remembering.**
1652 ///
1653 /// The out-of-tree plugin nobody will edit must not be offered self-service renewal: the
1654 /// default does not advertise it. The ticket itself is the host's to check (A32-13: the
1655 /// host checks it under the block's or the delivered key, `monetize_server::site_ticket`),
1656 /// so the trait has no verb a plugin could answer it with.
1657 #[test]
1658 fn a_product_that_never_heard_of_a_ticket_offers_no_renewal() {
1659 let p = OldPlugin;
1660 assert!(!p.facts().tickets, "a console must not be told to offer self-service here");
1661 }
1662}
1663
1664#[cfg(test)]
1665mod estate_tenant_tests {
1666 use super::*;
1667
1668 /// ★ G16: a sold estate's tenant is `@<estate>/<namespace>` on the ONE ledger and reads
1669 /// back whole (a namespace keeps its own slashes); a tenant of the product's own estate,
1670 /// nested or not, never reads as one, and a half-mark is no estate.
1671 #[test]
1672 fn a_sold_estates_tenant_carries_the_estate_in_its_path_and_no_own_tenant_reads_as_one() {
1673 let t = TenantId::on_estate("acme", "bob");
1674 assert_eq!(t.0, "@acme/bob");
1675 assert_eq!(t.estate(), Some(("acme", "bob")));
1676 assert_eq!(TenantId::on_estate("acme", "team/sub").estate(), Some(("acme", "team/sub")));
1677 for own in ["bob", "team/sub", "acme/bob", "", "@acme", "@/bob", "@acme/", "x@acme/bob"] {
1678 assert_eq!(TenantId(own.into()).estate(), None, "{own:?}");
1679 }
1680 }
1681}