Skip to main content

gunnar_server/
feed.rs

1//! **The estate values the installer's door FEEDS**, taken instead of the
2//! medium's — so one medium serves any estate.
3//!
4//! The join reply the pairing loop receives carries a `feed` table
5//! (`nordisk_pairing::join::JoinReply::feed`); the loop persists the whole
6//! reply to `<data>/pairing/joined.toml` ([`crate::pairing::JOINED_REL`]).
7//! Three of its keys configure this box:
8//!
9//! | key               | what it replaces on the medium                       | applied            |
10//! |-------------------|------------------------------------------------------|--------------------|
11//! | `monetize_pubkey` | `GUNNAR_MONETIZE_PUBKEY` / `--monetize-pubkey`        | at start, or LIVE when it arrives |
12//! | `clock_key`       | `ESTATE_CLOCK_KEY` / `GUNNAR_CLOCK_KEY` (`korp.env.` / `clock_key=` param) | on every clock sync|
13//! | `clock_peer`      | `ESTATE_CLOCK` / `GUNNAR_CLOCK` (`korp.env.` / `clock=` param)         | on every clock sync|
14//!
15//! (`admin_pubkey` is the fourth feed key; the pairing loop installs it itself.)
16//!
17//! # Precedence: there is none — a disagreement is REFUSED by name
18//!
19//! Medium alone → the medium's. Door alone → the door's. Both and EQUAL →
20//! that value. Both and DIFFERENT → [`FEED_MISMATCH`], never a silent pick:
21//! the medium and the installer disagree about which estate this box is in,
22//! and choosing either one is choosing whose money or whose time to trust
23//! without saying so (the reasoning of ops' `monetize-pubkey-mismatch`
24//! preflight, which refuses a bring-up over the same disagreement).
25//!
26//! * The monetize key at start: a refusal to start ([`resolve_monetize`]).
27//! * The monetize key arriving while running (first boot): the key in force
28//!   stays, the `Admin.Health` alarm [`FEED_MISMATCH`] is raised.
29//! * The clock: every sync reads the effective configuration
30//!   ([`clock_config`]), and a disagreement is the sync's `misconfigured`
31//!   report naming [`FEED_MISMATCH`] — the clock is left alone.
32//!
33//! # The monetize key applies LIVE (owner 2026-09-25: avoid restarts)
34//!
35//! A metered box on a `fed` medium starts KEYLESS: its entitlement slot and
36//! its growth go-ahead policy are built with no key (every namespace Free,
37//! `Entitlement.Set` refused, a `monetize` go-ahead refused) and registered
38//! as takers ([`on_monetize_key`]). When the installer feeds the key (or vm1
39//! offers it), [`monetize_arrived`] hands it to each taker, which swaps it in
40//! place — the slot re-verifies every stored fact under it — and the key is
41//! persisted in `joined.toml` for the next start as before. Until 2026-09-25
42//! this was a restart of the machine. The clock (`clock_key`, `clock_peer`)
43//! and the fed principals were already live: the clock reads the feed on
44//! every sync, and the primary installs principals on every feed.
45//!
46//! # The bus is the shared one (L10, 2026-10-20)
47//!
48//! Every feed this process takes is published on nordisk-pairing's process-wide
49//! feed bus ([`feedbus::publish`], Skidbladnir 0.5.15, A7 row 8): a part that
50//! applies a fed value LIVE subscribes with [`feedbus::on_published`] and reads
51//! the typed [`Fed::from_feed`]; [`Fed::current`] is the typed view of the
52//! value in force. Gunnar's own bus (this module's `publish`, `on_published`,
53//! `current`) is gone; this module is the typed view over the shared one.
54
55use std::path::Path;
56use std::sync::{Mutex, OnceLock, RwLock};
57
58use nordisk_pairing::feedbus::{self, Feed};
59
60/// The relay a sold estate's main mails through, as the shop feeds it (monetize-server
61/// `estate_order::FEED_SMTP_*`, P1).
62pub const FEED_SMTP_URL: &str = "secret.smtp_url";
63/// **The front's egress door** (Skidbladnir nordisk-pairing `egress::FEED_EGRESS`, the lean
64/// estate, A19): fed by the shop to a box laid with no public IPv4. The box's clients that
65/// reach the internet (the identity provider's, the webhooks', the mirrors', the mail relay's)
66/// leave through it ([`take_egress`], `nordisk_pairing::egress::proxy`).
67pub const FEED_EGRESS: &str = nordisk_pairing::egress::FEED_EGRESS;
68pub const FEED_SMTP_FROM: &str = "secret.smtp_from";
69/// One more root the relay's certificate is trusted under (PEM; a relay behind a private CA,
70/// RA5-23). Empty or absent: the public roots alone.
71pub const FEED_SMTP_CA: &str = "secret.smtp_ca";
72
73/// The feed keys this module reads (the contract's names; never renamed).
74pub const FEED_MONETIZE: &str = "monetize_pubkey";
75pub const FEED_CLOCK_KEY: &str = "clock_key";
76pub const FEED_CLOCK_PEER: &str = "clock_peer";
77/// **The operator's internal networks a company IdP may live on** (owner
78/// 2026-10-06, a private gunnar): a list of CIDRs, estate config the
79/// installer feeds; never an account admin's. Empty or absent: today's guard
80/// (`gunnar_grpc::sso::PrivateNets`). Applied LIVE on every feed.
81pub const FEED_SSO_PRIVATE_IDP_CIDRS: &str = "sso_private_idp_cidrs";
82/// **The operator's switch for company SSO's network** (A34-16, owner rule: default =
83/// AIRGAP, outbound needs explicit config): exactly `on` lets this box dial an account's
84/// identity provider; absent or anything else, no IdP is dialled and a sign-in through one
85/// is refused by name (`[sso-egress-off]`). Estate config the installer feeds; never an
86/// account admin's. Applied LIVE. A box with no feed takes `GUNNAR_SSO_EGRESS=on`.
87pub const FEED_SSO_EGRESS: &str = "sso_egress";
88/// **The DNS-over-HTTPS resolver an e-mail domain's proof is read from** (A34-5): an
89/// https URL answering `application/dns-json` (dns.google/resolve, Cloudflare's
90/// dns-query); absent: no domain proof is read (RA5-24, no default resolver). Estate
91/// config; applied LIVE.
92pub const FEED_SSO_DNS_RESOLVER: &str = "sso_dns_resolver";
93/// **This estate SELLS room** (2026-10-08): `true` on a selling shop's boxes (gunnar.rs,
94/// test2), absent everywhere else — a SOLD estate and the open build keep "no fact is
95/// uncapped". Applied LIVE ([`Fed::sells_room`], gunnar-grpc `selling`).
96pub const FEED_SELLS_ROOM: &str = "sells_room";
97/// **The ONE monetize's `Fleet` door** (5.4, ONE monetize, M-D35): what `--fleet` names on
98/// the medium. shop-pairing feeds it to the front, the front offers it to every box; the
99/// box's pull channel reports there ([`crate::repair::resolve_fleet`]).
100pub const FEED_FLEET: &str = "fleet";
101/// **The estate this box reports for** (`--fleet-instance`), fed beside [`FEED_FLEET`].
102pub const FEED_FLEET_INSTANCE: &str = "fleet_instance";
103/// **Where the pull channel connects** (`host:port`), fed beside [`FEED_FLEET`] when the
104/// door's name is not what this box should dial (a test bench whose guests resolve no
105/// names). TLS still verifies the certificate under [`FEED_FLEET`]'s host. Absent: the
106/// URL's own host, resolved. A BENCH address only (ST-19, [`crate::repair::seam_refusal`]).
107pub const FEED_FLEET_AT: &str = "fleet_at";
108/// **The PEM the pull channel's door is checked against**, fed beside [`FEED_FLEET`] when
109/// its certificate is not under the public roots. Absent: the public roots. Only beside a
110/// bench [`FEED_FLEET_AT`] (ST-19).
111pub const FEED_FLEET_CA: &str = "fleet_ca";
112
113/// **The SITE's pay page** (UW-6, feed key `refusal_link`: the open build names no money): a `{tenant}` template, `https://<site>/billing/{tenant}`
114/// — a page the site's own console serves. The medium is one for every estate, so the
115/// page it bakes (`--billing-url`) is a placeholder this replaces, LIVE
116/// ([`crate::entitlement::EntitlementSlot::set_billing_url`]).
117pub const FEED_BILLING_URL: &str = "refusal_link";
118/// **Auto-fix, fed** (main rebuild, 2026-10-12): exactly `true` turns the box's auto-fix on,
119/// as `--repair-auto` does. The estate's opt-in, carried by its installer; nothing else
120/// changes what the guard allows (`gunnar_repair::rebuild`).
121pub const FEED_REPAIR_AUTO: &str = "repair_auto";
122/// **The repair key, fed** (main rebuild, 2026-10-12): an OpenSSH private key this box signs
123/// its repair calls with when no `--repair-key` is given — a re-laid main holds no twin key,
124/// and without one every pulled order and the auto recharge were refused. The installer
125/// feeds its public half as a principal, so the twin reads its log to it and a fresh main
126/// takes its `Bootstrap.Restore`.
127pub const FEED_REPAIR_KEY: &str = "secret.repair_key";
128
129/// The alarm/refusal word for a medium value and a DIFFERENT fed value.
130pub const FEED_MISMATCH: &str = "pairing.feed_mismatch";
131/// The alarm for a fed monetize key this running process could not apply
132/// (no entitlement slot to apply it to, or a taker refused it). It used to be
133/// `pairing.feed_restart` and a reboot; the key is applied live now.
134pub const FEED_NOT_APPLIED: &str = "pairing.feed_not_applied";
135
136/// **`vm`'s offer of `key`** (`offer.<vm>.<key>`), trimmed; blank is absent.
137pub fn offered_by<'a>(
138    feed: &'a std::collections::BTreeMap<String, String>,
139    vm: &str,
140    key: &str,
141) -> Option<&'a str> {
142    feed.get(&format!("offer.{vm}.{key}"))
143        .map(|v| v.trim())
144        .filter(|v| !v.is_empty())
145}
146
147/// What the door fed, as far as this box's configuration goes.
148#[derive(Debug, Clone, Default, PartialEq, Eq)]
149pub struct Fed {
150    pub monetize_pubkey: Option<String>,
151    pub clock_key: Option<String>,
152    pub clock_peer: Option<String>,
153    /// [`FEED_SSO_PRIVATE_IDP_CIDRS`], as fed.
154    pub sso_private_idp_cidrs: Option<String>,
155    /// [`FEED_SSO_EGRESS`]: exactly `on`, as fed; anything else is off.
156    pub sso_egress: bool,
157    /// [`FEED_SSO_DNS_RESOLVER`], as fed.
158    pub sso_dns_resolver: Option<String>,
159    /// [`FEED_SELLS_ROOM`]: exactly `true`, as fed; anything else is false.
160    pub sells_room: bool,
161    /// [`FEED_FLEET`], as fed.
162    pub fleet: Option<String>,
163    /// [`FEED_FLEET_INSTANCE`], as fed.
164    pub fleet_instance: Option<String>,
165    /// [`FEED_FLEET_AT`], as fed.
166    pub fleet_at: Option<String>,
167    /// [`FEED_FLEET_CA`], as fed.
168    pub fleet_ca: Option<String>,
169    /// [`FEED_SMTP_URL`], as fed: the relay a SOLD estate's main mails through (P1). Its
170    /// `Debug` withholds it (it carries the submission token).
171    pub smtp_url: Option<Withheld>,
172    /// [`FEED_SMTP_FROM`], as fed.
173    pub smtp_from: Option<String>,
174    /// [`FEED_SMTP_CA`], as fed (a PEM root for the relay's TLS).
175    pub smtp_ca: Option<String>,
176    /// [`FEED_BILLING_URL`], as fed.
177    pub billing_url: Option<String>,
178    /// [`FEED_REPAIR_AUTO`]: exactly `true`, as fed; anything else is false.
179    pub repair_auto: bool,
180    /// [`FEED_REPAIR_KEY`], as fed (withheld from `Debug`).
181    pub repair_key: Option<Withheld>,
182    /// [`FEED_EGRESS`], as fed: an `http://` door only.
183    pub egress: Option<String>,
184}
185
186/// A fed value that is a credential: kept, compared, never shown.
187#[derive(Clone, Default, PartialEq, Eq)]
188pub struct Withheld(pub String);
189
190impl std::fmt::Debug for Withheld {
191    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
192        f.write_str("<withheld>")
193    }
194}
195
196impl Fed {
197    /// From the reply's `feed` table. Blank values are absent. A value the
198    /// installer does not feed itself may reach this box as a VM's OFFER,
199    /// fed onward as `offer.<vm>.<key>` (edda nordisk-pairing 0.3.1; since
200    /// 0.4 the front's keys — monetize's verifying key, the clock key — come
201    /// that way): the plain key wins, then the first offer by VM name.
202    pub fn from_feed(feed: &std::collections::BTreeMap<String, String>) -> Fed {
203        let get = |k: &str| {
204            feed.get(k)
205                .map(|v| v.trim().to_string())
206                .filter(|v| !v.is_empty())
207                .or_else(|| nordisk_pairing::feedbus::offered(feed, k))
208        };
209        Fed {
210            monetize_pubkey: get(FEED_MONETIZE),
211            clock_key: get(FEED_CLOCK_KEY),
212            clock_peer: get(FEED_CLOCK_PEER),
213            sso_private_idp_cidrs: get(FEED_SSO_PRIVATE_IDP_CIDRS),
214            sso_egress: get(FEED_SSO_EGRESS).as_deref() == Some("on"),
215            sso_dns_resolver: get(FEED_SSO_DNS_RESOLVER),
216            sells_room: get(FEED_SELLS_ROOM).as_deref() == Some("true"),
217            fleet: get(FEED_FLEET),
218            fleet_instance: get(FEED_FLEET_INSTANCE),
219            fleet_at: get(FEED_FLEET_AT),
220            fleet_ca: get(FEED_FLEET_CA),
221            smtp_url: get(FEED_SMTP_URL).map(Withheld),
222            smtp_from: get(FEED_SMTP_FROM),
223            smtp_ca: get(FEED_SMTP_CA),
224            billing_url: get(FEED_BILLING_URL),
225            repair_auto: get(FEED_REPAIR_AUTO).as_deref() == Some("true"),
226            repair_key: get(FEED_REPAIR_KEY).map(Withheld),
227            egress: get(FEED_EGRESS).filter(|d| d.starts_with("http://")),
228        }
229    }
230
231    pub fn is_empty(&self) -> bool {
232        self == &Fed::default()
233    }
234
235    /// `<data>/pairing/joined.toml`, if the loop has persisted one. Absent is
236    /// `None`; present and unreadable is an error — a persisted answer that
237    /// lies must not be quietly ignored.
238    pub fn read(root: &Path) -> anyhow::Result<Option<Fed>> {
239        Ok(kept_feed(root)?.as_ref().map(Fed::from_feed))
240    }
241
242    /// **The typed view of the feed in force** on the shared bus
243    /// (nordisk-pairing [`feedbus::current`]); `None` before anything was published.
244    pub fn current() -> Option<Fed> {
245        feedbus::current().map(|feed| Fed::from_feed(&feed))
246    }
247}
248
249/// **This box leaves through the door it was fed** ([`FEED_EGRESS`]): kept process-wide
250/// (`nordisk_pairing::egress::set_proxy`, the first wins: the door is the front's overlay
251/// address, the same for the life of the estate) for every client that reaches the internet.
252/// Called with each feed published (`serve`); a feed with no door changes nothing.
253pub fn take_egress(feed: &std::collections::BTreeMap<String, String>) {
254    if let Some(door) = Fed::from_feed(feed).egress {
255        if nordisk_pairing::egress::proxy().is_none() {
256            tracing::info!(target: "gunnar::audit", event = "egress_door", door = %door, "this box leaves through its front's egress door (no public IPv4 of its own)");
257        }
258        nordisk_pairing::egress::set_proxy(&door);
259    }
260}
261
262/// The proxy a ureq client of this box is built with: the fed door ([`take_egress`]), or none.
263#[cfg(any(feature = "mirror-http", feature = "sso-egress"))]
264pub fn ureq_proxy() -> Option<ureq::Proxy> {
265    nordisk_pairing::egress::proxy().and_then(|d| ureq::Proxy::new(d).ok())
266}
267
268/// The feed of `<data>/pairing/joined.toml`, as the door fed it ([`Fed::read`]).
269#[cfg(feature = "wg-client")]
270pub fn kept_feed(root: &Path) -> anyhow::Result<Option<Feed>> {
271    let p = root.join(crate::pairing::JOINED_REL);
272    let Some(text) = crate::fed_at_rest::read_checked(root, crate::pairing::JOINED_REL)? else {
273        return Ok(None);
274    };
275    let reply = nordisk_pairing::join::JoinReply::parse(&text)
276        .map_err(|e| anyhow::anyhow!("{}: {e}", p.display()))?;
277    Ok(Some(reply.feed))
278}
279
280/// A build without the pairing loop has no door to be fed by.
281#[cfg(not(feature = "wg-client"))]
282pub fn kept_feed(_root: &Path) -> anyhow::Result<Option<Feed>> {
283    Ok(None)
284}
285
286/// **The operator's internal networks a company IdP may live on**, as `fed`
287/// names them: none when nothing is fed. A list gunnar refuses (a public or
288/// loopback range) is an error, said by the caller and applied as none.
289pub fn sso_private_nets(fed: Option<&Fed>) -> Result<gunnar_grpc::sso::PrivateNets, String> {
290    match fed.and_then(|f| f.sso_private_idp_cidrs.as_deref()) {
291        Some(text) => gunnar_grpc::sso::PrivateNets::parse(text)
292            .map_err(|why| format!("`{FEED_SSO_PRIVATE_IDP_CIDRS}`: {why}")),
293        None => Ok(gunnar_grpc::sso::PrivateNets::default()),
294    }
295}
296
297/// **Is company SSO's network on?** The feed's [`FEED_SSO_EGRESS`] (`on`), or, for a box
298/// that is fed nothing, the operator's `GUNNAR_SSO_EGRESS=on` (`env`). Off otherwise.
299pub fn sso_egress_on(fed: Option<&Fed>, env: Option<&str>) -> bool {
300    fed.is_some_and(|f| f.sso_egress) || env.is_some_and(|v| v.trim() == "on")
301}
302
303/// **The resolver a domain's DNS proof is read from** (A34-5): the feed's
304/// [`FEED_SSO_DNS_RESOLVER`], or, where the feed names none, the operator's
305/// `GUNNAR_SSO_DNS_RESOLVER` (`env`), as [`sso_egress_on`] takes `GUNNAR_SSO_EGRESS`.
306/// `None`: no resolver is named, and no proof is read (RA5-24: none is a default).
307pub fn sso_dns_resolver(fed: Option<&Fed>, env: Option<&str>) -> Option<String> {
308    fed.and_then(|f| f.sso_dns_resolver.clone()).or_else(|| {
309        env.map(str::trim)
310            .filter(|v| !v.is_empty())
311            .map(str::to_owned)
312    })
313}
314
315/// Read `joined.toml` under `root` and publish its feed on the shared bus
316/// ([`feedbus::publish`]). Called once at start; an unreadable file is logged
317/// and treated as no feed (the loop re-joins and rewrites it).
318pub fn load_at_start(root: &Path) -> Option<Fed> {
319    match kept_feed(root) {
320        Ok(Some(feed)) => {
321            let fed = Fed::from_feed(&feed);
322            if !fed.is_empty() {
323                tracing::info!(monetize = fed.monetize_pubkey.is_some(), clock_peer = ?fed.clock_peer, clock_key = fed.clock_key.is_some(), "feed: the installer's values from {}", "<data>/pairing/joined.toml");
324            }
325            feedbus::publish(feed);
326            Some(fed)
327        }
328        Ok(None) => None,
329        Err(e) => {
330            tracing::error!(error = %format!("{e:#}"), "feed: the persisted join reply is unreadable — this start uses the medium's values only");
331            None
332        }
333    }
334}
335
336// ── the monetize key ────────────────────────────────────────────────────────
337
338/// The key the running server enforces with: recorded at start (outer `None`
339/// = nothing recorded, e.g. a test), and moved ONCE more when an awaited key
340/// arrives and is applied live ([`monetize_arrived`]).
341fn in_force_slot() -> &'static RwLock<Option<Option<[u8; 32]>>> {
342    static IN_FORCE: OnceLock<RwLock<Option<Option<[u8; 32]>>>> = OnceLock::new();
343    IN_FORCE.get_or_init(|| RwLock::new(None))
344}
345
346pub fn record_monetize_in_force(key: Option<[u8; 32]>) {
347    if let Ok(mut s) = in_force_slot().write() {
348        *s = Some(key);
349    }
350}
351
352pub fn monetize_in_force() -> Option<[u8; 32]> {
353    in_force_slot().read().ok().and_then(|s| s.flatten())
354}
355
356/// Something that takes the monetize key LIVE: the entitlement slot (both
357/// front doors' plan gate and `Entitlement.Set` read it) and the growth
358/// go-ahead policy. Registered by `serve` once each is built.
359pub type MonetizeTaker = Box<dyn Fn(&[u8; 32]) -> Result<(), String> + Send + Sync>;
360
361fn takers() -> &'static Mutex<Vec<(&'static str, MonetizeTaker)>> {
362    static TAKERS: OnceLock<Mutex<Vec<(&'static str, MonetizeTaker)>>> = OnceLock::new();
363    TAKERS.get_or_init(|| Mutex::new(Vec::new()))
364}
365
366/// **Register a taker of the monetize key** (owner 2026-09-25: avoid
367/// restarts) — called by `serve` for the entitlement slot and the go-ahead
368/// policy it built keyless, so a key the installer feeds later is applied in
369/// place by [`monetize_arrived`].
370pub fn on_monetize_key(name: &'static str, take: MonetizeTaker) {
371    if let Ok(mut t) = takers().lock() {
372        t.push((name, take));
373    }
374}
375
376fn hex(k: &[u8; 32]) -> String {
377    k.iter().map(|b| format!("{b:02x}")).collect()
378}
379
380/// **The monetize key at start.** `medium` is the resolved key off
381/// `--monetize-pubkey`/`GUNNAR_MONETIZE_PUBKEY`; `fed` is the door's spelling
382/// (64 hex). A fed value that is not a key, or that differs from the
383/// medium's, is refused by name.
384pub fn resolve_monetize(
385    medium: Option<[u8; 32]>,
386    fed: Option<&str>,
387) -> Result<Option<[u8; 32]>, String> {
388    let fed = match fed {
389        None => return Ok(medium),
390        Some(text) => gunnar_common::monetize_key::parse_hex32(text.trim()).map_err(|why| {
391            format!("REFUSED [pairing-feed-monetize-malformed] the installer fed `{FEED_MONETIZE}` = `{text}`, which is not a monetize key: {why}")
392        })?,
393    };
394    match medium {
395        None => Ok(Some(fed)),
396        Some(m) if m == fed => Ok(Some(m)),
397        Some(m) => Err(format!(
398            "REFUSED [{FEED_MISMATCH}] the medium trusts monetize key {} and the installer's door fed {} — \
399             this box will not pick one silently. Compose the medium without `monetize_pubkey` (the door \
400             feeds it), or with the front's live key",
401            hex(&m),
402            hex(&fed)
403        )),
404    }
405}
406
407/// Why a fed monetize key found no taker, in the words THIS build can say.
408///
409/// The metered build names the flag that opens the slot. The open build has no
410/// such flag (`--billing-url` is declared in `entitlement.rs`, which is
411/// `metered`-only) and must not compile the word into anything an operator can
412/// read — `tests/open_build_speaks_no_money.rs::the_open_sources_say_no_money`,
413/// the owner's ruling of 2026-09-15. Before this split the open build carried
414/// the metered sentence verbatim and the guard was red on it (feed.rs:243).
415#[cfg(feature = "metered")]
416const NO_TAKER_WHY: &str =
417    "the metered start opened no entitlement slot (no --billing-url on a `fed` medium)";
418#[cfg(not(feature = "metered"))]
419const NO_TAKER_WHY: &str =
420    "this build carries no entitlement module, so no start opens a slot to take it";
421
422/// **The fed monetize key arriving while running** (the pairing loop's
423/// answer, or vm1's offer carried by the installer): APPLIED IN PLACE by every
424/// registered taker ([`on_monetize_key`]), with no restart (owner 2026-09-25).
425/// `None` = nothing to do (no key fed, the same key already in force, or a
426/// process that recorded none — a community build or a test).
427pub fn monetize_arrived(fed: Option<&str>) -> Option<MonetizeArrival> {
428    let in_force = in_force_slot().read().ok().and_then(|s| *s)?;
429    let fed = fed?;
430    match resolve_monetize(in_force, Some(fed)) {
431        Ok(Some(k)) if Some(k) == in_force => None,
432        Ok(Some(k)) => {
433            let takers = takers().lock().ok()?;
434            if takers.is_empty() {
435                let detail = format!(
436                    "the installer fed monetize key {} and nothing on this box can enforce it: {NO_TAKER_WHY}",
437                    hex(&k)
438                );
439                raise(FEED_NOT_APPLIED, &detail);
440                return Some(MonetizeArrival::NotApplied(detail));
441            }
442            let failed: Vec<String> = takers
443                .iter()
444                .filter_map(|(name, take)| take(&k).err().map(|e| format!("{name}: {e}")))
445                .collect();
446            if !failed.is_empty() {
447                let detail = format!(
448                    "the installer fed monetize key {}, and it could not be applied: {}",
449                    hex(&k),
450                    failed.join("; ")
451                );
452                raise(FEED_NOT_APPLIED, &detail);
453                return Some(MonetizeArrival::NotApplied(detail));
454            }
455            record_monetize_in_force(Some(k));
456            let detail = format!("the installer fed monetize key {}; it is IN FORCE now (applied in place, no restart)", hex(&k));
457            tracing::info!(event = "pairing.feed_applied", "feed: {detail}");
458            Some(MonetizeArrival::Applied(detail))
459        }
460        Ok(None) => None,
461        Err(why) => {
462            raise(FEED_MISMATCH, &why);
463            Some(MonetizeArrival::Mismatch(why))
464        }
465    }
466}
467
468#[derive(Debug, Clone, PartialEq, Eq)]
469pub enum MonetizeArrival {
470    /// The server started with no key; the fed one is enforced from now on.
471    Applied(String),
472    /// The server started with no key and cannot take the fed one: alarmed.
473    NotApplied(String),
474    /// The medium's key is in force and the door fed another: refused, alarmed.
475    Mismatch(String),
476}
477
478// ── the clock ───────────────────────────────────────────────────────────────
479
480/// **The clock's two values**, medium against door — edda estate-clock's
481/// (`offer::resolve_clock`, moved there from here AS IS; the refusal names
482/// [`FEED_MISMATCH`], the same word). `Ok((clock, key))` is what
483/// [`estate_clock::config::Config::parse`] is handed.
484pub use estate_clock::offer::resolve_clock;
485
486/// **The clock configuration in force now**: the process environment (the
487/// medium's `korp.env.ESTATE_CLOCK*`, or Gunnar's `korp.env.GUNNAR_CLOCK*`
488/// read as its alias, `estate_clock::config::var`) merged with the current
489/// feed. Read on EVERY sync, so a feed that arrives while running applies to
490/// the next one.
491pub fn clock_config() -> Result<estate_clock::config::Config, String> {
492    use estate_clock::config::{var, Config, Role, CLOCK_ENV, CLOCK_KEY_ENV};
493    let env_clock = var(CLOCK_ENV);
494    let env_key = var(CLOCK_KEY_ENV);
495    let fed = Fed::current().unwrap_or_default();
496    let (clock, key) = resolve_clock(
497        env_clock.as_deref(),
498        env_key.as_deref(),
499        fed.clock_peer.as_deref(),
500        fed.clock_key.as_deref(),
501    )?;
502    Config::parse(clock.as_deref(), key.as_deref(), Role::Appliance)
503}
504
505/// Whether the door fed a clock source.
506pub fn clock_fed() -> bool {
507    Fed::current().is_some_and(|f| f.clock_peer.is_some())
508}
509
510// ── alarms ──────────────────────────────────────────────────────────────────
511
512fn alarm_slot() -> &'static Mutex<Vec<gunnar_proto::HealthAlarm>> {
513    static ALARMS: OnceLock<Mutex<Vec<gunnar_proto::HealthAlarm>>> = OnceLock::new();
514    ALARMS.get_or_init(|| Mutex::new(Vec::new()))
515}
516
517/// Raise (or re-raise, counting) a feed alarm; said on the log too.
518pub fn raise(event: &str, detail: &str) {
519    tracing::error!(event, "feed: {detail}");
520    let now = std::time::SystemTime::now()
521        .duration_since(std::time::UNIX_EPOCH)
522        .map(|d| d.as_millis() as i64)
523        .unwrap_or(0);
524    if let Ok(mut alarms) = alarm_slot().lock() {
525        match alarms.iter_mut().find(|a| a.event == event) {
526            Some(a) => {
527                a.count += 1;
528                a.detail = detail.to_string();
529            }
530            None => alarms.push(gunnar_proto::HealthAlarm {
531                event: event.into(),
532                count: 1,
533                since_unix_ms: now,
534                detail: detail.to_string(),
535            }),
536        }
537    }
538}
539
540/// The feed alarms, for `Admin.Health`.
541pub fn alarms() -> Vec<gunnar_proto::HealthAlarm> {
542    alarm_slot().lock().map(|a| a.clone()).unwrap_or_default()
543}
544
545/// **The mock's seam, honoured only as the mock feeds it** (ST-19): a fed `fleet_at` is a
546/// bench forward — an address literal `ip:port` on a private, loopback, link-local or
547/// shared (100.64/10) network — and a fed `fleet_ca` comes only beside one. Anything else
548/// (a public address, a name, a CA with no bench address) is refused by name: a release
549/// box never dials elsewhere, nor re-roots its trust, on a feed's word.
550pub fn seam_refusal(at: Option<&str>, ca: bool) -> Option<String> {
551    const WORD: &str = "fleet-seam-bench-only";
552    match at {
553        None if ca => Some(format!(
554            "REFUSED [{WORD}] the installer fed `{}` with no `{}`: a door's own CA is a bench's, beside a bench address only — a public door is checked under the public roots",
555            crate::feed::FEED_FLEET_CA,
556            crate::feed::FEED_FLEET_AT
557        )),
558        None => None,
559        Some(at) => match at.trim().parse::<std::net::SocketAddr>() {
560            Ok(a) if bench_address(a.ip()) => None,
561            _ => Some(format!(
562                "REFUSED [{WORD}] the installer fed `{}` = {at}: the pull channel is dialled elsewhere than its door's name only on a bench (a private, loopback or link-local `ip:port`)",
563                crate::feed::FEED_FLEET_AT
564            )),
565        },
566    }
567}
568
569/// A bench guest's network: private, loopback, link-local, or shared address space.
570fn bench_address(ip: std::net::IpAddr) -> bool {
571    match ip {
572        std::net::IpAddr::V4(v) => {
573            v.is_private()
574                || v.is_loopback()
575                || v.is_link_local()
576                || (v.octets()[0] == 100 && (v.octets()[1] & 0xc0) == 64)
577        }
578        std::net::IpAddr::V6(v) => {
579            v.is_loopback()
580                || (v.segments()[0] & 0xfe00) == 0xfc00
581                || (v.segments()[0] & 0xffc0) == 0xfe80
582        }
583    }
584}
585
586#[cfg(test)]
587mod tests {
588    use super::*;
589
590    /// ★ **P1: the relay a sold estate's main mails through comes in the feed** — read from
591    /// `secret.smtp_url` / `secret.smtp_from`; the URL (it carries the token) never shows.
592    #[test]
593    fn the_fed_relay_is_read_and_its_url_withheld() {
594        let feed: std::collections::BTreeMap<String, String> = [
595            (FEED_SMTP_URL, "smtp://u:TOKENvalue@smtp.example:587"),
596            (FEED_SMTP_FROM, "u@smtp.example"),
597        ]
598        .iter()
599        .map(|(k, v)| (k.to_string(), v.to_string()))
600        .collect();
601        let f = Fed::from_feed(&feed);
602        assert_eq!(
603            f.smtp_url,
604            Some(Withheld("smtp://u:TOKENvalue@smtp.example:587".into()))
605        );
606        assert_eq!(f.smtp_from.as_deref(), Some("u@smtp.example"));
607        assert!(!format!("{f:?}").contains("TOKENvalue"), "{f:?}");
608        assert!(Fed::from_feed(&Default::default()).smtp_url.is_none());
609    }
610
611    /// ★ **The front's egress door a box is fed** (the lean estate, A19 rows 10-14): read from
612    /// `egress` (Skidbladnir nordisk-pairing's key), an `http://` door only.
613    #[test]
614    fn the_fed_egress_door_is_read_and_only_an_http_one() {
615        let feed = |pairs: &[(&str, &str)]| {
616            pairs
617                .iter()
618                .map(|(k, v)| (k.to_string(), v.to_string()))
619                .collect::<std::collections::BTreeMap<_, _>>()
620        };
621        assert_eq!(FEED_EGRESS, nordisk_pairing::egress::FEED_EGRESS);
622        assert_eq!(
623            Fed::from_feed(&feed(&[(FEED_EGRESS, " http://10.44.0.1:3128 ")]))
624                .egress
625                .as_deref(),
626            Some("http://10.44.0.1:3128")
627        );
628        assert_eq!(
629            Fed::from_feed(&feed(&[(FEED_EGRESS, "socks5://10.44.0.1:1080")])).egress,
630            None,
631            "only an http:// door is one"
632        );
633        assert_eq!(
634            Fed::from_feed(&feed(&[])).egress,
635            None,
636            "a box with its own way out is fed none"
637        );
638    }
639
640    const A: &str = "43cb00000000000000000000000000000000000000000000000000000000842a";
641    const B: &str = "43cb00000000000000000000000000000000000000000000000000000000842b";
642
643    fn k(s: &str) -> [u8; 32] {
644        gunnar_common::monetize_key::parse_hex32(s).unwrap()
645    }
646
647    /// Since 0.4 the front's keys reach the boxes as vm1's offer; the plain
648    /// key, when the installer feeds one, still wins.
649    #[test]
650    fn a_value_offered_by_a_vm_is_taken_when_the_installer_feeds_none_itself() {
651        let feed = |pairs: &[(&str, &str)]| {
652            pairs
653                .iter()
654                .map(|(k, v)| (k.to_string(), v.to_string()))
655                .collect::<std::collections::BTreeMap<_, _>>()
656        };
657        let f = Fed::from_feed(&feed(&[
658            ("offer.front.monetize_pubkey", A),
659            ("offer.front.clock_key", "ssh-ed25519 AAAAk"),
660            ("clock_peer", "peer://10.13.8.1:50123"),
661            ("offer.front.xmonetize_pubkey", B),
662        ]));
663        assert_eq!(
664            f,
665            Fed {
666                monetize_pubkey: Some(A.into()),
667                clock_key: Some("ssh-ed25519 AAAAk".into()),
668                clock_peer: Some("peer://10.13.8.1:50123".into()),
669                ..Fed::default()
670            }
671        );
672        assert_eq!(
673            Fed::from_feed(&feed(&[
674                (FEED_MONETIZE, B),
675                ("offer.front.monetize_pubkey", A)
676            ]))
677            .monetize_pubkey
678            .as_deref(),
679            Some(B),
680            "the plain key wins"
681        );
682        assert_eq!(
683            nordisk_pairing::feedbus::offered(
684                &feed(&[
685                    ("offer..monetize_pubkey", A),
686                    ("offer.a.b.monetize_pubkey", A)
687                ]),
688                FEED_MONETIZE
689            ),
690            None,
691            "not a VM name"
692        );
693    }
694
695    /// ONE monetize (M-D35): shop-pairing feeds the front `fleet` and `fleet_instance`; the
696    /// front offers them, and every box takes them — the plain key first, then the offer.
697    #[test]
698    fn the_fleet_door_and_the_estate_are_taken_from_the_feed_or_the_fronts_offer() {
699        let feed = |pairs: &[(&str, &str)]| {
700            pairs
701                .iter()
702                .map(|(k, v)| (k.to_string(), v.to_string()))
703                .collect::<std::collections::BTreeMap<_, _>>()
704        };
705        let f = Fed::from_feed(&feed(&[
706            ("offer.front.fleet", "https://monetize.ignalina.dk:8443"),
707            ("offer.front.fleet_instance", "acme"),
708            ("offer.front.monetize_pubkey", A),
709        ]));
710        assert_eq!(
711            (
712                f.fleet.as_deref(),
713                f.fleet_instance.as_deref(),
714                f.monetize_pubkey.as_deref()
715            ),
716            (
717                Some("https://monetize.ignalina.dk:8443"),
718                Some("acme"),
719                Some(A)
720            )
721        );
722        let f = Fed::from_feed(&feed(&[
723            (FEED_FLEET, "https://one:8443"),
724            ("offer.front.fleet", "https://other:8443"),
725            (FEED_FLEET_INSTANCE, " "),
726            ("offer.front.fleet_instance", "acme"),
727        ]));
728        assert_eq!(
729            (f.fleet.as_deref(), f.fleet_instance.as_deref()),
730            (Some("https://one:8443"), Some("acme")),
731            "the plain key wins; blank is absent"
732        );
733        assert!(Fed::from_feed(&feed(&[])).fleet.is_none());
734    }
735
736    /// ★ **`sells_room` is the installer's word, exactly `true`** (2026-10-08): fed on a
737    /// selling shop's boxes, absent on a sold estate; anything else is false.
738    #[test]
739    fn sells_room_is_fed_as_exactly_true() {
740        let feed = |pairs: &[(&str, &str)]| {
741            pairs
742                .iter()
743                .map(|(k, v)| (k.to_string(), v.to_string()))
744                .collect::<std::collections::BTreeMap<_, _>>()
745        };
746        assert!(Fed::from_feed(&feed(&[(FEED_SELLS_ROOM, "true")])).sells_room);
747        assert!(
748            Fed::from_feed(&feed(&[(FEED_SELLS_ROOM, " true\n")])).sells_room,
749            "trimmed, as fed"
750        );
751        assert!(!Fed::from_feed(&feed(&[(FEED_SELLS_ROOM, "yes")])).sells_room);
752        assert!(
753            !Fed::from_feed(&feed(&[])).sells_room,
754            "a sold estate is fed nothing"
755        );
756    }
757
758    /// ★ UW-6: **the site's pay page is the installer's word** (`refusal_link`), trimmed;
759    /// blank or absent is none (the medium's page stays).
760    #[test]
761    fn the_sites_pay_page_is_read_from_the_feed() {
762        let feed = |pairs: &[(&str, &str)]| {
763            pairs
764                .iter()
765                .map(|(k, v)| (k.to_string(), v.to_string()))
766                .collect::<std::collections::BTreeMap<_, _>>()
767        };
768        assert_eq!(FEED_BILLING_URL, "refusal_link");
769        assert_eq!(
770            Fed::from_feed(&feed(&[(
771                FEED_BILLING_URL,
772                " https://test2.gunnar.rs/billing/{tenant}\n"
773            )]))
774            .billing_url
775            .as_deref(),
776            Some("https://test2.gunnar.rs/billing/{tenant}")
777        );
778        assert_eq!(
779            Fed::from_feed(&feed(&[(FEED_BILLING_URL, " ")])).billing_url,
780            None
781        );
782        assert_eq!(Fed::from_feed(&feed(&[])).billing_url, None);
783    }
784
785    /// ★ **Company SSO's network is the operator's to turn on** (A34-16): exactly `on` in
786    /// the feed, or `GUNNAR_SSO_EGRESS=on` on a box fed nothing; absent, blank or anything
787    /// else is off (airgap).
788    #[test]
789    fn sso_egress_is_off_unless_the_operator_turns_it_on() {
790        let feed = |pairs: &[(&str, &str)]| {
791            pairs
792                .iter()
793                .map(|(k, v)| (k.to_string(), v.to_string()))
794                .collect::<std::collections::BTreeMap<_, _>>()
795        };
796        let on = Fed::from_feed(&feed(&[(FEED_SSO_EGRESS, " on ")]));
797        assert!(on.sso_egress);
798        assert!(sso_egress_on(Some(&on), None));
799        for word in ["off", "true", "yes", "ON?", ""] {
800            let fed = Fed::from_feed(&feed(&[(FEED_SSO_EGRESS, word)]));
801            assert!(!sso_egress_on(Some(&fed), None), "{word:?} turned it on");
802        }
803        assert!(!sso_egress_on(None, None), "nothing said is airgap");
804        assert!(
805            sso_egress_on(None, Some("on")),
806            "a box fed nothing takes the operator's environment"
807        );
808        assert!(!sso_egress_on(None, Some("1")));
809        let off = Fed::from_feed(&feed(&[]));
810        assert!(
811            sso_egress_on(Some(&off), Some("on")),
812            "the environment holds when the feed says nothing"
813        );
814    }
815
816    /// ★ **The domain proof's resolver is the operator's to name** (A34-5, RA5-24): the
817    /// feed's `sso_dns_resolver`, or `GUNNAR_SSO_DNS_RESOLVER` on a box the feed names none
818    /// for (as `GUNNAR_SSO_EGRESS` for the switch; gunnar-ui's uitest proves a domain this
819    /// way, against its own loopback resolver); blank is none, and none is no proof.
820    #[test]
821    fn the_domain_proofs_resolver_is_fed_or_the_operators_environment() {
822        let feed = |pairs: &[(&str, &str)]| {
823            pairs
824                .iter()
825                .map(|(k, v)| (k.to_string(), v.to_string()))
826                .collect::<std::collections::BTreeMap<_, _>>()
827        };
828        let fed = Fed::from_feed(&feed(&[(
829            FEED_SSO_DNS_RESOLVER,
830            "https://doh.example/resolve",
831        )]));
832        assert_eq!(
833            sso_dns_resolver(Some(&fed), None).as_deref(),
834            Some("https://doh.example/resolve")
835        );
836        assert_eq!(
837            sso_dns_resolver(Some(&fed), Some("http://127.0.0.1:9/resolve")).as_deref(),
838            Some("https://doh.example/resolve"),
839            "the feed's resolver wins over the environment"
840        );
841        let none = Fed::from_feed(&feed(&[]));
842        assert_eq!(
843            sso_dns_resolver(Some(&none), Some(" http://127.0.0.1:9/resolve ")).as_deref(),
844            Some("http://127.0.0.1:9/resolve")
845        );
846        assert_eq!(
847            sso_dns_resolver(None, Some("http://127.0.0.1:9/resolve")).as_deref(),
848            Some("http://127.0.0.1:9/resolve"),
849            "a box fed nothing"
850        );
851        assert_eq!(sso_dns_resolver(None, Some("  ")), None, "blank is none");
852        assert_eq!(
853            sso_dns_resolver(None, None),
854            None,
855            "nothing named is no resolver (RA5-24)"
856        );
857    }
858
859    /// ★ **The operator's internal IdP networks are the installer's feed**
860    /// (owner 2026-10-06): read from the feed, applied to every taker on
861    /// every publish (live), parsed by gunnar's own rule — a public or
862    /// loopback range is refused, nothing fed is none.
863    #[test]
864    fn the_operators_idp_networks_are_fed_and_applied_live() {
865        let feed = |pairs: &[(&str, &str)]| {
866            pairs
867                .iter()
868                .map(|(k, v)| (k.to_string(), v.to_string()))
869                .collect::<std::collections::BTreeMap<_, _>>()
870        };
871        let fed = Fed::from_feed(&feed(&[(FEED_SSO_PRIVATE_IDP_CIDRS, " 10.0.2.100/32 ")]));
872        assert_eq!(fed.sso_private_idp_cidrs.as_deref(), Some("10.0.2.100/32"));
873        assert_eq!(
874            sso_private_nets(Some(&fed))
875                .expect("a private network")
876                .to_string(),
877            "10.0.2.100/32"
878        );
879        assert!(sso_private_nets(None).expect("nothing fed").is_empty());
880        let public = Fed::from_feed(&feed(&[(FEED_SSO_PRIVATE_IDP_CIDRS, "8.8.8.0/24")]));
881        assert!(
882            sso_private_nets(Some(&public)).is_err(),
883            "a public range is refused"
884        );
885        let seen = std::sync::Arc::new(Mutex::new(Vec::<String>::new()));
886        let into = std::sync::Arc::clone(&seen);
887        feedbus::on_published("test: the idp networks", move |f: &Feed| {
888            into.lock().unwrap().push(
889                Fed::from_feed(f)
890                    .sso_private_idp_cidrs
891                    .clone()
892                    .unwrap_or_default(),
893            )
894        });
895        feedbus::publish(feed(&[(FEED_SSO_PRIVATE_IDP_CIDRS, " 10.0.2.100/32 ")]));
896        feedbus::publish(feed(&[]));
897        let got = seen.lock().unwrap().clone();
898        let at = got
899            .iter()
900            .position(|v| v == "10.0.2.100/32")
901            .expect("the list reached the taker");
902        assert!(
903            got[at + 1..].iter().any(String::is_empty),
904            "every publish reaches the taker: {got:?}"
905        );
906    }
907
908    /// **A role-bound offer is read from the VM that holds the role**: with the
909    /// twin sorting first, "the first by name" is the twin's stale key.
910    #[test]
911    fn an_offer_is_read_from_the_named_vm_not_the_first_by_name() {
912        let feed: std::collections::BTreeMap<String, String> = [
913            ("offer.vm2.ticket_pubkey", A),
914            ("offer.vm3.ticket_pubkey", B),
915            ("offer.vm4.ticket_pubkey", " "),
916        ]
917        .iter()
918        .map(|(k, v)| (k.to_string(), v.to_string()))
919        .collect();
920        assert_eq!(
921            nordisk_pairing::feedbus::offered(&feed, "ticket_pubkey").as_deref(),
922            Some(A),
923            "the first by name"
924        );
925        assert_eq!(
926            offered_by(&feed, "vm3", "ticket_pubkey"),
927            Some(B),
928            "the primary's own"
929        );
930        assert_eq!(
931            offered_by(&feed, "vm4", "ticket_pubkey"),
932            None,
933            "blank is absent"
934        );
935        assert_eq!(offered_by(&feed, "vm9", "ticket_pubkey"), None);
936    }
937
938    #[test]
939    fn monetize_door_alone_medium_alone_and_equal_are_taken() {
940        assert_eq!(
941            resolve_monetize(None, Some(A)),
942            Ok(Some(k(A))),
943            "one medium, any estate: the door's key"
944        );
945        assert_eq!(
946            resolve_monetize(Some(k(A)), None),
947            Ok(Some(k(A))),
948            "no feed: today's behaviour"
949        );
950        assert_eq!(resolve_monetize(Some(k(A)), Some(A)), Ok(Some(k(A))));
951        assert_eq!(
952            resolve_monetize(None, None),
953            Ok(None),
954            "a metered build with no key from either source: today's behaviour"
955        );
956    }
957
958    #[test]
959    fn monetize_medium_and_a_different_door_key_is_refused_by_name() {
960        let why = resolve_monetize(Some(k(A)), Some(B)).unwrap_err();
961        assert!(
962            why.contains(&format!("[{FEED_MISMATCH}]")) && why.contains(A) && why.contains(B),
963            "{why}"
964        );
965        let why = resolve_monetize(None, Some("not-a-key")).unwrap_err();
966        assert!(why.contains("[pairing-feed-monetize-malformed]"), "{why}");
967    }
968
969    #[test]
970    fn the_feed_table_reads_the_contract_keys_and_blank_is_absent() {
971        let mut feed = std::collections::BTreeMap::new();
972        feed.insert(FEED_MONETIZE.to_string(), A.to_string());
973        feed.insert(
974            FEED_CLOCK_PEER.to_string(),
975            "peer://10.13.8.179:50123".to_string(),
976        );
977        feed.insert(FEED_CLOCK_KEY.to_string(), "  ".to_string());
978        feed.insert(
979            "admin_pubkey".to_string(),
980            "ssh-ed25519 AAAAconsole".to_string(),
981        );
982        let fed = Fed::from_feed(&feed);
983        assert_eq!(
984            fed,
985            Fed {
986                monetize_pubkey: Some(A.into()),
987                clock_key: None,
988                clock_peer: Some("peer://10.13.8.179:50123".into()),
989                ..Fed::default()
990            }
991        );
992    }
993
994    #[cfg(feature = "wg-client")]
995    #[test]
996    fn a_persisted_join_reply_is_read_back_and_absent_is_none() {
997        let dir = std::env::temp_dir().join(format!("gunnar-feed-{}", std::process::id()));
998        let _ = std::fs::create_dir_all(dir.join("pairing"));
999        assert_eq!(Fed::read(&dir.join("nowhere")).unwrap(), None);
1000        let mut feed = std::collections::BTreeMap::new();
1001        feed.insert(FEED_MONETIZE.to_string(), A.to_string());
1002        feed.insert(FEED_CLOCK_KEY.to_string(), "ssh-ed25519 AAAAk".to_string());
1003        let reply = nordisk_pairing::join::JoinReply {
1004            estate: "gunnar.rs".into(),
1005            me: nordisk_pairing::join::Assigned {
1006                name: "main".into(),
1007                overlay: nordisk_pairing::OVERLAY_BOX_A,
1008            },
1009            cert_pem: None,
1010            role: None,
1011            primary_overlay: None,
1012            primary_utility: None,
1013            peers: vec![],
1014            feed,
1015        };
1016        std::fs::write(
1017            dir.join(crate::pairing::JOINED_REL),
1018            reply.render().unwrap(),
1019        )
1020        .unwrap();
1021        let fed = Fed::read(&dir).unwrap().unwrap();
1022        assert_eq!(fed.monetize_pubkey.as_deref(), Some(A));
1023        assert_eq!(fed.clock_key.as_deref(), Some("ssh-ed25519 AAAAk"));
1024        let _ = std::fs::remove_dir_all(&dir);
1025    }
1026}