monetize_product/pull.rs
1//! **What an estate's box tells monetize about its product** (5.4, G1 pull).
2//!
3//! Monetize never calls INTO an estate. Every read the server loops used to dial the
4//! product for — the tenant list and who each tenant is, each tenant's usage, how much
5//! the box can still serve, whether its paid door is there, how much room a raise would
6//! find — arrives instead in the box's own `Fleet.Report` (`monetize_embed::fleet::Report`,
7//! field `product`, this struct as JSON). Everything that CHANGES the estate (an
8//! entitlement, a new principal) leaves monetize as a signed order the box pulls
9//! ([`order`]).
10//!
11//! The product plugin keeps every verb that needs no box (prices, meters, tickets); the
12//! pull wrapper in `monetize-guardian` (`pulled::Pulled`) answers the rest from here.
13
14use std::collections::BTreeMap;
15
16use crate::{MeteredDoor, Servable, TenantOwner, Usage};
17
18/// The order kinds a product's box runs beside the repair verbs. The body is JSON.
19pub mod order {
20 /// Body: the signed [`crate::EntitlementFact`], exactly as `Entitlement.Set` took it.
21 /// The box verifies the fact's own signature under `monetize_pubkey` as before; newest
22 /// per tenant wins, so an unacked older fact for the same tenant is dropped when a new
23 /// one is queued.
24 pub const ENTITLEMENT: &str = "entitlement";
25 /// Body: [`super::AddPrincipal`]. The box creates the account (gunnar:
26 /// `Access.AddPrincipal`) and acks:
27 ///
28 /// * `ok`, `detail` = the namespace it made (empty = the username) — also when the
29 /// principal is already there and was made for this same `signup` (idempotent: monetize
30 /// asks again when it lost the ack);
31 /// * not `ok`, `detail` starting with [`TAKEN`] — the name is somebody else's (the buyer
32 /// picks another, `Signup.Rename`);
33 /// * not `ok`, anything else — refused, final; the detail is the operator's words.
34 ///
35 /// Monetize queues the signup's entitlement only after an `ok` ack, so a fact never lands
36 /// on a name that turned out to be somebody else's.
37 pub const ADD_PRINCIPAL: &str = "add_principal";
38 /// The ack-detail prefix of an `add_principal` refused because the name is taken.
39 pub const TAKEN: &str = "taken";
40 /// ★ UW-5. Body: [`super::AddUsers`]. **"Add users", granted** on the account — one named
41 /// user or `count` seats (gunnar: `Accounts.EnrollEmployee` as the seller, under `order`).
42 /// The box acks `ok` with `detail` = the product's `UsersAnswer` as JSON (a retry of the
43 /// same `order` answers the first grant, `unchanged`); not `ok` is refused, final — the
44 /// detail is the words, and monetize refunds what was paid for it. No address rides in
45 /// the order: monetize mails the invite itself.
46 pub const ADD_USERS: &str = "add_users";
47 /// ★ UW-4. Body: [`super::GrowRoom`]. **Room was sold past what the set has now**: queued,
48 /// and the set is to be resized (owner: the customer never sees a cap). The box keeps the
49 /// newest want, says it on its health for its operator, and acks `ok`; the resize itself
50 /// is the set's own growth. The newest want replaces an unacked older one.
51 pub const GROW_ROOM: &str = "grow_room";
52 /// ★ UW-11. Body: [`super::PurgeTenant`]. **The account is deleted for non-payment**
53 /// (nonpay's end of retention): with no `job`, the box starts the account's purge (or
54 /// adopts the purge of it already running) and acks `ok` with [`super::PurgeAnswer`] as
55 /// JSON; with a `job`, it answers that job's state the same way. A purge is minutes of
56 /// unlinking, so monetize asks again (a new order naming the job) until the answer is no
57 /// longer `running`. Not `ok` is refused for this ask, the detail its words: a job the
58 /// box no longer knows, a box that does not serve.
59 pub const PURGE_TENANT: &str = "purge_tenant";
60 /// `monetize_embed::fleet::GrowGoAhead`: **a sold estate's paid data step**, queued to the
61 /// boxes that hold the data once the step's money is captured. The box verifies the go-ahead
62 /// under monetize's key, then grows its own data disks through the ask [`super::ask::GROW_SET`].
63 pub const GROW_GO_AHEAD: &str = "grow_go_ahead";
64 /// `monetize_embed::fleet::SignInAgain`: **a sold estate's resend sign-in link**, queued to
65 /// the primary when its buyer asks. No secret rides in it: the box mints the invite itself.
66 pub const SIGN_IN_AGAIN: &str = "sign_in_again";
67 /// `monetize_embed::fleet::Refeed`: **estate keys fed again to an already-laid estate**
68 /// (MAIL-RELAY + NIGHT-PUBREPO, 2026-10-28): a box is fed its estate keys at LAY time only;
69 /// a key the ONE box holds later (the mains' mail relay, a site's `public_repositories`)
70 /// reaches a laid box through this one signed order, queued by the operator (`monetize site
71 /// refeed <site>`). The box takes each key it knows BY NAME (`monetize_embed::fleet::refeed`)
72 /// and refuses the order by name if one is not its own; it keeps them beside the feed it was
73 /// laid with, which they go over. The body may CARRY A SECRET (the relay's token): never
74 /// logged, never shown.
75 pub const REFEED: &str = "refeed";
76
77 /// **The core's order kinds, the one list** (A24 §5.7, A28 M25): what the ONE box queues to
78 /// any product's boxes. A product's guardian adds its own kinds (Gunnar's repair verbs,
79 /// `monetize_embed::fleet::kind::GUARDIAN`); a box refuses what it has no verb for BY NAME.
80 pub const CORE: [&str; 8] = [ENTITLEMENT, ADD_PRINCIPAL, ADD_USERS, GROW_ROOM, PURGE_TENANT, GROW_GO_AHEAD, SIGN_IN_AGAIN, REFEED];
81}
82
83/// The core's `Fleet.Ask` kind that is not a remedy: answered at once, no case opened.
84pub mod ask {
85 /// **Size the calling box's own data disks** at monetize's provider: no more than is paid
86 /// for its role, never smaller.
87 pub const GROW_SET: &str = "grow_set";
88 /// Body: [`super::FetchAsk`]. **The box pulls the file a product order carries**
89 /// ([`super::ProductOrder::fetch`], MONETIZE-FIX 2026-11-01): the ONE box answers the bytes
90 /// in `FleetAskAnswer.answer`, only to a box with an order queued for it that names the file.
91 /// The ONE box never pushes a file into an estate.
92 pub const FETCH: &str = "fetch";
93 /// The core's ask kinds, the one list (A24 §5.7: 7 orders + 1 ask; MAIL-RELAY made it 8
94 /// orders: `refeed`; MONETIZE-FIX added the `fetch` ask a product order's file is pulled by).
95 pub const CORE: [&str; 2] = [GROW_SET, FETCH];
96}
97
98/// Whether `kind` is one of the core's 9 kinds ([`order::CORE`], [`ask::CORE`]).
99pub fn is_core(kind: &str) -> bool {
100 order::CORE.contains(&kind) || ask::CORE.contains(&kind)
101}
102
103/// The body of an [`order::PURGE_TENANT`] order.
104#[derive(Clone, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
105#[serde(default)]
106pub struct PurgeTenant {
107 /// The account, as its own estate's main names it (no `@<estate>/`).
108 pub tenant: String,
109 /// Why, shown to the locked-out account and kept on its gate.
110 pub reason: String,
111 /// The job a start answered; empty: start (or adopt the running one).
112 pub job: String,
113}
114
115/// **The main's word about one purge job** — an [`order::PURGE_TENANT`] ack's detail.
116#[derive(Clone, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
117#[serde(default)]
118pub struct PurgeAnswer {
119 pub job: String,
120 /// Queued or running: not finished, the account stays locked.
121 pub running: bool,
122 /// The job FAILED: nothing of the account's may be released.
123 pub failed: bool,
124 pub error: String,
125 /// What was removed, in the product's words (`5 of 5 stores, 1.2 GiB`).
126 pub detail: String,
127 pub bytes_reclaimed: Option<u64>,
128 /// The lockout the purge took is still held.
129 pub still_locked: bool,
130 /// One line per store it could not reclaim.
131 pub failures: Vec<String>,
132}
133
134/// The body of an [`order::ADD_USERS`] order.
135#[derive(Clone, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
136#[serde(default)]
137pub struct AddUsers {
138 /// The account, as its own estate's main names it (no `@<estate>/`).
139 pub tenant: String,
140 /// What was asked — never the address (`email`, `invite_origin` are emptied).
141 pub ask: crate::UsersAsk,
142 /// The order reference the grant belongs to: a retry answers the first grant.
143 pub order: String,
144}
145
146/// The body of an [`order::GROW_ROOM`] order.
147#[derive(Clone, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
148#[serde(default)]
149pub struct GrowRoom {
150 /// Stored bytes sold past what the set serves now.
151 pub need_bytes: u64,
152 /// Why, for the estate's operator (which account, what it holds).
153 pub reason: String,
154}
155
156/// **One account's room for "Add users"**, as the main's own dry run says it.
157#[derive(Clone, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
158#[serde(default)]
159pub struct AccountRoom {
160 /// The account's room neither stored nor promised; `None`: nothing caps it.
161 pub unused_bytes: Option<u64>,
162 /// The room one user holds when the ask names none (the product's default).
163 pub room_bytes: u64,
164 pub seats: u32,
165 pub seats_open: u32,
166}
167
168/// **One box's word about its product**, as of `measured_unix_ms`.
169#[derive(Clone, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
170#[serde(default)]
171pub struct ProductReport {
172 /// Every tenant, and who it is (`Product::tenant_owners`).
173 pub tenants: Vec<TenantOwner>,
174 /// Each tenant's usage, by tenant name (`Product::read_usage`).
175 pub usage: BTreeMap<String, Usage>,
176 /// What the product could still serve (`Product::servable`); absent = it did not say.
177 pub servable: Option<Servable>,
178 /// Whether the paid door answered on the box (`Product::metered_door`).
179 pub metered: Option<MeteredDoor>,
180 /// **The room a raise would find**, per meter: what may still be handed to tenants
181 /// beyond what they hold. `Product::can_absorb` answers from it.
182 pub room: BTreeMap<String, u64>,
183 /// What each tenant holds PROMISED today, per meter (its current caps), by tenant name.
184 /// A raise needs `target − promised` of `room`.
185 pub promised: BTreeMap<String, BTreeMap<String, u64>>,
186 /// When a raise does not fit `room`: `Some(note)` = a bought disk becomes usable (by the
187 /// hand `note` names), `None` = it does not, and the raise is refused.
188 pub with_iron: Option<String>,
189 /// **Every name the box holds** — principals and namespaces, a tenant or not. A signup's
190 /// name is free when it is absent here (and no queued `add_principal` asks for it):
191 /// monetize never asks the box.
192 pub names: Vec<String>,
193 /// ★ UW-5: each account's room for "Add users", by tenant name (`Product::users_fit`
194 /// answers from it). Absent: the box does not say, and nothing is guessed.
195 pub accounts: BTreeMap<String, AccountRoom>,
196 /// ★ UW-10: each tenant's kept activity over the last [`ACTIVITY_DAYS`] days, by tenant
197 /// name (`Product::read_activity` answers from it). Absent: the box did not say.
198 pub activity: BTreeMap<String, crate::Activity>,
199 pub measured_unix_ms: u64,
200}
201
202/// ★ UW-10: how many days of each tenant's activity a box's report carries.
203pub const ACTIVITY_DAYS: u64 = 30;
204
205/// The body of an [`order::ADD_PRINCIPAL`] order: a paid signup's account.
206#[derive(Clone, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
207#[serde(default)]
208pub struct AddPrincipal {
209 pub username: String,
210 /// `person` | `org`, the product's own words.
211 pub kind: String,
212 /// The signup this account is for (the reference its row carries).
213 pub signup: String,
214}
215
216/// **The body of a product's OWN order kind** (MONETIZE-FIX 2026-11-01): a kind the product's
217/// facts declare (`monetize_product_guest::facts::OrderKindFacts`), queued by the operator on one
218/// of his sites and pulled by the role the declaration names. The core checks the params against
219/// the declaration and signs the order; what the kind does is the product's box's alone. The box
220/// acks `ok` with its answer as `detail` (its words or JSON), or not `ok` with its refusal.
221#[derive(Clone, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
222#[serde(default)]
223pub struct ProductOrder {
224 /// The declared params, each a value as the operator typed it.
225 pub params: BTreeMap<String, String>,
226 /// The file the order carries, kept on the ONE box: the box pulls it ([`ask::FETCH`]).
227 #[serde(skip_serializing_if = "Option::is_none")]
228 pub fetch: Option<Fetch>,
229}
230
231/// **A file a product order carries**, named by its content.
232#[derive(Clone, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
233#[serde(default)]
234pub struct Fetch {
235 /// The file's sha256, lowercase hex: what the box asks for and checks what it got against.
236 pub id: String,
237 pub bytes: u64,
238 /// The declared param the file is (the operator's `--file <param>=<path>`).
239 pub param: String,
240}
241
242/// The body of an [`ask::FETCH`] ask.
243#[derive(Clone, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
244#[serde(default)]
245pub struct FetchAsk {
246 pub id: String,
247}
248
249#[cfg(test)]
250mod core_kinds_tests {
251 use super::{ask, is_core, order};
252
253 /// A24 §5.7: the core kinds in this one list (A28 M25): 8 orders (MAIL-RELAY 2026-10-28 added
254 /// `refeed`) and 2 asks (MONETIZE-FIX 2026-11-01 added `fetch`). A product's own kinds are
255 /// its facts' (`orders`), never in this list.
256 #[test]
257 fn the_core_kinds_are_eight_orders_and_two_asks_in_one_list() {
258 let mut orders = order::CORE.to_vec();
259 orders.sort_unstable();
260 orders.dedup();
261 assert_eq!(orders.len(), 8, "eight distinct core orders");
262 assert_eq!(order::CORE, [order::ENTITLEMENT, order::ADD_PRINCIPAL, order::ADD_USERS, order::GROW_ROOM, order::PURGE_TENANT, order::GROW_GO_AHEAD, order::SIGN_IN_AGAIN, order::REFEED]);
263 assert_eq!(ask::CORE, [ask::GROW_SET, ask::FETCH]);
264 for k in order::CORE.iter().chain(ask::CORE.iter()) {
265 assert!(is_core(k), "{k} is core");
266 }
267 // The relay products' own guardian kinds are not the core's, nor is a kind nobody has.
268 for k in ["recharge_from_twin", "fence_primary", "adopt_twin", "review_twin", "restate_entitlements", "join_member", ""] {
269 assert!(!is_core(k), "{k:?} is not a core kind");
270 }
271 }
272}