crates/gunnar-repair/src/order.rs
//! **The repair order an estate PULLS** (owner 2026-10-10 08:25): its kinds and bodies.
//!
//! monetize never calls into an estate. Its guardian "fires" only by queuing a signed
//! order; the server or the twin fetches it on its next heartbeat, and the SHARED pump
//! (monetize-estate-side `pump::check`, A28 M11) checks it: signed by the monetize key
//! (`nordisk-order-v1`, strict Ed25519), for this box, newer than the last one taken, in
//! its window. What is gunnar's is only what the order says ([`decode`]) and the SAME verb
//! the local button and the boot check run ([`crate::exec::execute`]).
//!
//! Until 2026-10-26 this module also carried its own copy of the canonical form, the
//! envelope check and the signature check; the shared check was made as strict as this
//! copy first (P4c), and `gunnar-front/tests/one_order_check.rs` held the two together
//! until the copy could go.
use serde::{Deserialize, Serialize};
use crate::plan::{refuse, Refusal, RefusalCode};
/// The order kinds and their bodies. Unknown body fields are ignored.
pub mod kind {
pub const RECHARGE_FROM_TWIN: &str = "recharge_from_twin";
pub const FENCE_PRIMARY: &str = "fence_primary";
pub const ADOPT_TWIN: &str = "adopt_twin";
pub const REVIEW_TWIN: &str = "review_twin";
pub const RESTATE_ENTITLEMENTS: &str = "restate_entitlements";
pub const ENTITLEMENT: &str = "entitlement";
pub const ADD_PRINCIPAL: &str = "add_principal";
pub const GROW_GO_AHEAD: &str = "grow_go_ahead";
pub const SIGN_IN_AGAIN: &str = "sign_in_again";
pub const ADD_USERS: &str = "add_users";
pub const GROW_ROOM: &str = "grow_room";
pub const PURGE_TENANT: &str = "purge_tenant";
/// monetize-product `pull::order::REFEED` (MAIL-RELAY + NIGHT-PUBREPO, 2026-10-28): estate
/// keys fed again to a laid box by the ONE box (`{"keys": {key: value}}`). Gunnar takes the
/// mail relay's keys and the public site's switch ([`REFEED_KEYS`]) and refuses any other by
/// name.
pub const REFEED: &str = "refeed";
/// The estate keys Gunnar takes again from a `refeed` (monetize-embed `fleet::refeed`): the
/// mains' mail relay (monetize-mail `FEED_SMTP_*`) and the site's public-site switch
/// ([`super::PUBLIC_SITE_KEY`], GRS-PUBLIC 2026-10-28).
pub const REFEED_KEYS: [&str; 4] = [
"secret.smtp_url",
"secret.smtp_from",
"secret.smtp_ca",
super::PUBLIC_SITE_KEY,
];
}
/// **The site's public-site switch, as the ONE box feeds it** (`console_public_site = on | off`,
/// the lay's own word: gunnar-front `console::FEED_PUBLIC_SITE`, monetize-shop-pairing
/// `contract::FEED_PUBLIC_SITE`). At lay it reaches the front; in a `refeed` it reaches the main,
/// which stores it as the console's switch ([`PUBLIC_SITE_SETTING`]).
///
/// GRS-PUBLIC (live bug 2026-10-28): gunnar.rs was laid by its kit before its declaration said
/// `public_site = true`, and a keep boot never feeds a front again, so its console was never
/// told and https://gunnar.rs/ asked every reader to log in. The refeed is how a declaration
/// made after the lay reaches a laid site.
pub const PUBLIC_SITE_KEY: &str = "console_public_site";
/// **The generic gunnar setting the console keeps its public-site switch under** (gunnar-ui
/// `console_setting::PUBLIC_SITE`; the stored position wins over the front's feed). gunnar-server
/// gives the name no meaning: the main stores the refeed's position under it, as an
/// administrator's `Admin.SetSetting` would (gunnar-front `tests/public_site_refeed.rs` holds the
/// two names together against the real console).
pub const PUBLIC_SITE_SETTING: &str = "console.public-site";
/// **What a `refeed` feeds this box again**: the mains' mail relay, the site's public-site
/// switch, or both. At least one; [`decode`] refuses a refeed that feeds nothing.
#[derive(Clone, PartialEq, Eq, Default, Debug, Serialize, Deserialize)]
#[serde(default)]
pub struct Refeed {
/// The relay the mains mail through (MAIL-RELAY).
#[serde(skip_serializing_if = "Option::is_none")]
pub relay: Option<MailRelay>,
/// The site's public-site switch (GRS-PUBLIC): `Some(true)` on, `Some(false)` off.
#[serde(skip_serializing_if = "Option::is_none")]
pub public_site: Option<bool>,
}
/// **The relay a laid main mails through, fed again** (a `refeed`'s relay keys, monetize-embed
/// `fleet::Refeed`, MAIL-RELAY 2026-10-28; named here so this crate links no monetize). **`smtp_url` is a
/// secret** (the relay's token): its `Debug` withholds it.
#[derive(Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct MailRelay {
/// `smtp://` (STARTTLS) or `smtps://` (implicit TLS), `user:token@host[:port]`.
pub smtp_url: String,
/// The address this box mails AS.
pub smtp_from: String,
/// One more root the relay's certificate is trusted under (PEM); empty: none.
#[serde(skip_serializing_if = "String::is_empty")]
pub smtp_ca: String,
}
impl std::fmt::Debug for MailRelay {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("MailRelay")
.field("smtp_url", &"<withheld>")
.field("smtp_from", &self.smtp_from)
.field("smtp_ca", &!self.smtp_ca.is_empty())
.finish()
}
}
/// A decoded order body.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum Order {
/// Recharge `main` from `twin` (whichever of the two pulled it runs the pump).
/// `fence`: the old primary to fence FIRST (`fence_primary`), when the twin may still
/// hear it; the recharge then runs with `primary_fenced`. `None` = no fence: a twin
/// that still hears a live primary is refused `not_fenced`.
RechargeFromTwin {
main: String,
twin: String,
#[serde(default)]
accept_lost: bool,
#[serde(default)]
epoch: u64,
#[serde(default)]
case: Option<String>,
#[serde(default)]
fence: Option<String>,
/// The guardian's proof that the old primary is fenced AT THE CLOUD (its server
/// given back and read back gone: old server id, when, case). Some = the recharge
/// runs with `primary_fenced`; empty = none.
#[serde(default)]
fenced: Option<String>,
},
/// Fence the old primary at `primary`: no client writes, no shipping.
FencePrimary {
primary: String,
#[serde(default)]
epoch: u64,
#[serde(default)]
case: Option<String>,
},
/// Certify the twin at `twin` for this primary.
AdoptTwin {
twin: String,
#[serde(default)]
principal: String,
#[serde(default)]
case: Option<String>,
},
/// Review the twin; the review rides the next Report.
ReviewTwin {
#[serde(default)]
case: Option<String>,
},
/// Restate monetize's entitlement facts on a main that came back without them
/// (after a recharge): `snapshot` is a `monetize_embed::Snapshot` as JSON, kept raw
/// here so this crate links no monetize. The box verifies it under its monetize
/// key, applies each fact newer-wins and deletes nothing.
RestateEntitlements {
snapshot: serde_json::Value,
#[serde(default)]
case: Option<String>,
},
/// One tenant's signed entitlement fact (`monetize_embed::EntitlementFact` as JSON, the
/// whole body): what `Entitlement.Set` took when monetize still dialled in. Verified
/// under the box's monetize key; newer wins.
Entitlement { fact: serde_json::Value },
/// A paid signup's account (monetize-product `pull::AddPrincipal`): `username` is the
/// principal and its namespace, `principal_kind` the product's word (`person` | `org`),
/// `signup` the reference that makes a repeat of the same order idempotent.
AddPrincipal {
username: String,
principal_kind: String,
signup: String,
},
/// A sold estate's paid data step (monetize-embed `fleet::GrowGoAhead`, G16-d):
/// `go_ahead` = monetize's signed `EstateGrowth` JSON, `data_gib` = GB a data disk
/// THIS box's role is paid to. The twin grows first; the main only once its twin
/// holds main's whole set.
GrowGoAhead { go_ahead: String, data_gib: u64 },
/// A sold estate's RESEND sign-in link (monetize-embed `fleet::SignInAgain`, S10, owner
/// 2026-10-11 05:53Z), for the main: `account` = whose first passkey, `to` = the order's
/// address, `host` = the console host the link points at, `ttl_secs` = how long the
/// invite lives. No secret: THIS box mints the invite and mails the link itself
/// (monetize-estate-side `sign_in`).
SignInAgain {
account: String,
to: String,
host: String,
ttl_secs: u64,
},
/// ★ UW-5: **"Add users", granted** (monetize-product `pull::AddUsers`): `tenant` the
/// account, `ask` monetize's `UsersAsk` as JSON (no address), `order` the reference a
/// retry answers the first grant under. The ack is the product's `UsersAnswer` as JSON.
AddUsers {
tenant: String,
ask: serde_json::Value,
order: String,
},
/// ★ UW-4: **room sold past the set** (monetize-product `pull::GrowRoom`): queued, the
/// set to be resized; the box keeps the newest want and says it to its operator.
GrowRoom { need_bytes: u64, reason: String },
/// ★ UW-11: delete the account `tenant` for non-payment (monetize-product
/// `pull::PurgeTenant`): no `job` starts the purge (or adopts the running one), a `job`
/// asks that job's state. The ack is `pull::PurgeAnswer` as JSON.
PurgeTenant {
tenant: String,
reason: String,
job: String,
},
/// **A `refeed`: estate keys fed again** by the ONE box: the relay this box mails through
/// (MAIL-RELAY: kept as the relay fed at lay is, used from the next mail) and the site's
/// public-site switch (GRS-PUBLIC: stored as the console's switch).
Refeed(Refeed),
}
#[derive(Deserialize)]
struct AddUsersBody {
#[serde(default)]
tenant: String,
#[serde(default)]
ask: serde_json::Value,
#[serde(default)]
order: String,
}
#[derive(Deserialize)]
struct PurgeTenantBody {
#[serde(default)]
tenant: String,
#[serde(default)]
reason: String,
#[serde(default)]
job: String,
}
#[derive(Deserialize)]
struct GrowRoomBody {
#[serde(default)]
need_bytes: u64,
#[serde(default)]
reason: String,
}
#[derive(Deserialize)]
struct RechargeBody {
main: String,
twin: String,
#[serde(default)]
accept_lost: bool,
#[serde(default)]
epoch: u64,
#[serde(default)]
case: Option<String>,
#[serde(default)]
fence: Option<String>,
#[serde(default)]
fenced: Option<String>,
}
#[derive(Deserialize)]
struct FenceBody {
primary: String,
#[serde(default)]
epoch: u64,
#[serde(default)]
case: Option<String>,
}
#[derive(Deserialize)]
struct AdoptBody {
twin: String,
#[serde(default)]
principal: String,
#[serde(default)]
case: Option<String>,
}
#[derive(Deserialize)]
struct RestateBody {
snapshot: serde_json::Value,
#[serde(default)]
case: Option<String>,
}
#[derive(Deserialize)]
struct AddPrincipalBody {
#[serde(default)]
username: String,
#[serde(default)]
kind: String,
#[serde(default)]
signup: String,
}
#[derive(Deserialize, Default)]
struct ReviewBody {
#[serde(default)]
case: Option<String>,
}
/// The ack the box sends back in its next Report.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct OrderAck {
pub seq: u64,
pub ok: bool,
pub detail: String,
/// The verb's [`crate::Receipt`] / [`crate::TwinReview`] as JSON; empty when refused.
pub receipt_json: String,
}
/// **What a checked order says**: its body, read for its `kind`. Unknown body fields are
/// ignored; a body that does not decode, or lacks what its kind needs, is refused by name;
/// so is a kind this box does not know. Pure: the order was checked (signature, addressee,
/// sequence, window) by the shared pump before it gets here.
pub fn decode(seq: u64, kind: &str, body: &[u8]) -> Result<Order, Refusal> {
let bad = |m: String| refuse(RefusalCode::BadOrder, m);
let undecodable = |e: serde_json::Error| {
bad(format!(
"order {seq} ({kind}) has a body that does not decode: {e}"
))
};
let body: &[u8] = if body.is_empty() { b"{}" } else { body };
Ok(match kind {
kind::RECHARGE_FROM_TWIN => {
let b: RechargeBody = serde_json::from_slice(body).map_err(undecodable)?;
Order::RechargeFromTwin {
main: b.main,
twin: b.twin,
accept_lost: b.accept_lost,
epoch: b.epoch,
case: b.case,
fence: b.fence.filter(|f| !f.trim().is_empty()),
fenced: b.fenced.filter(|f| !f.trim().is_empty()),
}
}
kind::FENCE_PRIMARY => {
let b: FenceBody = serde_json::from_slice(body).map_err(undecodable)?;
Order::FencePrimary {
primary: b.primary,
epoch: b.epoch,
case: b.case,
}
}
kind::ADOPT_TWIN => {
let b: AdoptBody = serde_json::from_slice(body).map_err(undecodable)?;
Order::AdoptTwin {
twin: b.twin,
principal: b.principal,
case: b.case,
}
}
kind::REVIEW_TWIN => {
let b: ReviewBody = serde_json::from_slice(body).map_err(undecodable)?;
Order::ReviewTwin { case: b.case }
}
kind::RESTATE_ENTITLEMENTS => {
let b: RestateBody = serde_json::from_slice(body).map_err(undecodable)?;
Order::RestateEntitlements {
snapshot: b.snapshot,
case: b.case,
}
}
kind::ENTITLEMENT => {
let fact: serde_json::Value = serde_json::from_slice(body).map_err(undecodable)?;
if !fact.is_object() {
return Err(bad(format!(
"order {} is an entitlement whose body is not a signed fact",
seq
)));
}
Order::Entitlement { fact }
}
kind::ADD_PRINCIPAL => {
let b: AddPrincipalBody = serde_json::from_slice(body).map_err(undecodable)?;
if b.username.trim().is_empty() || b.signup.trim().is_empty() {
return Err(bad(format!(
"order {} is an add_principal without a username or a signup",
seq
)));
}
Order::AddPrincipal {
username: b.username,
principal_kind: b.kind,
signup: b.signup,
}
}
kind::GROW_GO_AHEAD => {
let b: GrowGoAheadBody = serde_json::from_slice(body).map_err(undecodable)?;
if b.go_ahead.trim().is_empty() || b.data_gib == 0 {
return Err(bad(format!(
"order {} is a grow_go_ahead without monetize's signed step or a size",
seq
)));
}
Order::GrowGoAhead {
go_ahead: b.go_ahead,
data_gib: b.data_gib,
}
}
kind::SIGN_IN_AGAIN => {
let b: SignInAgainBody = serde_json::from_slice(body).map_err(undecodable)?;
// What it names; whether a link can carry it is the main's to check before it
// mints anything (monetize-estate-side `sign_in::check`, in the executor).
if b.account.trim().is_empty() || b.to.trim().is_empty() || b.host.trim().is_empty() {
return Err(bad(format!(
"order {seq} is a sign_in_again without an account, an address or a host"
)));
}
Order::SignInAgain {
account: b.account,
to: b.to,
host: b.host,
ttl_secs: b.ttl_secs,
}
}
kind::ADD_USERS => {
let b: AddUsersBody = serde_json::from_slice(body).map_err(undecodable)?;
if b.tenant.trim().is_empty() || b.order.trim().is_empty() || !b.ask.is_object() {
return Err(bad(format!(
"order {} is an add_users without an account, an ask or an order reference",
seq
)));
}
Order::AddUsers {
tenant: b.tenant,
ask: b.ask,
order: b.order,
}
}
kind::GROW_ROOM => {
let b: GrowRoomBody = serde_json::from_slice(body).map_err(undecodable)?;
if b.need_bytes == 0 {
return Err(bad(format!(
"order {} is a grow_room that wants nothing",
seq
)));
}
Order::GrowRoom {
need_bytes: b.need_bytes,
reason: b.reason,
}
}
kind::PURGE_TENANT => {
let b: PurgeTenantBody = serde_json::from_slice(body).map_err(undecodable)?;
if b.tenant.trim().is_empty() || b.reason.trim().is_empty() {
return Err(bad(format!(
"order {} is a purge_tenant without an account or a reason",
seq
)));
}
Order::PurgeTenant {
tenant: b.tenant,
reason: b.reason,
job: b.job,
}
}
kind::REFEED => {
// The body carries the relay's token: its decode error is not echoed.
#[derive(Deserialize, Default)]
#[serde(default)]
struct RefeedBody {
keys: std::collections::BTreeMap<String, String>,
}
let b: RefeedBody = serde_json::from_slice(body).map_err(|_| {
bad(format!(
"order {seq} is a refeed whose body does not decode as estate keys"
))
})?;
if let Some(k) = b
.keys
.keys()
.find(|k| !kind::REFEED_KEYS.contains(&k.as_str()))
{
return Err(bad(format!("order {seq} is a refeed of the estate key {k:?}, which Gunnar does not take again (it takes {}): nothing was kept", kind::REFEED_KEYS.join(", "))));
}
let get = |k: &str| {
b.keys
.get(k)
.map(|v| v.trim().to_string())
.unwrap_or_default()
};
let relay = if ["secret.smtp_url", "secret.smtp_from", "secret.smtp_ca"]
.iter()
.any(|k| b.keys.contains_key(*k))
{
let r = MailRelay {
smtp_url: get("secret.smtp_url"),
smtp_from: get("secret.smtp_from"),
smtp_ca: get("secret.smtp_ca"),
};
let url = r.smtp_url.as_str();
if url.is_empty() || r.smtp_from.is_empty() {
return Err(bad(format!(
"order {seq} is a refeed that names no mail relay or no sender"
)));
}
if !(url.starts_with("smtp://") || url.starts_with("smtps://")) {
return Err(bad(format!(
"order {seq} is a refeed whose relay is not smtp:// or smtps://"
)));
}
Some(r)
} else {
None
};
let public_site = match b.keys.get(PUBLIC_SITE_KEY).map(|v| v.trim().to_ascii_lowercase()) {
None => None,
Some(v) if v == "on" => Some(true),
Some(v) if v == "off" => Some(false),
Some(v) => return Err(bad(format!("order {seq} is a refeed of {PUBLIC_SITE_KEY} = {v:?}, which is neither `on` nor `off`: nothing was kept"))),
};
if relay.is_none() && public_site.is_none() {
return Err(bad(format!("order {seq} is a refeed that feeds nothing Gunnar takes (no mail relay, no {PUBLIC_SITE_KEY}): nothing was kept")));
}
Order::Refeed(Refeed { relay, public_site })
}
other => {
return Err(bad(format!(
"order {} has kind {other:?}, which this box does not know",
seq
)))
}
})
}
#[derive(Deserialize)]
struct GrowGoAheadBody {
#[serde(default)]
go_ahead: String,
#[serde(default)]
data_gib: u64,
}
#[derive(Deserialize)]
struct SignInAgainBody {
#[serde(default)]
account: String,
#[serde(default)]
to: String,
#[serde(default)]
host: String,
#[serde(default)]
ttl_secs: u64,
}
#[cfg(test)]
mod tests {
use super::*;
/// A pulled order as the shared pump hands it on: its sequence, kind and body.
#[derive(Clone)]
struct Raw {
seq: u64,
role: String,
kind: String,
body: Vec<u8>,
}
fn golden() -> Raw {
Raw { seq: 3, role: "twin".into(), kind: "recharge_from_twin".into(), body: br#"{"accept_lost":false,"epoch":5,"main":"10.6.0.10:50051","twin":"10.6.0.12:50051"}"#.to_vec() }
}
fn check(o: &Raw) -> Result<Order, Refusal> {
let _ = &o.role;
decode(o.seq, &o.kind, &o.body)
}
#[test]
fn a_recharge_order_decodes_its_body() {
let order = check(&golden()).unwrap();
assert!(
matches!(order, Order::RechargeFromTwin { ref main, epoch: 5, .. } if main == "10.6.0.10:50051")
);
let mut o = golden();
o.body = b"{not json".to_vec();
let e = check(&o).unwrap_err();
assert!(
e.code == RefusalCode::BadOrder
&& e.message
.contains("order 3 (recharge_from_twin) has a body that does not decode"),
"{e}"
);
}
#[test]
fn unknown_fields_are_ignored_and_unknown_kinds_refused() {
let mut o = golden();
o.body = br#"{"main":"m","twin":"t","case":"c-1","future":42}"#.to_vec();
assert!(
matches!(check(&o).unwrap(), Order::RechargeFromTwin { case: Some(c), .. } if c == "c-1")
);
o.body = br#"{"main":"m","twin":"t","fence":"10.6.0.9:50051"}"#.to_vec();
assert!(
matches!(check(&o).unwrap(), Order::RechargeFromTwin { fence: Some(f), .. } if f == "10.6.0.9:50051"),
"the old primary to fence rides the body"
);
o.body = br#"{"main":"m","twin":"t","fence":""}"#.to_vec();
assert!(
matches!(
check(&o).unwrap(),
Order::RechargeFromTwin { fence: None, .. }
),
"an empty fence is none"
);
o.body =
br#"{"main":"m","twin":"t","fenced":"server/0a1b read back gone 21:30Z case c-9"}"#
.to_vec();
assert!(
matches!(check(&o).unwrap(), Order::RechargeFromTwin { fenced: Some(f), fence: None, .. } if f.starts_with("server/0a1b")),
"the cloud fence's proof rides the body"
);
o.body = br#"{"main":"m","twin":"t","fenced":" "}"#.to_vec();
assert!(
matches!(
check(&o).unwrap(),
Order::RechargeFromTwin { fenced: None, .. }
),
"an empty proof is none"
);
o.kind = "format_disk".into();
assert!(check(&o).is_err());
}
/// monetize's guardian queues `restate_entitlements → main` after a recharge
/// (monetize feat/r5-4-guardian 2a87383): the snapshot rides the body as JSON.
#[test]
fn a_restate_order_carries_its_snapshot_and_one_without_is_refused() {
let mut o = golden();
o.role = "main".into();
o.kind = kind::RESTATE_ENTITLEMENTS.into();
o.body = br#"{"snapshot":{"issued_unix_ms":9,"facts":[],"signature":[1,2]},"case":"c-7"}"#
.to_vec();
match check(&o).expect("a known kind") {
Order::RestateEntitlements { snapshot, case } => {
assert_eq!(snapshot["issued_unix_ms"], 9);
assert_eq!(case.as_deref(), Some("c-7"));
}
other => panic!("{other:?}"),
}
o.body = br#"{"case":"c-7"}"#.to_vec();
let e = check(&o).expect_err("no snapshot, nothing to restate");
assert_eq!(e.code, RefusalCode::BadOrder);
assert!(e.message.contains("snapshot"), "{e}");
}
/// ONE monetize (G1-pull #1): an entitlement leaves monetize as order `entitlement`,
/// the body the signed fact itself (monetize-product `pull::order::ENTITLEMENT`).
#[test]
fn an_entitlement_order_carries_the_fact_as_its_body() {
let mut o = golden();
o.role = "main".into();
o.kind = kind::ENTITLEMENT.into();
o.body = br#"{"tenant":"acme","plan":"10gb","state":"Paid","signature":[1]}"#.to_vec();
match check(&o).expect("a known kind") {
Order::Entitlement { fact } => assert_eq!(fact["tenant"], "acme"),
other => panic!("{other:?}"),
}
o.body = b"[1,2]".to_vec();
assert_eq!(
check(&o).expect_err("not a fact").code,
RefusalCode::BadOrder
);
}
/// ONE monetize (G1-pull): a paid signup's account leaves monetize as order
/// `add_principal` (monetize-product `pull::order::ADD_PRINCIPAL`), body
/// `{username, kind, signup}`. A body without a name or a signup is no order.
#[test]
fn an_add_principal_order_carries_the_signups_name_and_reference() {
let mut o = golden();
o.role = "main".into();
o.kind = kind::ADD_PRINCIPAL.into();
o.body = br#"{"username":"bob","kind":"person","signup":"stripe:cs_1"}"#.to_vec();
assert_eq!(
check(&o).expect("a known kind"),
Order::AddPrincipal {
username: "bob".into(),
principal_kind: "person".into(),
signup: "stripe:cs_1".into()
}
);
for body in [
&br#"{"kind":"person","signup":"stripe:cs_1"}"#[..],
br#"{"username":"bob","kind":"person"}"#,
br#"{"username":" ","kind":"person","signup":"s"}"#,
] {
o.body = body.to_vec();
let e = check(&o).expect_err("no name or no signup");
assert_eq!(e.code, RefusalCode::BadOrder, "{e}");
}
}
/// ★ UW-5 / UW-4: `add_users` carries `{tenant, ask, order}` (monetize-product
/// `pull::AddUsers`) and `grow_room` `{need_bytes, reason}` (`pull::GrowRoom`). An
/// `add_users` without a tenant or an order, or a `grow_room` of nothing, is no order.
#[test]
fn add_users_and_grow_room_orders_carry_monetizes_bodies() {
let mut o = golden();
o.role = "main".into();
o.kind = kind::ADD_USERS.into();
o.body = br#"{"tenant":"acme","ask":{"count":1,"room_bytes":0,"name":"eve"},"order":"gunnar/acme/users-1"}"#.to_vec();
assert_eq!(
check(&o).expect("a known kind"),
Order::AddUsers {
tenant: "acme".into(),
ask: serde_json::json!({"count": 1, "room_bytes": 0, "name": "eve"}),
order: "gunnar/acme/users-1".into()
}
);
o.body = br#"{"tenant":"acme","ask":{"count":1}}"#.to_vec();
assert_eq!(check(&o).expect_err("no order").code, RefusalCode::BadOrder);
o.kind = kind::GROW_ROOM.into();
o.body = br#"{"need_bytes":32212254720,"reason":"acme needs 30 GiB"}"#.to_vec();
assert_eq!(
check(&o).expect("a known kind"),
Order::GrowRoom {
need_bytes: 30 << 30,
reason: "acme needs 30 GiB".into()
}
);
o.body = br#"{"need_bytes":0}"#.to_vec();
assert_eq!(
check(&o).expect_err("nothing wanted").code,
RefusalCode::BadOrder
);
assert_eq!(
(kind::ADD_USERS, kind::GROW_ROOM),
("add_users", "grow_room"),
"monetize-product pull::order, word for word"
);
}
/// ★ UW-11: `purge_tenant` carries `{tenant, reason, job}` (monetize-product
/// `pull::PurgeTenant`); no account or no reason is no order.
#[test]
fn a_purge_tenant_order_carries_monetizes_body() {
let mut o = golden();
o.role = "main".into();
o.kind = kind::PURGE_TENANT.into();
o.body = br#"{"tenant":"bob","reason":"nonpay: bob lapsed","job":"j-1"}"#.to_vec();
assert_eq!(
check(&o).expect("a known kind"),
Order::PurgeTenant {
tenant: "bob".into(),
reason: "nonpay: bob lapsed".into(),
job: "j-1".into()
}
);
o.body = br#"{"tenant":"bob","reason":"nonpay"}"#.to_vec();
assert_eq!(
check(&o).expect("a start"),
Order::PurgeTenant {
tenant: "bob".into(),
reason: "nonpay".into(),
job: String::new()
}
);
for body in [
&br#"{"reason":"nonpay"}"#[..],
br#"{"tenant":"bob","reason":" "}"#,
] {
o.body = body.to_vec();
assert_eq!(
check(&o).expect_err("no account or no reason").code,
RefusalCode::BadOrder
);
}
assert_eq!(
kind::PURGE_TENANT,
"purge_tenant",
"monetize-product pull::order, word for word"
);
}
/// G16-d: a sold estate's paid data step leaves monetize as order `grow_go_ahead`,
/// body `{go_ahead, data_gib}` (monetize-embed `fleet::GrowGoAhead`). No go-ahead or
/// no size is no order.
#[test]
fn a_grow_go_ahead_order_carries_monetizes_signed_step_and_this_roles_size() {
let mut o = golden();
o.kind = kind::GROW_GO_AHEAD.into();
o.body = br#"{"go_ahead":"{\"estate\":\"acme\"}","data_gib":120}"#.to_vec();
assert_eq!(
check(&o).expect("a known kind"),
Order::GrowGoAhead {
go_ahead: r#"{"estate":"acme"}"#.into(),
data_gib: 120
}
);
for body in [
&br#"{"data_gib":120}"#[..],
br#"{"go_ahead":" ","data_gib":120}"#,
br#"{"go_ahead":"{}"}"#,
br#"{"go_ahead":"{}","data_gib":0}"#,
] {
o.body = body.to_vec();
let e = check(&o).expect_err("no go-ahead or no size");
assert_eq!(e.code, RefusalCode::BadOrder, "{e}");
}
}
/// ★ S10 (owner 2026-10-11 05:53Z): a sold estate's RESEND sign-in link leaves monetize as
/// order `sign_in_again` for the estate's main (monetize-embed `fleet::SignInAgain`), body
/// `{account, to, host, ttl_secs}` — whose passkey, which address, which console host, how
/// long — and NO secret: the estate mints the invite and mails the link itself. No account,
/// no address or no host is no order; an address or a name that could carry a second mail
/// header is refused by the main before it mints (gunnar-server `tests/sign_in_again.rs`).
#[test]
fn a_sign_in_again_order_names_the_account_the_address_and_the_host_and_no_secret() {
let mut o = golden();
o.role = "main".into();
o.kind = kind::SIGN_IN_AGAIN.into();
o.body = br#"{"account":"alice","to":"buyer@mock.invalid","host":"acme.gunnar.rs","ttl_secs":604800}"#.to_vec();
let got = check(&o).expect("a known kind");
assert_eq!(
serde_json::to_value(&got).unwrap(),
serde_json::json!({"kind": "sign_in_again", "account": "alice", "to": "buyer@mock.invalid", "host": "acme.gunnar.rs", "ttl_secs": 604800})
);
for body in [
&br#"{"to":"buyer@mock.invalid","host":"acme.gunnar.rs"}"#[..],
br#"{"account":"alice","host":"acme.gunnar.rs"}"#,
br#"{"account":"alice","to":"buyer@mock.invalid"}"#,
] {
o.body = body.to_vec();
let e = check(&o).expect_err("not a resend this box can mail");
assert!(e.to_string().contains("sign_in_again"), "{e}");
}
}
/// ★ **MAIL-RELAY: a `refeed` names the relay and the sender**; one that names neither, a key
/// Gunnar does not take again (`public_repositories`),
/// or a relay that is not `smtp://`/`smtps://`, is refused by name, never echoing the token;
/// its Debug withholds the token.
#[test]
fn a_refeed_order_names_the_relay_and_a_key_gunnar_does_not_take_is_refused_by_name() {
let mut o = golden();
o.role = "main".into();
o.kind = kind::REFEED.into();
o.body = br#"{"keys":{"secret.smtp_url":"smtps://gunnar%40gunnar.rs:t0kenVALUE@relay.mock.invalid:465","secret.smtp_from":"Gunnar <gunnar@mock.invalid>"}}"#.to_vec();
match check(&o).expect("a known kind") {
Order::Refeed(Refeed {
relay: Some(r),
public_site: None,
}) => {
assert_eq!(
(r.smtp_url.as_str(), r.smtp_from.as_str()),
(
"smtps://gunnar%40gunnar.rs:t0kenVALUE@relay.mock.invalid:465",
"Gunnar <gunnar@mock.invalid>"
)
);
assert!(!format!("{r:?}").contains("t0kenVALUE"));
}
other => panic!("{other:?}"),
}
for (body, word) in [
(&br#"{"keys":{"secret.smtp_from":"g@mock.invalid"}}"#[..], "no mail relay"),
(br#"{"keys":{"secret.smtp_url":"smtps://u:t0kenVALUE@r.mock.invalid"}}"#, "no sender"),
(br#"{"keys":{"secret.smtp_url":"http://u:t0kenVALUE@r.mock.invalid","secret.smtp_from":"g@mock.invalid"}}"#, "not smtp://"),
(br#"{"keys":{"public_repositories":"crates"}}"#, "public_repositories"),
(br#"{"keys":7}"#, "does not decode"),
] {
o.body = body.to_vec();
let e = check(&o).expect_err("not a refeed Gunnar takes").to_string();
assert!(e.contains("refeed") && e.contains(word) && !e.contains("t0kenVALUE"), "{e}");
}
}
/// ★ **GRS-PUBLIC (live bug 2026-10-28): a `refeed` feeds the site's public-site switch
/// again** (`console_public_site = on | off`, the lay's own word). gunnar.rs was laid by its
/// kit before its declaration said `public_site = true`, and a keep boot never feeds a front
/// again, so its console's switch stayed OFF and https://gunnar.rs/ asked every reader to log
/// in. The switch arrives alone or beside the relay; a value that is neither `on` nor `off`,
/// and a refeed that feeds nothing, are refused by name.
#[test]
fn a_refeed_order_feeds_the_public_site_switch_alone_or_beside_the_relay() {
assert!(
kind::REFEED_KEYS.contains(&"console_public_site"),
"{:?}",
kind::REFEED_KEYS
);
let mut o = golden();
o.role = "main".into();
o.kind = kind::REFEED.into();
for (value, on) in [("on", true), (" OFF ", false)] {
o.body = format!(r#"{{"keys":{{"console_public_site":"{value}"}}}}"#).into_bytes();
let got =
serde_json::to_value(check(&o).expect("the switch alone is a refeed Gunnar takes"))
.unwrap();
assert_eq!(
(got["kind"].as_str(), got["public_site"].as_bool()),
(Some("refeed"), Some(on)),
"{got}"
);
assert!(got["relay"].is_null(), "no relay was fed: {got}");
}
o.body = br#"{"keys":{"secret.smtp_url":"smtps://u:t0kenVALUE@relay.mock.invalid:465","secret.smtp_from":"g@mock.invalid","console_public_site":"on"}}"#.to_vec();
let both = check(&o).expect("the relay and the switch together");
assert!(!format!("{both:?}").contains("t0kenVALUE"), "{both:?}");
let got = serde_json::to_value(&both).unwrap();
assert_eq!(
(
got["relay"]["smtp_from"].as_str(),
got["public_site"].as_bool()
),
(Some("g@mock.invalid"), Some(true)),
"{got}"
);
for (body, word) in [
(
&br#"{"keys":{"console_public_site":"yes"}}"#[..],
"console_public_site",
),
(br#"{"keys":{}}"#, "nothing"),
] {
o.body = body.to_vec();
let e = check(&o)
.expect_err("not a refeed Gunnar takes")
.to_string();
assert!(e.contains("refeed") && e.contains(word), "{e}");
}
}
}