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 /// (`CloudProvider::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 (`CloudProvider::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/// The plugin seam. Sync for the same reason as the vendor traits.
770pub trait Product: Send + Sync {
771 /// `gunnar`, `holger`. Also the first segment of every payment reference.
772 fn id(&self) -> &'static str;
773 /// The meters this product declares — the UI and the order form read this,
774 /// so an order can only cap a meter that exists, and the console can say
775 /// whether a cap on it is policed ([`Enforcement`]).
776 fn meters(&self) -> &[Meter];
777
778 fn list_tenants(&self) -> Result<Vec<TenantId>, ProductError>;
779 fn read_usage(&self, tenant: &TenantId) -> Result<Usage, ProductError>;
780
781 /// **Many tenants' usage in one call, optionally only what CHANGED** —
782 /// see [`UsagePageReq`] (2026-10-06).
783 ///
784 /// `Ok(None)` means *this product (or the box behind it) has no batched
785 /// read*: an older plugin, an older guest, an older appliance. The caller
786 /// then falls back to [`Product::read_usage`] per tenant, exactly as before
787 /// — which is why this has a default and why the default is `None` rather
788 /// than a refusal: a product that never heard of batching must keep being
789 /// polled, not stop being polled.
790 fn read_usage_page(&self, req: &UsagePageReq) -> Result<Option<UsagePage>, ProductError> {
791 let _ = req;
792 Ok(None)
793 }
794
795 /// **Who each tenant is** ([`TenantOwner`]) — what a SELLING shop's poller asks so every
796 /// person's account holds a fact (2026-10-08: an account an invite, an operator or a
797 /// failed sign-up made held none and was UNCAPPED). `Ok(None)`: this product (or its box)
798 /// cannot say — nothing is issued, exactly as before.
799 fn tenant_owners(&self) -> Result<Option<Vec<TenantOwner>>, ProductError> {
800 Ok(None)
801 }
802
803 /// **What the tenant has been DOING, day by day** — the flow series behind
804 /// the console's SILENT and FRICTION lists.
805 ///
806 /// [`Product::read_usage`] answers *how much is held*. Only this answers
807 /// *is this customer using the product, has it gone quiet, and is it being
808 /// refused* — and no stock meter has ever been able to.
809 ///
810 /// The window is Unix milliseconds, inclusive, resolved by the PRODUCT to
811 /// whole UTC days: `0` for `since` means the whole kept window, `0` for
812 /// `until` means now. The product is the half that knows what it keeps, and
813 /// a second arithmetic here would be a second answer to one question.
814 ///
815 /// # The default REFUSES BY NAME
816 ///
817 /// [`Product::purge_tenant`]'s rule, for the same shape of danger. The
818 /// permissive answer here is an empty series, and an empty series is
819 /// rendered as *this tenant has gone quiet* — so a product that never
820 /// implemented this would put every one of its customers on the SILENT list
821 /// and an operator would go and ask them why they had stopped. A refusal
822 /// naming the product is the safe wrong answer, and it says which plugin
823 /// owes the work.
824 fn read_activity(
825 &self,
826 tenant: &TenantId,
827 since_unix_ms: u64,
828 until_unix_ms: u64,
829 ) -> Result<Activity, ProductError> {
830 let _ = (tenant, since_unix_ms, until_unix_ms);
831 Err(ProductError::Refused(format!(
832 "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.",
833 self.id()
834 )))
835 }
836 fn push_entitlement(&self, fact: &EntitlementFact) -> Result<(), ProductError>;
837
838 /// **Can this product actually DELIVER these caps to this tenant, today —
839 /// and out of what?** `Ok` says where the room comes from ([`Absorb`]);
840 /// `Err(reason)` is a refusal that names what is missing, in words for an
841 /// OPERATOR (the transaction gives a customer its own sentence instead).
842 ///
843 /// # Why it exists
844 ///
845 /// Everything else in monetize checks whether the money can be taken and
846 /// whether the iron can be bought. Nothing asked the third question, and it
847 /// is the one that decides whether the sale is honest: **once the disk is
848 /// attached, can the product use it?**
849 ///
850 /// For gunnar today the answer is often NO. The appliance's `/data` is a
851 /// 4x75 GB raid0 xfs set laid once at install (`InstallMode::Fresh`), there
852 /// is no runtime data-set growth, and gunnar is PID 1 with no shell — so a
853 /// disk monetize buys and attaches is **billed and invisible**. Selling a
854 /// bigger quota there does not fail; it succeeds, charges the customer, signs
855 /// a fact promising capacity, and the capacity is not there. That is the
856 /// worst failure shape this system has: money moved, everything green, the
857 /// promise hollow.
858 ///
859 /// # The contract
860 ///
861 /// * `caps` is the FULL target cap set, not a delta — the product is asked
862 /// about the world it would have to serve, not about the change.
863 /// * It is asked **before the reserve**, so a refusal costs nothing and
864 /// nothing has to be unwound. See
865 /// `monetize::transaction::Transaction::increase`.
866 /// * `Ok` is a claim, not a shrug. A plugin that cannot tell must say so
867 /// in an `Err`, because a plugin that guesses yes is indistinguishable
868 /// from one that knows, right up until a tenant is charged for nothing.
869 /// * `Ok(Absorb::OnHand)` forbids a purchase and `Ok(Absorb::WithIron)`
870 /// permits one. A product whose store cannot use bought iron must never
871 /// answer `WithIron`: it refuses instead, and says what would help.
872 /// * It has **no default implementation**, deliberately. A default `Ok(())`
873 /// would let a product that never thought about this answer yes forever,
874 /// which is exactly the silence this method exists to break.
875 ///
876 /// It may talk to the product (it is the plugin's own control plane), so it
877 /// may fail for the usual reasons; report those as a refusal with the reason
878 /// in it rather than inventing a yes.
879 fn can_absorb(&self, tenant: &TenantId, caps: &BTreeMap<String, u64>) -> Result<Absorb, String>;
880
881 /// **How much this product could still serve, across the whole product.**
882 ///
883 /// The denominator of `UI.md`'s `184 / 300 GiB` and the servable half of the
884 /// oversell number (`monetize::fleet`). [`Product::can_absorb`] asks the same
885 /// box a narrower question — *can you take THIS tenant to THIS cap* — and
886 /// answers yes or no; this one asks for the figure, because a fleet total
887 /// needs a number and not a verdict.
888 ///
889 /// **It has no default implementation, for [`Product::can_absorb`]'s reason.**
890 /// A default would let a product that never thought about capacity answer
891 /// forever, and whichever constant it returned would be a lie: 0 reads as a
892 /// full disk, `u64::MAX` reads as an empty one, and the truth for a product
893 /// that cannot measure is [`Servable::Unmeasured`] — which the plugin must
894 /// say in its own words, naming what is missing.
895 ///
896 /// It may talk to the product, so it may fail for the usual reasons; report
897 /// those as `Unmeasured` with the reason in them rather than inventing a
898 /// number.
899 fn servable(&self) -> Servable;
900
901 /// **What the box of ONE estate of this product can serve** (STINKY ST-11, ONE monetize
902 /// selling several sites of one product): a pulled product answers from that estate's
903 /// main's report. The default — a product with one box — is [`Product::servable`].
904 fn servable_on(&self, estate: &str) -> Servable {
905 let _ = estate;
906 self.servable()
907 }
908
909 /// **Is the PAID DOOR actually there?** One call to a verb only a metered
910 /// build answers, so a deployment that sells can prove the box it sells
911 /// against can enforce what it sold.
912 ///
913 /// # Why this is not covered by anything else
914 ///
915 /// Everything the poller does — `list_tenants`, `read_usage` — is an OPEN
916 /// build's service. gunnar's community ISO answers both perfectly, and
917 /// `poll tenants=11 failures=0` therefore proves connectivity and nothing
918 /// whatever about entitlements. The first thing that finds out is a paying
919 /// customer: the sink creates the principal, pushes the entitlement,
920 /// `Entitlement.Set` answers `UNIMPLEMENTED`, and the signup is refused
921 /// with the money already taken (`monetize_server::signup::Owed`). The last
922 /// proof this estate's appliance is a metered build is a journal line from
923 /// 2026-09-15.
924 ///
925 /// So: a READ-ONLY metered verb, asked on a schedule. It must not create,
926 /// change or delete anything — it is called on every poll tick, against a
927 /// live box, forever.
928 ///
929 /// # The default is UNPROVEN, and unproven is not "yes"
930 ///
931 /// Unlike [`Product::can_absorb`] and [`Product::servable`], which have no
932 /// default because every constant they could return is a lie about
933 /// capacity, this one has a safe answer that is not a claim:
934 /// [`MeteredDoor::Unproven`], naming the plugin that owes the work. A
935 /// default of `Enforcing` would let every plugin that never thought about
936 /// it assert the one thing this exists to check.
937 fn metered_door(&self) -> MeteredDoor {
938 MeteredDoor::Unproven(format!(
939 "the {} plugin has no metered-door probe, so nothing has asked this box whether it can enforce an entitlement at all",
940 self.id()
941 ))
942 }
943
944 /// **Delete everything this tenant has in the product, and keep them locked
945 /// out while it happens.**
946 ///
947 /// The product half of a super purge. `monetize` can give the IRON back on
948 /// its own — `transaction::release_tenant` and
949 /// `CloudProvider::release_for` — and it cannot delete a byte of what is on
950 /// it, because only the product knows what a tenant's data IS. In gunnar it
951 /// is every repository in the account plus two classes of bytes that are
952 /// not in the catalog at all; nothing on this side of the seam could
953 /// enumerate that.
954 ///
955 /// # It must LOCK, and the lock is the product's
956 ///
957 /// A purge is not atomic — it walks and reclaims over minutes while the
958 /// product goes on serving — so anything the tenant does during the walk
959 /// lands behind it. The lockout that prevents that can only live where the
960 /// requests arrive, which is the product. gunnar's is `Accounts.Gate`, and
961 /// it is total rather than read-only because in gunnar a read creates
962 /// things: a fetch explodes objects into a cache the purge just unlinked.
963 ///
964 /// [`Purged::still_locked`] carries the outcome back, because a product
965 /// that emptied a tenant and could not let them back in has left an account
966 /// nobody can use, and that must not read as success anywhere above here.
967 ///
968 /// # The default REFUSES BY NAME
969 ///
970 /// Unlike [`Product::can_absorb`] and [`Product::servable`], which have no
971 /// default at all because every constant they could return is a lie about
972 /// capacity, this one's dangerous answer is a permissive `Ok` — a product
973 /// that never implemented it reporting a tenant's data gone when it is
974 /// still there, after which a super purge would cheerfully destroy the
975 /// disks it is on. A refusal naming the product is the safe wrong answer
976 /// and it says which product owes the work.
977 /// **"Add users", asked before money moves** (owner, 2026-10-04): would
978 /// `ask` fit in the room the tenant (an account) holds and has neither
979 /// stored nor promised, and if not, how much is missing? The product
980 /// answers from its own books — it knows what is stored and what it
981 /// promised its users; monetize never measures — and writes nothing. A
982 /// name the product would refuse (one that exists) is refused here too,
983 /// so no money moves for a user that could never be created.
984 ///
985 /// The default refuses by name: a product with no users to add has no
986 /// "yes" to give.
987 fn users_fit(&self, tenant: &TenantId, ask: &UsersAsk) -> Result<UsersAnswer, ProductError> {
988 let _ = (tenant, ask);
989 Err(ProductError::Refused(format!("the {} plugin cannot add users to a tenant", self.id())))
990 }
991
992 /// **"Add users", granted**: ONE named user, or `ask.count` unnamed seats,
993 /// each holding `ask.room_bytes` of the tenant's room. `order` is the
994 /// reference the grant belongs to, so a retry answers the first grant
995 /// (`UsersAnswer::unchanged`) instead of adding twice. Called when the
996 /// room is there — at once from unused room, or once an order bought the
997 /// rest — and the product checks the room again in the write.
998 fn add_users(&self, tenant: &TenantId, ask: &UsersAsk, order: &str) -> Result<UsersAnswer, ProductError> {
999 let _ = (tenant, ask, order);
1000 Err(ProductError::Refused(format!("the {} plugin cannot add users to a tenant", self.id())))
1001 }
1002
1003 fn purge_tenant(&self, tenant: &TenantId, reason: &str) -> Result<Purged, ProductError> {
1004 let _ = (tenant, reason);
1005 Err(ProductError::Refused(format!(
1006 "the {} plugin cannot purge a tenant's data: it has no purge verb, so nothing here \
1007 can promise the tenant's bytes are gone. Empty the tenant in the product itself \
1008 before releasing its resources.",
1009 self.id()
1010 )))
1011 }
1012
1013 /// ★ UW-4: **Is room sold past the set QUEUED for a resize the product's own estate does**
1014 /// (the pull channel: monetize grows nothing it cannot reach)? `false` — the default —
1015 /// leaves the answer to the grow lane ([`Product::grow`]).
1016 fn queues_room(&self) -> bool {
1017 false
1018 }
1019
1020 /// ★ UW-4: **Tell the estate serving `tenant` it was sold `need_bytes` past its room**, to
1021 /// be resized later. Only asked of a product that [`Product::queues_room`]. The default
1022 /// refuses by name.
1023 fn queue_room(&self, tenant: &TenantId, need_bytes: u64, why: &str) -> Result<(), ProductError> {
1024 let _ = (tenant, need_bytes, why);
1025 Err(ProductError::Refused(format!("the {} plugin queues no room", self.id())))
1026 }
1027
1028 /// **The product's own half of a data-set growth**, if it has one. `None`
1029 /// — the default — is a product whose set cannot be grown at runtime from
1030 /// outside; monetize then refuses a growth by name before touching a
1031 /// volume. See [`ApplianceGrow`].
1032 fn grow(&self) -> Option<&dyn ApplianceGrow> {
1033 None
1034 }
1035
1036 /// **The TWIN's own half of ITS data-set growth**, if the product has a twin whose
1037 /// control plane monetize can reach. Owner ruling 2026-09-22 (T21): the twin, built
1038 /// from the same medium, grows EXACTLY like main — the same [`ApplianceGrow`] verbs on
1039 /// the twin's own control plane (drain, unmount, wait, grow, mount; `maintenance`
1040 /// meanwhile) around the cloud's same `grow_set`, and it is NEVER stopped or
1041 /// restarted. `None` — the default — is a product whose twin cannot be grown that
1042 /// way; its twin-growth lane then holds by name rather than stop the box.
1043 fn twin_grow(&self) -> Option<&dyn ApplianceGrow> {
1044 None
1045 }
1046
1047 /// **The product's estate guardian** (5.4): how its estates are diagnosed and repaired,
1048 /// as plans of cloud steps and signed orders the estate pulls. `None` — the default — is
1049 /// a product whose estates monetize does not repair. See [`guardian::EstateGuardian`].
1050 fn guardian(&self) -> Option<&dyn guardian::EstateGuardian> {
1051 None
1052 }
1053
1054 /// **The product's twins, as its primary hears them** — one row per twin:
1055 /// the heartbeat state, and the fill of the twin's data volume the twin
1056 /// reported on its last poll. What the batcher's twin-fill trigger reads.
1057 /// The default is no twins: a product without a twin has nothing to grow.
1058 fn twins(&self) -> Result<Vec<TwinFill>, ProductError> {
1059 Ok(Vec::new())
1060 }
1061
1062 /// **Whether a tenant-presented actor ticket can be verified at all by this
1063 /// product.** False means a console must not offer self-service renewal —
1064 /// there is nothing here that could tell a customer from a stranger, so the
1065 /// route must not exist rather than existing and refusing.
1066 ///
1067 /// Reported on the wire as `ProductInfo.tenant_renewal`.
1068 fn tenant_renewal(&self) -> bool {
1069 false
1070 }
1071
1072 /// **The test buttons this product offers its operator** ([`TestAction`]).
1073 /// Empty (the default): the console shows none for it.
1074 fn test_actions(&self) -> Vec<TestAction> {
1075 Vec::new()
1076 }
1077
1078 /// **May the bearer of this ticket act for `tenant`? Default DENY.**
1079 ///
1080 /// The ticket is the product's own appliance vouching that the human
1081 /// driving a console holds the tenant they are paying for
1082 /// (`monetize_embed::ticket`). monetize's gRPC surface authenticates one
1083 /// shared bearer — an empty one means open — so this is the ONLY thing that
1084 /// can tell "alice renewing alice" from "somebody renewing alice".
1085 ///
1086 /// # Why the argument is raw bytes
1087 ///
1088 /// `monetize-embed` — which owns `ActorTicket` and `verify_ticket` —
1089 /// depends on THIS crate, so this crate cannot name that type without a
1090 /// dependency cycle. The verification therefore happens in the PLUGIN,
1091 /// which may depend on `monetize-embed`, and the seam carries the opaque
1092 /// bytes it was handed. That is not a compromise: the plugin is also the
1093 /// only half that holds the appliance's key, so it is where the check
1094 /// belongs whichever way the crates pointed.
1095 ///
1096 /// # Why the default refuses
1097 ///
1098 /// A plugin is out of tree and nobody edits it when a path like this ships.
1099 /// A default of `Allowed` would silently turn every such plugin into one
1100 /// that honours signed bytes it holds no key for; a default that REFUSES
1101 /// makes an unaudited product safe by omission rather than by somebody
1102 /// remembering. The cost of the wrong default here is a customer who cannot
1103 /// renew themselves; the cost of the other one is a stranger who can.
1104 /// `purpose` and `caps` are what the REQUEST says, and they are arguments
1105 /// rather than things the plugin reads off the ticket, because a verifier
1106 /// cannot compare a claim against itself. A ticket names the verb it
1107 /// authorises and — for an order — the amount it covers; the plugin's job
1108 /// is to check both against the call that arrived. Passing them in is what
1109 /// makes "this ticket is for a different order" a refusal that no
1110 /// implementation can forget to make.
1111 ///
1112 /// `caps` is empty for a renewal, and a plugin must refuse a renewal ticket
1113 /// that carries any.
1114 /// **The key the product's own estate DELIVERED** for its actor tickets (its owner's
1115 /// install notice, `ticket_key`; ST-3): monetize hands it to the product whose HOME estate
1116 /// delivered it, so no operator copies it into `[products.<id>] appliance_key`. A key the
1117 /// operator configured wins. The default takes none, by name.
1118 fn deliver_appliance_key(&self, hex: &str) -> Result<String, String> {
1119 let _ = hex;
1120 Err(format!("{} takes no delivered appliance key", self.id()))
1121 }
1122
1123 fn may_act_for(
1124 &self,
1125 _tenant: &TenantId,
1126 _purpose: &str,
1127 _caps: &std::collections::BTreeMap<String, u64>,
1128 _ticket: &[u8],
1129 ) -> Result<ActorVerdict, ProductError> {
1130 Ok(ActorVerdict::Refused("this product cannot verify a tenant-presented ticket".into()))
1131 }
1132}
1133
1134/// **A plugin's product served under another id** (G15): `[products.gunnar-test2]
1135/// plugin = "gunnar"` is the gunnar plugin, answering `gunnar-test2`. Everything
1136/// but [`Product::id`] is the plugin's own; the id is the BLOCK's, which is what
1137/// the ledger, the prices, the vendor route and the signing key key on — so two
1138/// estates of one product never share one id.
1139pub struct ServedAs {
1140 id: &'static str,
1141 inner: Box<dyn Product>,
1142}
1143
1144impl ServedAs {
1145 pub fn new(id: &'static str, inner: Box<dyn Product>) -> Self {
1146 Self { id, inner }
1147 }
1148 /// The id the plugin itself answers.
1149 pub fn plugin_id(&self) -> &'static str {
1150 self.inner.id()
1151 }
1152}
1153
1154impl Product for ServedAs {
1155 fn id(&self) -> &'static str {
1156 self.id
1157 }
1158 fn meters(&self) -> &[Meter] {
1159 self.inner.meters()
1160 }
1161 fn list_tenants(&self) -> Result<Vec<TenantId>, ProductError> {
1162 self.inner.list_tenants()
1163 }
1164 fn read_usage(&self, tenant: &TenantId) -> Result<Usage, ProductError> {
1165 self.inner.read_usage(tenant)
1166 }
1167 fn read_usage_page(&self, req: &UsagePageReq) -> Result<Option<UsagePage>, ProductError> {
1168 self.inner.read_usage_page(req)
1169 }
1170 fn tenant_owners(&self) -> Result<Option<Vec<TenantOwner>>, ProductError> {
1171 self.inner.tenant_owners()
1172 }
1173 fn read_activity(&self, tenant: &TenantId, since_unix_ms: u64, until_unix_ms: u64) -> Result<Activity, ProductError> {
1174 self.inner.read_activity(tenant, since_unix_ms, until_unix_ms)
1175 }
1176 fn push_entitlement(&self, fact: &EntitlementFact) -> Result<(), ProductError> {
1177 self.inner.push_entitlement(fact)
1178 }
1179 fn can_absorb(&self, tenant: &TenantId, caps: &BTreeMap<String, u64>) -> Result<Absorb, String> {
1180 self.inner.can_absorb(tenant, caps)
1181 }
1182 fn servable(&self) -> Servable {
1183 self.inner.servable()
1184 }
1185 fn servable_on(&self, estate: &str) -> Servable {
1186 self.inner.servable_on(estate)
1187 }
1188 fn metered_door(&self) -> MeteredDoor {
1189 self.inner.metered_door()
1190 }
1191 fn users_fit(&self, tenant: &TenantId, ask: &UsersAsk) -> Result<UsersAnswer, ProductError> {
1192 self.inner.users_fit(tenant, ask)
1193 }
1194 fn add_users(&self, tenant: &TenantId, ask: &UsersAsk, order: &str) -> Result<UsersAnswer, ProductError> {
1195 self.inner.add_users(tenant, ask, order)
1196 }
1197 fn purge_tenant(&self, tenant: &TenantId, reason: &str) -> Result<Purged, ProductError> {
1198 self.inner.purge_tenant(tenant, reason)
1199 }
1200 fn grow(&self) -> Option<&dyn ApplianceGrow> {
1201 self.inner.grow()
1202 }
1203 fn twin_grow(&self) -> Option<&dyn ApplianceGrow> {
1204 self.inner.twin_grow()
1205 }
1206 fn queues_room(&self) -> bool {
1207 self.inner.queues_room()
1208 }
1209 fn queue_room(&self, tenant: &TenantId, need_bytes: u64, why: &str) -> Result<(), ProductError> {
1210 self.inner.queue_room(tenant, need_bytes, why)
1211 }
1212 fn guardian(&self) -> Option<&dyn guardian::EstateGuardian> {
1213 self.inner.guardian()
1214 }
1215 fn twins(&self) -> Result<Vec<TwinFill>, ProductError> {
1216 self.inner.twins()
1217 }
1218 fn tenant_renewal(&self) -> bool {
1219 self.inner.tenant_renewal()
1220 }
1221 fn deliver_appliance_key(&self, hex: &str) -> Result<String, String> {
1222 self.inner.deliver_appliance_key(hex)
1223 }
1224 fn may_act_for(&self, tenant: &TenantId, purpose: &str, caps: &BTreeMap<String, u64>, ticket: &[u8]) -> Result<ActorVerdict, ProductError> {
1225 self.inner.may_act_for(tenant, purpose, caps, ticket)
1226 }
1227 fn test_actions(&self) -> Vec<TestAction> {
1228 self.inner.test_actions()
1229 }
1230}
1231
1232/// **What a product says about a ticket somebody presented.**
1233///
1234/// Not a `bool` and not a `Result<(), ProductError>`: a refusal is a normal,
1235/// expected answer that carries a SENTENCE — the words a caller may show — and
1236/// a `ProductError` means something else entirely (the product's control plane
1237/// is down, the question could not be asked). Conflating the two would let an
1238/// unreachable appliance read as a refused ticket, or worse, the reverse.
1239#[derive(Clone, PartialEq, Eq, Debug)]
1240pub enum ActorVerdict {
1241 /// The ticket verifies, names this tenant, this product and this verb, is
1242 /// inside its life, and has not been spent before.
1243 Allowed,
1244 /// Refused, and why — in words a customer surface may show. A verifier must
1245 /// not name the ticket's own tenant back to whoever asked about another
1246 /// one; see `monetize_embed::verify_ticket`.
1247 Refused(String),
1248}
1249
1250/// One twin as its primary last heard from it (gunnar's `TwinHeartbeat`).
1251#[derive(Clone, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
1252pub struct TwinFill {
1253 pub principal: String,
1254 pub address: String,
1255 /// `ok` | `behind` | `silent`.
1256 pub state: String,
1257 /// Wall clock of the twin's last poll; 0 = never heard.
1258 pub last_seen_unix_ms: i64,
1259 pub lag_entries: u64,
1260 pub disk_total_bytes: u64,
1261 pub disk_used_bytes: u64,
1262 /// used × 1000 / total; 0 when total is unknown.
1263 pub fill_permille: u32,
1264}
1265
1266impl TwinFill {
1267 /// The primary can still hear it: `ok` or `behind`, never `silent` or a word we do not know.
1268 pub fn visible(&self) -> bool {
1269 matches!(self.state.as_str(), "ok" | "behind")
1270 }
1271}
1272
1273// ── the appliance's half of a data-set growth ───────────────────────────────
1274
1275/// **What the appliance does INSIDE the box while the outside actor grows its
1276/// volumes** (`DATA-SET-GROWTH-FLOW.md` §0 ruling 1, T6): drain, unmount, wait
1277/// for the members to come back larger, run its engine, mount, reopen. monetize
1278/// drives it — flush → start → wait `unmounted` → cloud steps → resume → wait
1279/// `done|failed` — and every verb here is one call on the product's control
1280/// plane. Vendor-neutral: the words are the phases the flow document names,
1281/// and the go-ahead is opaque bytes the product verifies on its own terms.
1282pub trait ApplianceGrow: Send + Sync {
1283 /// **Prove the twin is caught up** (T11, step 0) — flush replication to
1284 /// every standby, bounded by `timeout_secs` (0 = the product's own ceiling),
1285 /// and report. Changes no role and no roster.
1286 fn flush(&self, timeout_secs: u32) -> Result<FlushReport, ProductError>;
1287 /// Begin the growth: drain, unmount, wait for the members. `go_ahead` is
1288 /// the signed approval (see `monetize_embed::signing::go_ahead_message`);
1289 /// `target_sectors_per_member` is in 512-byte sectors, 0 = "whatever every
1290 /// member comes back larger at". Answers the status after the call.
1291 fn start(&self, target_sectors_per_member: u64, go_ahead: &[u8]) -> Result<GrowStatus, ProductError>;
1292 /// The phase and the members, as the product sees them. **Must answer
1293 /// while the set is unmounted and, where the product offers it, without a
1294 /// credential** — the outside actor decides whether to re-attach from it.
1295 fn status(&self) -> Result<GrowStatus, ProductError>;
1296 /// "Look again": the members are back. With `give_up`, stop waiting and
1297 /// serve whatever is there at whatever size it is.
1298 fn resume(&self, give_up: bool) -> Result<GrowStatus, ProductError>;
1299}
1300
1301/// The phases a product reports, as `DATA-SET-GROWTH-FLOW.md` names them.
1302/// Kept as words on the wire (`GrowStatus::phase`); this is the reader.
1303#[derive(Clone, Copy, PartialEq, Eq, Debug)]
1304pub enum GrowPhase {
1305 Idle,
1306 Draining,
1307 Unmounted,
1308 WaitingMembers,
1309 Growing,
1310 Mounting,
1311 Done,
1312 Failed,
1313 /// A word this monetize does not know — a newer product. Shown, never
1314 /// acted on.
1315 Other,
1316}
1317
1318impl GrowPhase {
1319 pub fn parse(word: &str) -> GrowPhase {
1320 match word {
1321 "idle" => GrowPhase::Idle,
1322 "draining" => GrowPhase::Draining,
1323 "unmounted" => GrowPhase::Unmounted,
1324 "waiting-members" => GrowPhase::WaitingMembers,
1325 "growing" => GrowPhase::Growing,
1326 "mounting" => GrowPhase::Mounting,
1327 "done" => GrowPhase::Done,
1328 "failed" => GrowPhase::Failed,
1329 _ => GrowPhase::Other,
1330 }
1331 }
1332 /// The set is off the stripe: the outside actor may touch the volumes.
1333 pub fn volumes_free(self) -> bool {
1334 matches!(self, GrowPhase::Unmounted | GrowPhase::WaitingMembers)
1335 }
1336 pub fn is_terminal(self) -> bool {
1337 matches!(self, GrowPhase::Done | GrowPhase::Failed | GrowPhase::Idle)
1338 }
1339}
1340
1341/// One member of the set as the product last surveyed it.
1342#[derive(Clone, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
1343pub struct GrowMember {
1344 pub index: u32,
1345 pub device: String,
1346 pub disk_bytes: u64,
1347 pub set_bytes: u64,
1348 pub larger: bool,
1349}
1350
1351/// The product's answer to [`ApplianceGrow::status`].
1352#[derive(Clone, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
1353pub struct GrowStatus {
1354 /// `idle` | `draining` | `unmounted` | `waiting-members` | `growing` |
1355 /// `mounting` | `done` | `failed`. Read with [`GrowPhase::parse`].
1356 pub phase: String,
1357 /// `failed`'s reason; empty otherwise.
1358 pub why: String,
1359 pub epoch: u64,
1360 pub target_sectors_per_member: u64,
1361 pub members: Vec<GrowMember>,
1362 pub members_verdict: String,
1363 pub engine_present: bool,
1364 pub serving: bool,
1365 pub in_flight: u64,
1366 pub since_unix_ms: i64,
1367 pub detail: String,
1368 pub go_ahead_signer: String,
1369 pub set_uuid: String,
1370 pub total_bytes: u64,
1371}
1372
1373impl GrowStatus {
1374 pub fn phase(&self) -> GrowPhase {
1375 GrowPhase::parse(&self.phase)
1376 }
1377}
1378
1379/// The product's answer to [`ApplianceGrow::flush`]: is the twin caught up,
1380/// and what does that prove.
1381#[derive(Clone, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
1382pub struct FlushReport {
1383 pub caught_up: bool,
1384 pub in_flight: u64,
1385 pub pending: u64,
1386 pub bus_gaps: u64,
1387 pub needs_full_resync: bool,
1388 pub last_success_unix_ms: i64,
1389 pub flushed_at_unix_ms: i64,
1390 pub waited_ms: u64,
1391 pub catalog_repos: u64,
1392 pub standbys: u64,
1393 pub verdict: String,
1394}
1395
1396/// **"Add users"** (owner, 2026-10-04): `count` users, each holding
1397/// `room_bytes` of the tenant's room. `count` 1 is ONE named user (`name`);
1398/// more is that many SEATS with no names, filled later (an invite, an
1399/// import). `room_bytes` 0 is the product's own default. The rules a person
1400/// types against (the range, the names, the room's floor) are the PRODUCT's,
1401/// which every door shares.
1402#[derive(Clone, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
1403#[serde(default)]
1404pub struct UsersAsk {
1405 pub count: u32,
1406 pub room_bytes: u64,
1407 #[serde(skip_serializing_if = "String::is_empty")]
1408 pub name: String,
1409 /// Where the named user's invite is mailed; empty: nowhere. Not part of
1410 /// the ask's identity ([`UsersAsk::fingerprint`]).
1411 #[serde(skip_serializing_if = "String::is_empty")]
1412 pub email: String,
1413 /// The console the invite is redeemed at.
1414 #[serde(skip_serializing_if = "String::is_empty")]
1415 pub invite_origin: String,
1416 /// **The named user as the ledger knows them** (G13, 5.4): an opaque id
1417 /// (`monetize::ledger::principal_id`), never the name. Empty for seats.
1418 #[serde(skip_serializing_if = "String::is_empty")]
1419 pub principal_id: String,
1420}
1421
1422impl UsersAsk {
1423 /// **What a ticket for this ask covers** — the "caps" an `order` actor
1424 /// ticket is minted over and compared against, so a ticket vouches for
1425 /// exactly this number of users at exactly this room, and no other.
1426 pub fn ticket_caps(&self) -> BTreeMap<String, u64> {
1427 [("users".to_owned(), u64::from(self.count.max(1))), ("user_room_bytes".to_owned(), self.room_bytes)].into_iter().collect()
1428 }
1429
1430 /// A short, stable fingerprint for a reference: `2x10737418240` or
1431 /// `1x10737418240:p-<id>` — the named user by [`UsersAsk::principal_id`],
1432 /// never by name (G13).
1433 pub fn fingerprint(&self) -> String {
1434 match self.principal_id.as_str() {
1435 "" => format!("{}x{}", self.count.max(1), self.room_bytes),
1436 id => format!("{}x{}:{id}", self.count.max(1), self.room_bytes),
1437 }
1438 }
1439
1440 /// **The ask as the ledger may hold it**: no name, no address — the
1441 /// named user only by [`UsersAsk::principal_id`].
1442 pub fn without_names(&self) -> UsersAsk {
1443 UsersAsk { name: String::new(), email: String::new(), ..self.clone() }
1444 }
1445
1446 /// One named user (the name, or its id, is known).
1447 pub fn is_named(&self) -> bool {
1448 !self.name.is_empty() || !self.principal_id.is_empty()
1449 }
1450}
1451
1452/// The product's answer to [`Product::users_fit`] / [`Product::add_users`].
1453#[derive(Clone, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
1454#[serde(default)]
1455pub struct UsersAnswer {
1456 /// The named user, once created; empty for seats and for a dry run.
1457 pub principal: String,
1458 /// Bytes each user holds, as the product resolved it (its default applied).
1459 pub room_bytes: u64,
1460 /// What the ask needs of the tenant's room: count × room.
1461 pub need_bytes: u64,
1462 /// The tenant's room neither stored nor promised, before the ask. `None`:
1463 /// nothing caps the tenant there.
1464 pub unused_bytes: Option<u64>,
1465 /// How much is missing; 0 = it fits.
1466 pub shortfall_bytes: u64,
1467 /// The tenant's seats after this: every seat, and the open ones.
1468 pub seats: u32,
1469 pub seats_open: u32,
1470 /// Nothing was written: a dry run, or the retry of a granted order.
1471 pub unchanged: bool,
1472 /// The new named user's invite secret, on the answer that created the
1473 /// user (and never again): what a `<console>/login?invite=` link carries.
1474 #[serde(skip_serializing_if = "String::is_empty")]
1475 pub invite_secret: String,
1476}
1477
1478/// What [`Product::purge_tenant`] did.
1479#[derive(Clone, PartialEq, Eq, Debug)]
1480pub struct Purged {
1481 pub tenant: TenantId,
1482 /// What the product removed, in its own words and its own units — "5 of 5
1483 /// stores, 1.2 GiB". Free text because every product counts different
1484 /// things and a shared schema would force each of them to lie a little.
1485 pub detail: String,
1486 /// Bytes the product says it reclaimed. `None` means it does not count
1487 /// them, which is not the same as zero and must not be rendered as it.
1488 pub bytes_reclaimed: Option<u64>,
1489 /// **Is the tenant still locked out of the product?**
1490 ///
1491 /// `true` is a real and expected outcome — an operator may have suspended
1492 /// the account separately, and the purge correctly refuses to lift a
1493 /// lockout it did not take — but it is also what a purge that could not
1494 /// unlock reports, and either way the tenant cannot use what is left.
1495 /// Carried so a super purge can say so rather than infer it.
1496 pub still_locked: bool,
1497 /// One line per thing the product could not remove. Non-empty means the
1498 /// tenant's data is PARTIALLY there, and the caller must not go on to
1499 /// destroy the iron it is sitting on.
1500 pub failures: Vec<String>,
1501}
1502
1503/// **Bytes in the largest unit that still leaves a digit before the decimal
1504/// point**, by integer arithmetic: `10.0 GiB`, `64.0 MiB`, `999 B`.
1505///
1506/// One writer, because there used to be three and every one of them carried the
1507/// same bug. Each divided by 1 GiB unconditionally and printed one decimal, so
1508/// anything under a gibibyte rendered `0.0 GiB` — and `0.0` beside a quantity is
1509/// read as *there is none*:
1510///
1511/// * the console (then a Plans page) showed a real 64 MiB
1512/// `pack_bytes` cap as `0.0 GiB`, which an operator reads as "no quota";
1513/// * the old catalogue validator said *"sells pack_bytes = 10.0 GiB but
1514/// its resources buy only 10.0 GiB — short 0.0 GiB"*, a sentence in which
1515/// every number is wrong in the direction of "nothing is the matter";
1516/// * `products/gunnar`'s capacity refusals said "short 0.0 GiB" for the same
1517/// reason.
1518///
1519/// It lives here because this is the crate that already owns the meter
1520/// vocabulary ([`Meter`], [`Backing::DiskBytes`]) and the only one every side
1521/// can depend on: core, the product plugins, and the browser console alike. It
1522/// pulls in nothing (this crate is `serde` and nothing else), so the wasm
1523/// bundle pays a few hundred bytes for a formatter it was carrying anyway.
1524///
1525/// Truncates, never rounds up: a reading must not appear to cross a cap it has
1526/// not crossed.
1527pub fn bytes(n: u64) -> String {
1528 const KIB: u64 = 1 << 10;
1529 const MIB: u64 = 1 << 20;
1530 const GIB: u64 = 1 << 30;
1531 const TIB: u64 = 1 << 40;
1532 let (unit, per) = match n {
1533 n if n >= TIB => ("TiB", TIB),
1534 n if n >= GIB => ("GiB", GIB),
1535 n if n >= MIB => ("MiB", MIB),
1536 n if n >= KIB => ("KiB", KIB),
1537 // Under a kibibyte there is nothing to scale to, and `0.0 KiB` would be
1538 // the same lie one unit down. Bytes are exact and short.
1539 n => return format!("{n} B"),
1540 };
1541 format!("{}.{} {unit}", n / per, ((n % per) * 10) / per)
1542}
1543
1544#[cfg(test)]
1545mod backing_tests {
1546 use super::Backing;
1547
1548 /// **A byte over the GiB is sold the next GiB, and a millicore is priced
1549 /// as a thousandth of a core.** Distinct non-zero numbers: 2200 öre per
1550 /// GiB-month, 90 GiB + 1 byte, 1500 millicores at 30 000.
1551 #[test]
1552 fn prices_round_bytes_up_to_the_unit_and_cpu_by_the_millicore() {
1553 const GIB: u64 = 1 << 30;
1554 assert_eq!(Backing::DiskBytes.price_month(90 * GIB, 2200), 198_000, "90 GiB × 22.00 SEK");
1555 assert_eq!(Backing::DiskBytes.price_month(90 * GIB + 1, 2200), 200_200, "one byte over is the 91st GiB");
1556 assert_eq!(Backing::DiskBytes.price_month(0, 2200), 0);
1557 assert_eq!(Backing::RamBytes.price_month(3 * GIB, 700), 2100);
1558 assert_eq!(Backing::CpuMillicores.price_month(1500, 30_000), 45_000, "1.5 cores at 300.00");
1559 assert_eq!(Backing::CpuMillicores.price_month(1, 30_000), 30, "one millicore is not free and not a core");
1560 // A counted unit is flat: five properties at 200.00 SEK-month are 1000.00,
1561 // and one is 200.00 — no rounding, no thousandths. This is the arm a per-property
1562 // subscription rides on; before it existed a property priced to zero.
1563 assert_eq!(Backing::Units.price_month(5, 20_000), 100_000, "5 × 200.00");
1564 assert_eq!(Backing::Units.price_month(1, 20_000), 20_000);
1565 assert_eq!(Backing::Units.price_month(0, 20_000), 0);
1566 assert_eq!(Backing::Units.unit(), "unit_month");
1567 // A zero unit price is FREE, whatever the delta: the open-source list.
1568 for b in Backing::ALL {
1569 assert_eq!(b.price_month(u64::MAX / 4, 0), 0, "{b:?}");
1570 }
1571 // The unit names are the price list's keys, both ways.
1572 for b in Backing::ALL {
1573 assert_eq!(Backing::from_unit(b.unit()), Some(b));
1574 }
1575 assert_eq!(Backing::from_unit("moon_month"), None);
1576 }
1577}
1578
1579#[cfg(test)]
1580mod bytes_tests {
1581 use super::bytes;
1582
1583 /// **A quantity that exists never renders as zero.** RED before this
1584 /// function existed: `0.0 GiB` for every value under 2^30, in three separate
1585 /// copies of the same six lines.
1586 #[test]
1587 fn only_a_genuine_zero_reads_as_zero() {
1588 for n in [1u64, 512, 1 << 20, 67_108_864, (1 << 30) - 1] {
1589 let s = bytes(n);
1590 assert!(!s.starts_with("0.0 ") && !s.starts_with("0 "), "{n} bytes rendered as {s:?}, which reads as nothing");
1591 }
1592 assert_eq!(bytes(0), "0 B");
1593 }
1594
1595 #[test]
1596 fn the_unit_scales_and_the_value_truncates() {
1597 assert_eq!(bytes(999), "999 B");
1598 assert_eq!(bytes(1536), "1.5 KiB");
1599 assert_eq!(bytes(67_108_864), "64.0 MiB");
1600 assert_eq!(bytes((1 << 30) - 1), "1023.9 MiB", "truncates, never rounds up past the cap");
1601 assert_eq!(bytes(10 << 30), "10.0 GiB");
1602 assert_eq!(bytes(3 << 40), "3.0 TiB");
1603 assert_eq!(bytes(u64::MAX), "16777215.9 TiB", "no overflow at the top of the range");
1604 }
1605}
1606
1607#[cfg(test)]
1608mod actor_tests {
1609 use super::*;
1610
1611 /// A plugin written before the tenant path existed: it implements every
1612 /// method the trait REQUIRES and knows nothing about tickets.
1613 struct OldPlugin;
1614
1615 impl Product for OldPlugin {
1616 fn id(&self) -> &'static str {
1617 "old"
1618 }
1619 fn meters(&self) -> &[Meter] {
1620 &[]
1621 }
1622 fn list_tenants(&self) -> Result<Vec<TenantId>, ProductError> {
1623 Ok(Vec::new())
1624 }
1625 fn read_usage(&self, _tenant: &TenantId) -> Result<Usage, ProductError> {
1626 Err(ProductError::Refused("no".into()))
1627 }
1628 fn push_entitlement(&self, _fact: &EntitlementFact) -> Result<(), ProductError> {
1629 Ok(())
1630 }
1631 fn can_absorb(&self, _tenant: &TenantId, _caps: &BTreeMap<String, u64>) -> Result<Absorb, String> {
1632 Ok(Absorb::OnHand)
1633 }
1634 fn servable(&self) -> Servable {
1635 Servable::Unmeasured("a test double measures nothing".to_owned())
1636 }
1637 }
1638
1639 /// ★ **Safe by OMISSION, not by remembering.**
1640 ///
1641 /// The out-of-tree plugin nobody will edit when this path ships must not
1642 /// start honouring signed bytes it has no key for. Both halves of the
1643 /// default say no: it does not advertise the capability, and it refuses
1644 /// every ticket rather than falling through to an allow.
1645 #[test]
1646 fn a_product_that_never_heard_of_a_ticket_refuses_every_one() {
1647 let p = OldPlugin;
1648 assert!(!p.tenant_renewal(), "a console must not be told to offer self-service here");
1649 let verdict = // The literal and not a constant: this crate cannot depend on
1650 // `monetize-embed` (that would be a cycle), which is exactly why the
1651 // trait takes the purpose as a `&str`. The default refuses whatever it
1652 // is handed, so the word decides nothing here.
1653 p.may_act_for(&TenantId("alice".into()), "renew", &Default::default(), b"anything at all").expect("the default answers rather than erroring");
1654 let ActorVerdict::Refused(why) = verdict else { panic!("the default must DENY") };
1655 assert!(!why.trim().is_empty(), "a refusal says why");
1656 }
1657}
1658
1659#[cfg(test)]
1660mod estate_tenant_tests {
1661 use super::*;
1662
1663 /// ★ G16: a sold estate's tenant is `@<estate>/<namespace>` on the ONE ledger and reads
1664 /// back whole (a namespace keeps its own slashes); a tenant of the product's own estate,
1665 /// nested or not, never reads as one, and a half-mark is no estate.
1666 #[test]
1667 fn a_sold_estates_tenant_carries_the_estate_in_its_path_and_no_own_tenant_reads_as_one() {
1668 let t = TenantId::on_estate("acme", "bob");
1669 assert_eq!(t.0, "@acme/bob");
1670 assert_eq!(t.estate(), Some(("acme", "bob")));
1671 assert_eq!(TenantId::on_estate("acme", "team/sub").estate(), Some(("acme", "team/sub")));
1672 for own in ["bob", "team/sub", "acme/bob", "", "@acme", "@/bob", "@acme/", "x@acme/bob"] {
1673 assert_eq!(TenantId(own.into()).estate(), None, "{own:?}");
1674 }
1675 }
1676}