Skip to main content

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}