Skip to main content

cloud/reconciler/
domain.rs

1//! Domain-level reconciler — `.yah/domains/*.toml` → live Cloudflare state.
2//!
3//! Today's only shape is the **R2 custom-domain** binding: a domain that
4//! declares `front_door = "bucket-direct"` (R594-F12) and names an R2 bucket
5//! in `cdn_bucket`. Cloudflare's R2 Custom Domains API binds the hostname to
6//! the bucket and writes the CNAME into the parent zone automatically when
7//! the zone lives on the same account — no DNS-side call is needed here.
8//!
9//! Domains with `front_door = "worker"` (e.g. `app-yah-dev.toml`) are out of
10//! scope for this shape; they get DNS + route management through the Worker
11//! reconciler below. `front_door = "passway"` is the sovereign-ingress path
12//! (W267) and is reconciled by [`ensure_passway_apex`] — R859-F1 — which
13//! renders the apex A record set from the domain manifest plus the workspace
14//! ingress collation, DNS-only, through the provider-agnostic `dns.*` envoy
15//! verbs.
16//!
17//! Before R594-F12 the shape was *inferred* from the absence of `[[routes]]`,
18//! which meant a route-carrying manifest bound straight to R2 was accepted
19//! and silently skipped by this pass. The discriminator is now declared and
20//! validated at load.
21//!
22//! @yah:relay(R859, "Sovereign-edge automation ring: DNS reconciled from declared intent, public addressing follows placement (W267 audit followups)")
23//! @yah:at(2026-09-04T19:06:24Z)
24//! @yah:status(handoff)
25//! @yah:assignee(agent:user-custom-char-gul2)
26//! @arch:see(.yah/docs/working/W267-sovereign-public-ingress.md)
27//! @yah:next("Filed from the 2026-09-04 HA/ingress audit (chat session, operator-reviewed). The grey door is live but the automation ring around it is manual: DNS flips ride scripts/cf-apex-mode.sh by hand, and node loss leaves a dead A record taking ~half the round-robin traffic until a human edits DNS.")
28//! @yah:next("Order: F1 (DNS reconciler) before F2 (floating-IP wiring) — F2's DNS-withdrawal half needs F1's record-rendering to exist. Sibling track R844-F23 (poll-N door discovery) is independent and filed under R844 where door discovery lives.")
29//! @yah:next("Deliberately out of scope here: replica layer (W284/R626), kamaji healthcheck execution, generic appliance backup/migrate (R742-F3), external synthetic prober — each is its own track; this relay is only 'public addressing follows declared intent'.")
30
31use std::path::Path;
32
33use anyhow::{Context, Result};
34use tracing::{debug, info, warn};
35
36use crate::CloudflareClient;
37
38/// Bind `domain` as a custom domain on `bucket_name`. Idempotent — does
39/// nothing when the binding is already present, regardless of `enabled`
40/// state (CF reflects newly-added bindings as `enabled: true` immediately;
41/// disabling is an explicit dashboard action we don't undo here).
42///
43/// Resolves account_id + the Cloudflare API token from the named provider
44/// (`.yah/infra/providers/<provider_id>.toml`, see
45/// [`super::cf_creds::CfProvider`]). Mirrors the list-first pattern of
46/// `static_asset::ensure_r2_bucket` so the apply loop is safe to re-run.
47///
48/// Required token scopes: `Workers R2 Storage: Edit` and `Zone: Read`
49/// (the latter because the API requires the zone id of the parent zone —
50/// CF writes the CNAME there). The caller does NOT need `DNS: Edit`; CF
51/// provisions the record itself when the binding is created.
52pub async fn ensure_r2_custom_domain(
53    workspace_root: &Path,
54    provider_id: &str,
55    bucket_name: &str,
56    domain: &str,
57) -> Result<()> {
58    let cf_provider = super::cf_creds::CfProvider::resolve(workspace_root, provider_id)?;
59    let account_id = cf_provider.account_id.clone();
60    let cf = CloudflareClient::new(cf_provider.api_token()?);
61    let existing = cf
62        .list_r2_custom_domains(&account_id, bucket_name)
63        .await
64        .with_context(|| format!("listing R2 custom domains on bucket {bucket_name:?}"))?;
65    if existing.iter().any(|d| d.domain == domain) {
66        debug!(
67            domain,
68            bucket_name, "R2 custom domain already bound — skipping"
69        );
70        return Ok(());
71    }
72    let zone_name = parent_zone_name(domain);
73    let zone_id = cf
74        .zone_id_for_name(zone_name)
75        .await
76        .with_context(|| format!("resolving zone id for {zone_name:?}"))?;
77    cf.add_r2_custom_domain(&account_id, bucket_name, domain, &zone_id)
78        .await
79        .with_context(|| format!("binding R2 custom domain {domain:?} → bucket {bucket_name:?}"))?;
80    info!(domain, bucket_name, zone_name, "R2 custom domain bound");
81    Ok(())
82}
83
84/// Heuristic: the parent CF zone of `domain` is its last two labels.
85///
86/// `cdn.yah.dev` → `yah.dev`; `yah.dev` → `yah.dev` (already apex). This is
87/// correct for every yah-owned zone today (all are two-label apexes). If a
88/// future workspace registers a three-label zone (e.g. `staging.yah.dev`
89/// as its own CF zone) and binds a subdomain under it, this will resolve
90/// to the wrong zone — swap in a longest-suffix match against
91/// `list_zones()` when that day arrives.
92fn parent_zone_name(domain: &str) -> &str {
93    let last_dot = domain.rfind('.');
94    let Some(last_dot) = last_dot else {
95        return domain; // single-label — let CF surface the bad input
96    };
97    if let Some(prev_dot) = domain[..last_dot].rfind('.') {
98        &domain[prev_dot + 1..]
99    } else {
100        domain // already a two-label apex
101    }
102}
103
104// ─── R561-F3: domain-manifest-driven static Worker ──────────────────────────
105//
106// A per-tenant alias-tier manifest (e.g. scrabcake.net.yah.dev) carries a
107// `static` route → `<service>/<component>`. Serving it means deploying the
108// shared router bundle (`WORKER_SCRIPT`) configured to fetch the tenant's
109// assets from its R2 prefix, then binding the subdomain to that Worker via the
110// Workers Custom Domains API. This is the routed-domain shape the apply loop
111// currently Skips.
112
113use crate::config::{DomainConfig, DomainRoute, FrontDoor, RouteMode};
114use crate::provider::cloudflare::WorkerBinding;
115use crate::reconciler::mesofact_static::WORKER_SCRIPT;
116use std::collections::BTreeMap;
117
118/// The plan for deploying one subdomain's static Worker — everything decided
119/// before any Cloudflare call. Pure output of [`plan_domain_worker`], so the
120/// decision logic is unit-testable offline; [`deploy_domain_worker`] performs
121/// the I/O.
122#[derive(Debug, Clone, PartialEq, Eq)]
123pub struct DomainWorkerPlan {
124    /// Worker script name (the domain manifest's file stem / `name`).
125    pub worker_name: String,
126    /// Hostname to bind via Workers Custom Domains, e.g. `scrabcake.net.yah.dev`.
127    pub custom_domain: String,
128    /// `ASSET_ORIGIN` the Worker fetches from: `<cdn_base>/<service>/<env>`.
129    pub asset_origin: String,
130    /// `plain_text` Worker bindings (mirrors the Static-mode shape of
131    /// mesofact_static's `worker_config_bindings`).
132    pub bindings: Vec<(String, String)>,
133}
134
135/// Static-mode Worker bindings for an alias-tier subdomain. Kept in lockstep
136/// with `mesofact_static::worker_config_bindings(WorkerMode::Static, …)`.
137///
138/// `route_headers` is the manifest's own `DomainConfig::route_headers_json` —
139/// this planner already holds the domain, so unlike the mirror-driven path it
140/// needs no lookup to find it (R746).
141fn static_worker_bindings(asset_origin: &str, route_headers: String) -> Vec<(String, String)> {
142    vec![
143        ("ASSET_ORIGIN".to_string(), asset_origin.to_string()),
144        ("UPLOAD_ORIGIN".to_string(), String::new()),
145        ("WORKER_MODE".to_string(), "static".to_string()),
146        ("SSR_ORIGIN".to_string(), String::new()),
147        ("SSR_PREFIXES".to_string(), "[]".to_string()),
148        ("ROUTE_HEADERS".to_string(), route_headers),
149    ]
150}
151
152/// Build the [`DomainWorkerPlan`] for a per-tenant alias-tier manifest.
153///
154/// `cdn_base` is the tier's CDN origin (an R2 custom domain bound to the
155/// tier bucket, e.g. `https://cdn.net.yah.dev`); `env` is the mirror env the
156/// publisher wrote under (e.g. `cloud`). The publisher lays assets down at
157/// `<bucket>/<service>/<env>/<key>` and the Worker fetches
158/// `${ASSET_ORIGIN}/<key>`, so `ASSET_ORIGIN = <cdn_base>/<service>/<env>`.
159///
160/// Fails fast (before any network call) when the manifest has no `static`
161/// route or its component ref is malformed.
162pub fn plan_domain_worker(
163    domain: &DomainConfig,
164    cdn_base: &str,
165    env: &str,
166) -> Result<DomainWorkerPlan> {
167    // R594-F12: refuse to synthesize a Worker for a domain that declared a
168    // different front door. Without this the plan succeeds and deploys a
169    // Worker that never receives traffic (bucket-direct) or that duplicates
170    // the sovereign ingress (passway).
171    if domain.front_door != FrontDoor::Worker {
172        anyhow::bail!(
173            "domain {} ({}) declares front_door = \"{}\" — only \"worker\" \
174             domains get a generated Cloudflare Worker",
175            domain.name,
176            domain.domain,
177            domain.front_door.as_str()
178        );
179    }
180
181    // First static route wins (v1 alias tier serves one site per subdomain).
182    let component = domain
183        .routes
184        .iter()
185        .find_map(|r| match &r.mode {
186            RouteMode::Static { component } => Some(component.as_str()),
187            _ => None,
188        })
189        .with_context(|| {
190            format!(
191                "domain {} ({}) has no `static` route — nothing for a static Worker to serve",
192                domain.name, domain.domain
193            )
194        })?;
195
196    let service = component
197        .split_once('/')
198        .map(|(svc, _)| svc)
199        .with_context(|| {
200            format!(
201                "domain {}: route component {component:?} — expected \"<service>/<component-id>\"",
202                domain.name
203            )
204        })?;
205
206    let asset_origin = format!("{}/{}/{}", cdn_base.trim_end_matches('/'), service, env);
207
208    Ok(DomainWorkerPlan {
209        worker_name: domain.name.clone(),
210        custom_domain: domain.domain.clone(),
211        bindings: static_worker_bindings(&asset_origin, domain.route_headers_json()),
212        asset_origin,
213    })
214}
215
216/// Deploy the planned Worker and bind the subdomain (R561-F3, live I/O).
217///
218/// Reuses the shared `WORKER_SCRIPT` bundle + the existing CloudflareClient
219/// deploy/custom-domain methods. Gated on `cloudflare-api-token`. The decision
220/// logic is covered by `plan_domain_worker`'s tests; this I/O path has NOT been
221/// exercised against a live account yet (no creds in CI) — treat as untested
222/// until a live `yah cloud apply` confirms it.
223///
224/// CAVEAT (zone resolution): `parent_zone_name` uses a two-label heuristic, so
225/// for `scrabcake.net.yah.dev` it returns `yah.dev`. But the alias tiers
226/// `net.yah.dev` / `com.yah.dev` are their own Cloudflare zones — the binding
227/// must target `net.yah.dev`, not `yah.dev`. Before this goes live, swap in a
228/// longest-suffix match against the account's zones (the upgrade path
229/// `parent_zone_name`'s doc already names). Tracked on R561-F3.
230pub async fn deploy_domain_worker(
231    workspace_root: &Path,
232    provider_id: &str,
233    plan: &DomainWorkerPlan,
234) -> Result<()> {
235    let cf_provider = super::cf_creds::CfProvider::resolve(workspace_root, provider_id)?;
236    let account_id = cf_provider.account_id.clone();
237    let cf = CloudflareClient::new(cf_provider.api_token()?);
238
239    let worker_bindings: Vec<WorkerBinding<'_>> = plan
240        .bindings
241        .iter()
242        .map(|(k, v)| WorkerBinding::PlainText {
243            name: k.as_str(),
244            text: v.as_str(),
245        })
246        .collect();
247
248    cf.deploy_worker_script(
249        &account_id,
250        &plan.worker_name,
251        WORKER_SCRIPT,
252        &worker_bindings,
253    )
254    .await
255    .with_context(|| format!("deploying static Worker {}", plan.worker_name))?;
256    info!(worker = %plan.worker_name, "alias-tier Worker deployed");
257
258    let zone = parent_zone_name(&plan.custom_domain);
259    let zone_id = cf
260        .zone_id_for_name(zone)
261        .await
262        .with_context(|| format!("resolving zone id for {zone:?}"))?;
263    cf.upsert_worker_custom_domain(
264        &account_id,
265        &zone_id,
266        &plan.custom_domain,
267        &plan.worker_name,
268    )
269    .await
270    .with_context(|| {
271        format!(
272            "binding {} to Worker {}",
273            plan.custom_domain, plan.worker_name
274        )
275    })?;
276    info!(domain = %plan.custom_domain, worker = %plan.worker_name, "alias-tier custom domain bound");
277    Ok(())
278}
279
280// ─── R561-F4: alias-tier registration ───────────────────────────────────────
281//
282// "Claim <name>.{com,net}.yah.dev": validate the label, check it's free, and
283// produce the per-tenant DomainConfig (the F2 manifest) the registration flow
284// writes. Pure + offline — the SaaS "sign up, get a subdomain" moment.
285
286/// The two wildcard alias tiers. `Com` = managed/commercial, `Net` = community.
287#[derive(Debug, Clone, Copy, PartialEq, Eq)]
288pub enum AliasTier {
289    Com,
290    Net,
291}
292
293impl AliasTier {
294    /// The tier's Cloudflare zone, e.g. `net.yah.dev`.
295    pub fn zone(self) -> &'static str {
296        match self {
297            AliasTier::Com => "com.yah.dev",
298            AliasTier::Net => "net.yah.dev",
299        }
300    }
301
302    /// The tier's shared R2 bucket (per-tenant `<svc>/<env>` prefixes within).
303    pub fn bucket(self) -> &'static str {
304        match self {
305            AliasTier::Com => "com-yah-dev",
306            AliasTier::Net => "net-yah-dev",
307        }
308    }
309
310    /// Manifest-name infix, e.g. `net` → `<name>-net-yah-dev` file stem.
311    fn slug(self) -> &'static str {
312        match self {
313            AliasTier::Com => "com",
314            AliasTier::Net => "net",
315        }
316    }
317}
318
319/// A valid DNS label: 1–63 chars, lowercase alphanumeric or hyphen, no
320/// leading/trailing hyphen. (Subdomain names a tenant can claim.)
321pub fn valid_subdomain_label(name: &str) -> bool {
322    !name.is_empty()
323        && name.len() <= 63
324        && !name.starts_with('-')
325        && !name.ends_with('-')
326        && name
327            .bytes()
328            .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'-')
329}
330
331/// Validate and build the per-tenant alias manifest for a claim.
332///
333/// `name` is the requested subdomain label (e.g. `scrabcake`); `component` is
334/// the tenant's static component ref (`"<service>/<component-id>"`). `existing`
335/// is the current domain map (from `CloudConfig`) — the claim fails if the
336/// resulting host or manifest name is already taken. Returns the
337/// [`DomainConfig`] to write at `.yah/domains/<stem>.toml`.
338pub fn plan_alias_claim(
339    tier: AliasTier,
340    name: &str,
341    component: &str,
342    existing: &BTreeMap<String, DomainConfig>,
343) -> Result<DomainConfig> {
344    if !valid_subdomain_label(name) {
345        anyhow::bail!(
346            "invalid subdomain {name:?} — must be 1–63 chars, lowercase \
347             alphanumeric or hyphen, no leading/trailing hyphen"
348        );
349    }
350    if component.split_once('/').is_none() {
351        anyhow::bail!("component {component:?} — expected \"<service>/<component-id>\"");
352    }
353
354    let domain = format!("{name}.{}", tier.zone());
355    let stem = format!("{name}-{}-yah-dev", tier.slug());
356
357    if existing.values().any(|d| d.domain == domain) {
358        anyhow::bail!("{domain} is already claimed");
359    }
360    if existing.contains_key(&stem) {
361        anyhow::bail!("domain manifest {stem:?} already exists");
362    }
363
364    Ok(DomainConfig {
365        schema_version: 1,
366        name: stem,
367        domain,
368        // R594-F12: every tenant manifest is Worker-served (R561-F3 binds the
369        // subdomain as a custom domain on the generated Worker).
370        front_door: FrontDoor::Worker,
371        cdn_bucket: tier.bucket().to_string(),
372        worker_bundle_path: None,
373        routes: vec![DomainRoute {
374            path: "/*".into(),
375            // A claimed alias serves one bundle at the root with no special
376            // header needs; a tenant that wants some edits its own manifest.
377            headers: BTreeMap::new(),
378            mode: RouteMode::Static {
379                component: component.to_string(),
380            },
381        }],
382    })
383}
384
385// ─── R859-F1: sovereign apex (front_door = "passway") ───────────────────────
386//
387// The two-source flip this dissolves: `.yah/domains/*.toml` DECLARED
388// `front_door`, while `scripts/cf-apex-mode.sh` imperatively MUTATED the apex
389// records, and nothing kept the two in agreement. The declaration drifted from
390// the live zone for 19 days once (R330-B36) and 4 more (R703-B4). Here the
391// manifest is the only source and the reconciler renders the records from it.
392//
393// Same house shape as the Worker arm: a pure planner (`plan_domain_passway` +
394// `diff_apex_records`) that is fully unit-testable offline, and an I/O applier
395// (`deploy_domain_passway`) that only executes the diff.
396
397use crate::config::MachineConfig;
398use crate::envoy::dns_record::{
399    DnsRecordDeleteInput, DnsRecordListInput, DnsRecordUpsertInput,
400};
401use crate::provider::cloudflare_envoy::CloudflareEnvoy;
402use std::net::Ipv4Addr;
403
404/// One declared public origin behind a sovereign apex — a machine that runs a
405/// passway front door and carries a routable IPv4 address.
406#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
407pub struct PasswayOrigin {
408    /// Machine name, as declared in `.yah/infra/machines/<name>.toml`. Carried
409    /// so every error and log line names the box an operator would open.
410    pub machine: String,
411    /// The address the apex A record will publish.
412    pub address: Ipv4Addr,
413}
414
415/// What [`public_origins`] resolved: the origins to publish, and the ones it
416/// deliberately held back because their machine is confirmed down (R859-F2).
417///
418/// Two lists rather than one shorter one, because the difference is
419/// load-bearing downstream: an origin that is *absent* from a declaration and
420/// an origin that is *present and known dead* want opposite treatment when the
421/// declaration itself is untrustworthy. See
422/// [`DomainPasswayPlan::health_withdrawn`].
423#[derive(Debug, Clone, Default, PartialEq, Eq)]
424pub struct ResolvedOrigins {
425    /// Origins the apex should publish.
426    pub origins: Vec<PasswayOrigin>,
427    /// Declared, resolvable origins withheld because their machine is
428    /// confirmed down.
429    pub health_withdrawn: Vec<PasswayOrigin>,
430}
431
432/// The apex record set one passway domain should carry — pure output of
433/// [`plan_domain_passway`], applied by [`deploy_domain_passway`].
434#[derive(Debug, Clone, PartialEq, Eq)]
435pub struct DomainPasswayPlan {
436    /// Parent Cloudflare zone apex name, from [`parent_zone_name`].
437    pub zone: String,
438    /// FQDN the records live at — the manifest's `domain`.
439    pub name: String,
440    /// Desired origins, deduplicated and sorted by address so two planning
441    /// runs over the same declaration are byte-identical.
442    pub origins: Vec<PasswayOrigin>,
443    /// Whether [`origins`](Self::origins) is the **whole** declared set.
444    ///
445    /// `false` when the ingress collation reported problems, because
446    /// [`collate_workspace_ingress`](crate::validate::collate_workspace_ingress)
447    /// returns `Ok` while *skipping* an edge whose declaration fails to plan —
448    /// so a passway edge with a config typo silently drops its machine from
449    /// `front_doors`. An absent machine and a withdrawn machine are then
450    /// indistinguishable from inside this plan, and the two want opposite
451    /// treatment: a withdrawal should prune the record, a typo must not.
452    ///
453    /// So the flag gates exactly one thing —
454    /// [`diff_apex_records`] withholds every prune when it is `false` —
455    /// and nothing else. Upserts still happen: growing the fleet must keep
456    /// working through an unrelated service's broken declaration, and an
457    /// upsert can never make the apex worse. **Fail-closed on withdrawal,
458    /// fail-open on addition.**
459    pub origins_complete: bool,
460    /// Addresses withdrawn because their machine is **confirmed down** —
461    /// R859-F2's cross-provider failover, rendered through R859-F1's existing
462    /// diff rather than through a second entry point.
463    ///
464    /// # Why this is not just "absent from `origins`"
465    ///
466    /// [`origins_complete`](Self::origins_complete) and this field are two
467    /// different facts and it is worth being exact about which is which, because
468    /// conflating them silently disables the failover this ticket exists for:
469    ///
470    /// - `origins_complete` answers **"is the *declaration* picture
471    ///   trustworthy?"** `false` means the collation skipped an edge, so an
472    ///   origin's absence might be a withdrawal or might be a config typo —
473    ///   indistinguishable, so every prune is withheld.
474    /// - `health_withdrawn` answers **"which declared machine do we know is
475    ///   down?"** It is *positive* evidence about a machine the collation
476    ///   plainly saw, not an inference from an absence.
477    ///
478    /// So a health withdrawal is **not** suppressed by `origins_complete =
479    /// false`. The discriminator `origins_complete` exists to protect is
480    /// exactly "did we see this machine declared?", and for these machines the
481    /// answer is yes — they were resolved, taint-checked and address-checked on
482    /// the way into this list. A broken edge elsewhere in the workspace says
483    /// nothing about a box we watched go down, and letting it veto the
484    /// withdrawal would leave a dead origin taking its share of the
485    /// round-robin for as long as some unrelated service's TOML is wrong.
486    ///
487    /// The fail-closed-on-withdrawal rule is not weakened by this: the gate for
488    /// a health withdrawal is the *quorum* verdict, applied one layer up in
489    /// [`plan_ingress_owner_effect`](crate::provider::floating_ip::plan_ingress_owner_effect),
490    /// which refuses to emit the exclusion at all out of a degraded quorum. Two
491    /// withdrawal paths, each fail-closed on the evidence that is actually
492    /// relevant to it.
493    pub health_withdrawn: Vec<PasswayOrigin>,
494}
495
496/// One A record currently live at the apex, as read back by
497/// `dns.record.list`.
498#[derive(Debug, Clone, PartialEq, Eq)]
499pub struct LiveApexRecord {
500    /// The published address.
501    pub content: String,
502    /// Whether the provider proxies it (Cloudflare orange-cloud).
503    pub proxied: bool,
504}
505
506/// What one apply has to change to converge the apex.
507#[derive(Debug, Clone, Default, PartialEq, Eq)]
508pub struct ApexRecordDiff {
509    /// Addresses to upsert as DNS-only A records.
510    pub upsert: Vec<String>,
511    /// Addresses to remove — surplus A records at the same name.
512    pub prune: Vec<String>,
513    /// Addresses that *would* have been pruned but were left in place because
514    /// [`DomainPasswayPlan::origins_complete`] was `false`.
515    ///
516    /// Kept rather than dropped so the applier can name the exact records it
517    /// declined to touch. A live A record sitting here is the visible symptom
518    /// of a broken ingress declaration somewhere in the workspace — it is
519    /// either a real withdrawal this apply refused to act on, or an origin the
520    /// collation could not see.
521    pub withheld_prune: Vec<String>,
522}
523
524impl ApexRecordDiff {
525    /// `true` when this apply has no write to make. Withheld prunes do not
526    /// count: they are deliberately not writes, and a converged-with-withheld
527    /// diff still has something to warn about.
528    pub fn is_converged(&self) -> bool {
529        self.upsert.is_empty() && self.prune.is_empty()
530    }
531}
532
533/// What [`deploy_domain_passway`] actually did, for the apply summary.
534#[derive(Debug, Clone, Default, PartialEq, Eq)]
535pub struct PasswayApexOutcome {
536    /// Addresses written (created or re-pointed to DNS-only).
537    pub upserted: Vec<String>,
538    /// Addresses withdrawn.
539    pub pruned: Vec<String>,
540    /// Addresses left in place that the declaration no longer names, because
541    /// the ingress collation was incomplete — see
542    /// [`DomainPasswayPlan::origins_complete`]. Surfaced in the outcome (not
543    /// only in the log) so `yah cloud apply` can print it: a withheld prune is
544    /// the operator's cue that some service's ingress declaration is broken.
545    pub withheld_prune: Vec<String>,
546}
547
548impl PasswayApexOutcome {
549    /// `true` when the live apex already matched the declaration and nothing
550    /// was written.
551    pub fn is_noop(&self) -> bool {
552        self.upserted.is_empty() && self.pruned.is_empty()
553    }
554}
555
556/// Parse `address` as a **publicly routable** IPv4, naming `machine` on
557/// failure.
558///
559/// This is the publicness cross-check nothing performs today: the `public-ip`
560/// taint and `connect.address` are two independent operator declarations, and
561/// [`crate::config::ConnectSpec::address`]'s own doc warns it is *whichever*
562/// address the operator chose to dial the box on — public, LAN, or tailnet
563/// (us-west-002 is deliberately pointed at its tailnet IP, R608-F10). Publishing
564/// a `100.64.0.x` or `192.168.x.x` A record on a public apex takes its share of
565/// the round-robin straight to a black hole, so a taint/address disagreement
566/// has to be a hard error rather than a silently-broken record.
567///
568/// Kept local to this arm on purpose (R859-F1 decision 4): a workspace-wide
569/// lint would fire on every machine that carries the taint for placement
570/// reasons while being dialled over the mesh, which is legitimate.
571fn public_ipv4_for(machine: &str, address: &str) -> Result<Ipv4Addr> {
572    let ip: Ipv4Addr = address.parse().map_err(|_| {
573        anyhow::anyhow!(
574            "machine {machine}: connect.address = {address:?} is not an IPv4 address, so it \
575             cannot be published as an apex A record. Either set it to the box's public IPv4 \
576             or drop the `public-ip` taint from {machine}.toml."
577        )
578    })?;
579    let o = ip.octets();
580    // 100.64.0.0/10 — RFC 6598 shared address space, which is also the
581    // tailnet range this fleet's mesh addresses live in.
582    let is_cgnat = o[0] == 100 && (64..=127).contains(&o[1]);
583    let reason = if ip.is_unspecified() {
584        Some("unspecified")
585    } else if ip.is_loopback() {
586        Some("loopback")
587    } else if ip.is_private() {
588        Some("RFC 1918 private")
589    } else if ip.is_link_local() {
590        Some("link-local")
591    } else if is_cgnat {
592        Some("RFC 6598 shared / tailnet")
593    } else if ip.is_multicast() {
594        Some("multicast")
595    } else if ip.is_broadcast() {
596        Some("broadcast")
597    } else {
598        None
599    };
600    if let Some(reason) = reason {
601        anyhow::bail!(
602            "machine {machine}: connect.address = {address:?} is a {reason} address, not a \
603             public one — publishing it at a public apex would black-hole its share of the \
604             round-robin. Set connect.address to the box's public IPv4 or drop the \
605             `public-ip` taint from {machine}.toml."
606        );
607    }
608    Ok(ip)
609}
610
611/// Resolve collated front-door machine names to the public origins the apex
612/// should publish (R859-F1).
613///
614/// Keeps only machines carrying the [`workload_spec::PUBLIC_IP_TAINT`]
615/// affinity taint — the same declaration that makes yubaba willing to place a
616/// public ingress appliance there — and reads each one's
617/// [`ConnectSpec::address`](crate::config::ConnectSpec::address). A named
618/// machine that is absent from `machines` is an error, not a silent drop: the
619/// alternative is an apex that quietly shrinks because a machine TOML was
620/// renamed.
621///
622/// Machines without the taint are skipped silently. That is the intended
623/// filter, not a swallowed error — a passway edge may be collated onto a
624/// mesh-only box that fronts internal hostnames.
625///
626/// # `health_excluded` — R859-F2's withdrawal seam
627///
628/// Machines named in `health_excluded` are declared, resolvable, and dropped
629/// from the origin list anyway, because something confirmed they are down. The
630/// resulting shorter list flows through [`plan_domain_passway`] and
631/// [`diff_apex_records`] unchanged, so the withdrawal renders as an ordinary
632/// prune and needs no second entry point — the whole point of extending this
633/// function rather than adding a parallel one.
634///
635/// Exclusions are returned separately (see [`ResolvedOrigins`]) rather than
636/// silently vanishing, because "declared but withheld for health" is a
637/// different fact from "never declared", and [`diff_apex_records`] has to be
638/// able to tell them apart. See [`DomainPasswayPlan::health_withdrawn`].
639///
640/// An excluded machine is still *resolved* first: it must exist, carry the
641/// taint and have a public address, exactly as if it were staying. Skipping
642/// those checks would let a health exclusion paper over a config error, and a
643/// machine we cannot resolve is one we cannot honestly say we are withdrawing.
644///
645/// A name in `health_excluded` that is not a front door at all is ignored — the
646/// caller's liveness view covers the whole fleet, and most of it never fronts
647/// anything.
648pub fn public_origins(
649    front_door_machines: &[String],
650    machines: &[MachineConfig],
651    health_excluded: &[String],
652) -> Result<ResolvedOrigins> {
653    let mut resolved = ResolvedOrigins::default();
654    for name in front_door_machines {
655        let machine = machines
656            .iter()
657            .find(|m| &m.name == name)
658            .with_context(|| {
659                format!(
660                    "front door collated onto machine {name:?}, which has no \
661                     .yah/infra/machines/*.toml — cannot resolve its public address"
662                )
663            })?;
664        if !machine
665            .taints
666            .iter()
667            .any(|t| t == workload_spec::PUBLIC_IP_TAINT)
668        {
669            debug!(
670                machine = %name,
671                "front-door machine has no `public-ip` taint — not an apex origin"
672            );
673            continue;
674        }
675        let address = machine
676            .connect
677            .as_ref()
678            .map(|c| c.address.as_str())
679            .with_context(|| {
680                format!(
681                    "machine {name} carries the `public-ip` taint but declares no \
682                     [connect] address — nothing to publish at the apex"
683                )
684            })?;
685        let origin = PasswayOrigin {
686            machine: name.clone(),
687            address: public_ipv4_for(name, address)?,
688        };
689        if health_excluded.iter().any(|m| m == name) {
690            debug!(
691                machine = %name,
692                address = %origin.address,
693                "front-door machine confirmed down — withheld from the apex origin set"
694            );
695            resolved.health_withdrawn.push(origin);
696        } else {
697            resolved.origins.push(origin);
698        }
699    }
700    Ok(resolved)
701}
702
703/// Build the apex record plan for a `front_door = "passway"` manifest.
704///
705/// Refuses two shapes outright:
706///
707/// 1. A manifest declaring a different front door — the same guard
708///    [`plan_domain_worker`] carries, for the same reason.
709/// 2. An **empty** origin set. The desired set is what the applier prunes
710///    against, so an empty one would not mean "leave it alone", it would mean
711///    "delete every A record at the apex" — a live outage rendered from a
712///    collation that simply found nothing. Nothing about "no machine is
713///    declared" says "take the site down", so it is an error.
714///
715/// That empty-set guard is checked **before** `origins_complete` is consulted:
716/// an empty set is unusable whether or not the collation was clean, so it stays
717/// the louder failure.
718///
719/// `origins_complete` is the caller's answer to "did the collation see the
720/// whole picture?" — `false` withholds every prune downstream. See
721/// [`DomainPasswayPlan::origins_complete`].
722///
723/// # The empty-set guard is also the health failover's backstop (R859-F2)
724///
725/// Guard 2 is checked against the origins that **survive** health exclusion, and
726/// that is the load-bearing interaction of this whole feature: if every declared
727/// front door is confirmed down, `origins` is empty and this refuses. "All our
728/// front doors are down" must never render as "withdraw every A record and take
729/// the site down" — a dead origin still in DNS is a partial outage, an empty
730/// apex is a total one, and between those the first is strictly better. So the
731/// health withdrawal is capped at "all but the last origin" by construction,
732/// with no separate rule to keep in sync.
733///
734/// An address that is *also* served by a surviving origin is dropped from
735/// [`health_withdrawn`](DomainPasswayPlan::health_withdrawn) for the same
736/// reason, one level finer: two machines can share a floating IP (the dedup
737/// comment below names that case), and pruning the record because one of them
738/// died would withdraw an address the other is still answering on.
739pub fn plan_domain_passway(
740    domain: &DomainConfig,
741    resolved: ResolvedOrigins,
742    origins_complete: bool,
743) -> Result<DomainPasswayPlan> {
744    let ResolvedOrigins {
745        origins,
746        health_withdrawn,
747    } = resolved;
748    if domain.front_door != FrontDoor::Passway {
749        anyhow::bail!(
750            "domain {} ({}) declares front_door = \"{}\" — only \"passway\" domains \
751             get a sovereign apex rendered here",
752            domain.name,
753            domain.domain,
754            domain.front_door.as_str()
755        );
756    }
757
758    // By address, not by machine name: the address is what lands in DNS, so
759    // ordering on it is what makes two planning runs byte-identical even if a
760    // box is renamed. Dedup on the same key — two edges collated onto one
761    // machine (or two machines sharing a floating IP) publish one record.
762    let mut origins = origins;
763    origins.sort_by(|a, b| (a.address, &a.machine).cmp(&(b.address, &b.machine)));
764    origins.dedup_by(|a, b| a.address == b.address);
765
766    if origins.is_empty() {
767        anyhow::bail!(
768            "domain {} ({}) declares front_door = \"passway\" but no declared front-door \
769             machine carries the `public-ip` taint with a public address — refusing to \
770             render an empty apex, which would withdraw every A record and take the site \
771             down. Declare the ingress edge's machines (or their taints) first.",
772            domain.name,
773            domain.domain
774        );
775    }
776
777    // Same normalisation as `origins`, plus the survivor filter: an address a
778    // live origin still answers on is not withdrawn, however many of the
779    // machines sharing it went down.
780    let mut health_withdrawn = health_withdrawn;
781    health_withdrawn.retain(|w| !origins.iter().any(|o| o.address == w.address));
782    health_withdrawn.sort_by(|a, b| (a.address, &a.machine).cmp(&(b.address, &b.machine)));
783    health_withdrawn.dedup_by(|a, b| a.address == b.address);
784
785    Ok(DomainPasswayPlan {
786        zone: parent_zone_name(&domain.domain).to_string(),
787        name: domain.domain.clone(),
788        origins,
789        origins_complete,
790        health_withdrawn,
791    })
792}
793
794/// Diff the planned apex against the A records live at that name.
795///
796/// `live` must already be narrowed to **type A at `plan.name`** — MX, TXT and
797/// AAAA records share the apex and are never this arm's to touch.
798///
799/// A live record whose content is desired but which is *proxied* still lands in
800/// `upsert`: orange-cloud is a break-glass state
801/// (`scripts/cf-apex-mode.sh orange`) and this reconciler declares DNS-only, so
802/// re-writing the record with `proxied = false` is convergence, not churn.
803///
804/// When [`plan.origins_complete`](DomainPasswayPlan::origins_complete) is
805/// `false` the surplus records move to
806/// [`withheld_prune`](ApexRecordDiff::withheld_prune) instead of `prune`, and
807/// `upsert` is unaffected. An incomplete collation cannot tell a withdrawn
808/// origin from one whose declaration failed to plan, and only one of those two
809/// wants a DNS withdrawal.
810///
811/// # …with one exception, and it is the point of R859-F2
812///
813/// A surplus address that appears in
814/// [`plan.health_withdrawn`](DomainPasswayPlan::health_withdrawn) is pruned
815/// **regardless of `origins_complete`**. The two flags are not two strengths of
816/// the same doubt, they are answers to different questions, and the four cases
817/// come out like this:
818///
819/// | `origins_complete` | in `health_withdrawn` | verdict |
820/// |---|---|---|
821/// | `true`  | no  | `prune` — an ordinary withdrawal from a trusted declaration |
822/// | `true`  | yes | `prune` — a health withdrawal from a trusted declaration |
823/// | `false` | no  | `withheld_prune` — might be a withdrawal, might be a typo |
824/// | `false` | yes | `prune` — we saw this machine declared *and* saw it die |
825///
826/// The bottom-right cell is the one that matters. `origins_complete = false`
827/// protects against mistaking an absence for a withdrawal; a health withdrawal
828/// is not an absence, it is a positive observation about a machine the
829/// collation resolved. An unrelated service's broken TOML is not evidence about
830/// a box we watched go down, and letting it withhold the prune would leave a
831/// dead origin serving its share of the round-robin for as long as that typo
832/// lives.
833pub fn diff_apex_records(plan: &DomainPasswayPlan, live: &[LiveApexRecord]) -> ApexRecordDiff {
834    let desired: Vec<String> = plan
835        .origins
836        .iter()
837        .map(|o| o.address.to_string())
838        .collect();
839    let upsert = desired
840        .iter()
841        .filter(|ip| {
842            !live
843                .iter()
844                .any(|r| &&r.content == ip && !r.proxied)
845        })
846        .cloned()
847        .collect();
848    let surplus: Vec<String> = live
849        .iter()
850        .filter(|r| !desired.contains(&r.content))
851        .map(|r| r.content.clone())
852        .collect();
853    if plan.origins_complete {
854        return ApexRecordDiff {
855            upsert,
856            prune: surplus,
857            withheld_prune: Vec::new(),
858        };
859    }
860    // Incomplete declaration: withhold every prune EXCEPT the health
861    // withdrawals, which rest on a positive observation rather than on an
862    // absence. See this function's doc table.
863    let withdrawn: Vec<String> = plan
864        .health_withdrawn
865        .iter()
866        .map(|o| o.address.to_string())
867        .collect();
868    let (prune, withheld_prune) = surplus
869        .into_iter()
870        .partition(|content| withdrawn.contains(content));
871    ApexRecordDiff {
872        upsert,
873        prune,
874        withheld_prune,
875    }
876}
877
878/// Apply a [`DomainPasswayPlan`] against live DNS (R859-F1, live I/O).
879///
880/// **First production consumer of the `dns.*` envoy verbs** — every read and
881/// write here goes through [`CloudflareEnvoy`]'s typed verb handlers rather
882/// than the `CloudflareClient` methods the older arms call directly, so
883/// swapping the DNS provider is a matter of resolving a different adapter here
884/// and nothing else.
885///
886/// Two invariants, both borrowed from `scripts/cf-apex-mode.sh`, whose
887/// behaviour this replaces for the routine case:
888///
889/// - **List first, skip when converged.** Same shape as
890///   [`ensure_r2_custom_domain`], so a re-run of `yah cloud apply` writes
891///   nothing.
892/// - **Upsert before prune.** Desired records are written first and surplus
893///   ones removed last, so the apex is never momentarily recordless. Only
894///   `A` records at the name are ever pruned; the apex's MX and TXT records
895///   (mail routing, SPF, site verification) are outside the filter by
896///   construction.
897/// - **Fail-closed on withdrawal, fail-open on addition.** When the plan says
898///   the origin set is incomplete
899///   ([`origins_complete = false`](DomainPasswayPlan::origins_complete)) the
900///   upserts still run but every prune is withheld and warned about — an
901///   absent origin might be a withdrawal or might be a config typo upstream,
902///   and only one of those should take a live record away.
903///
904/// Always writes `proxied = false`: the manifest cannot express grey vs
905/// orange, and orange stays break-glass in the script (W267 tier ladder).
906pub async fn deploy_domain_passway(
907    workspace_root: &Path,
908    provider_id: &str,
909    plan: &DomainPasswayPlan,
910) -> Result<PasswayApexOutcome> {
911    let cf_provider = super::cf_creds::CfProvider::resolve(workspace_root, provider_id)?;
912    let envoy = CloudflareEnvoy::new(cf_provider.api_token()?, cf_provider.account_id.clone());
913
914    let listed = envoy
915        .dns_record_list(DnsRecordListInput {
916            zone: plan.zone.clone(),
917            name: Some(plan.name.clone()),
918            record_type: Some("A".to_string()),
919        })
920        .await
921        .with_context(|| format!("listing A records at {}", plan.name))?;
922    let live: Vec<LiveApexRecord> = listed
923        .records
924        .into_iter()
925        .map(|r| LiveApexRecord {
926            content: r.content,
927            proxied: r.proxied,
928        })
929        .collect();
930
931    let diff = diff_apex_records(plan, &live);
932
933    // Warned before the converged early-return: a withheld prune is worth
934    // saying out loud even on an apply that writes nothing, because the record
935    // it names stays live until someone fixes the declaration.
936    if !diff.withheld_prune.is_empty() {
937        warn!(
938            domain = %plan.name,
939            withheld = %diff.withheld_prune.join(", "),
940            "apex A records NOT withdrawn: the ingress collation reported problems, so an \
941             origin missing from it may be a broken declaration rather than a withdrawal. \
942             Run `yah cloud validate` and fix the reported ingress declaration; these \
943             records stay live until the collation is clean."
944        );
945    }
946
947    if diff.is_converged() {
948        debug!(
949            domain = %plan.name,
950            origins = plan.origins.len(),
951            "sovereign apex already matches the declaration — skipping"
952        );
953        return Ok(PasswayApexOutcome {
954            withheld_prune: diff.withheld_prune,
955            ..Default::default()
956        });
957    }
958
959    // Writes first: never leave the apex without a routing record.
960    for ip in &diff.upsert {
961        envoy
962            .dns_record_upsert(DnsRecordUpsertInput {
963                zone: plan.zone.clone(),
964                name: plan.name.clone(),
965                record_type: "A".to_string(),
966                content: ip.clone(),
967                ttl: 1,
968                proxied: false,
969                // A round-robin apex is a multi-valued RRset: without this the
970                // second origin would overwrite the first.
971                match_content: true,
972            })
973            .await
974            .with_context(|| format!("upserting A {} -> {ip}", plan.name))?;
975        info!(domain = %plan.name, origin = %ip, "sovereign apex A record written");
976    }
977
978    // Prune last, and only ever type A carrying an undeclared value.
979    for ip in &diff.prune {
980        envoy
981            .dns_record_delete(DnsRecordDeleteInput {
982                zone: plan.zone.clone(),
983                name: plan.name.clone(),
984                record_type: Some("A".to_string()),
985                content: Some(ip.clone()),
986            })
987            .await
988            .with_context(|| format!("pruning surplus A {} -> {ip}", plan.name))?;
989        info!(domain = %plan.name, origin = %ip, "surplus apex A record withdrawn");
990    }
991
992    Ok(PasswayApexOutcome {
993        upserted: diff.upsert,
994        pruned: diff.prune,
995        withheld_prune: diff.withheld_prune,
996    })
997}
998
999/// Reconcile one `front_door = "passway"` domain end to end — the entry point
1000/// `yah cloud apply` calls (R859-F1).
1001///
1002/// Collates the workspace's declared ingress edges, keeps the passway front
1003/// doors, resolves their machines to public addresses, plans, and applies.
1004/// Every input is *declared* config: no network read decides which origins the
1005/// apex publishes, which is what makes the manifest the single source the
1006/// two-source flip lacked.
1007///
1008/// ## Why `report.problems` gates the prune
1009///
1010/// [`collate_workspace_ingress`](crate::validate::collate_workspace_ingress)
1011/// returns `Ok` while **skipping** an edge whose declaration fails to plan
1012/// (`validate.rs`, the `IngressProblem::Declaration` arms). A passway edge with
1013/// a config typo therefore drops its machine out of `front_doors` silently, and
1014/// from inside the plan that is indistinguishable from the operator having
1015/// withdrawn the node — so without this gate a typo would render as a DNS
1016/// withdrawal and take that origin's share of the apex offline.
1017///
1018/// Any non-empty `problems` therefore sets `origins_complete = false`, which
1019/// withholds every prune for this apply. Deliberately *not* narrowed to the
1020/// problems that mention this domain's edge: attributing a problem to an edge
1021/// requires the very planning that failed, and "non-empty means the picture is
1022/// incomplete" is the whole of what the prune path needs to know. Equally
1023/// deliberately, problems do **not** fail the arm — one service's broken
1024/// ingress declaration must not block another's DNS apply, and upserts can
1025/// only ever add reachable origins.
1026pub async fn ensure_passway_apex(
1027    workspace_root: &Path,
1028    provider_id: &str,
1029    domain: &DomainConfig,
1030) -> Result<PasswayApexOutcome> {
1031    let report = crate::validate::collate_workspace_ingress(workspace_root)
1032        .context("collating workspace ingress to find the passway front doors")?;
1033
1034    let origins_complete = report.problems.is_empty();
1035    if !origins_complete {
1036        for problem in &report.problems {
1037            warn!(
1038                domain = %domain.domain,
1039                "ingress collation problem — apex prunes withheld this apply: {}",
1040                problem.message()
1041            );
1042        }
1043    }
1044
1045    let mut front_door_machines: Vec<String> = report
1046        .collation
1047        .front_doors
1048        .iter()
1049        .filter(|fd| fd.provider == crate::config::IngressProvider::Passway)
1050        .map(|fd| fd.machine.clone())
1051        .collect();
1052    front_door_machines.sort();
1053    front_door_machines.dedup();
1054
1055    let machines: Vec<MachineConfig> = crate::validate::load_machine_tomls(
1056        workspace_root,
1057        crate::validate::MachineLoadMode::Strict,
1058    )?
1059    .into_iter()
1060    .map(|(_, m)| m)
1061    .collect();
1062
1063    // R859-F2: no health exclusions on the `yah cloud apply` path, deliberately
1064    // and not as a stub. The confirmed-down fact is a *fleet-runtime* one —
1065    // it comes from a leader's lease hysteresis, gated on a healthy quorum
1066    // (`plan_ingress_owner_effect`) — and `yah cloud apply` is an operator
1067    // running a declarative converge from a laptop, which holds no such view.
1068    // Feeding it a liveness guess here would let a laptop with a flaky uplink
1069    // withdraw a live origin. The exclusion set enters through the effector
1070    // that *has* the fact; this path renders the declaration as declared.
1071    let resolved = public_origins(&front_door_machines, &machines, &[])?;
1072    let plan = plan_domain_passway(domain, resolved, origins_complete)?;
1073    deploy_domain_passway(workspace_root, provider_id, &plan).await
1074}
1075
1076#[cfg(test)]
1077mod tests {
1078    use super::parent_zone_name;
1079
1080    #[test]
1081    fn parent_zone_strips_one_label_off_subdomain() {
1082        assert_eq!(parent_zone_name("cdn.yah.dev"), "yah.dev");
1083        assert_eq!(parent_zone_name("app.yah.dev"), "yah.dev");
1084    }
1085
1086    #[test]
1087    fn parent_zone_returns_self_for_apex() {
1088        assert_eq!(parent_zone_name("yah.dev"), "yah.dev");
1089    }
1090
1091    #[test]
1092    fn parent_zone_strips_only_first_label_for_deeper_subdomain() {
1093        // Two-label heuristic: yah-side zones are all two-label apexes today.
1094        assert_eq!(parent_zone_name("a.b.yah.dev"), "yah.dev");
1095    }
1096
1097    use super::{plan_domain_worker, DomainWorkerPlan};
1098    use crate::config::{DomainConfig, DomainRoute, FrontDoor, RouteMode};
1099
1100    fn net_tier_manifest() -> DomainConfig {
1101        // Mirrors .yah/domains/scrabcake-net-yah-dev.toml.
1102        DomainConfig {
1103            schema_version: 1,
1104            name: "scrabcake-net-yah-dev".into(),
1105            domain: "scrabcake.net.yah.dev".into(),
1106            front_door: FrontDoor::Worker,
1107            cdn_bucket: "net-yah-dev".into(),
1108            worker_bundle_path: None,
1109            routes: vec![DomainRoute {
1110                headers: Default::default(),
1111                path: "/*".into(),
1112                mode: RouteMode::Static {
1113                    component: "scrabcake/site".into(),
1114                },
1115            }],
1116        }
1117    }
1118
1119    #[test]
1120    fn plan_resolves_worker_name_domain_and_asset_origin() {
1121        let plan =
1122            plan_domain_worker(&net_tier_manifest(), "https://cdn.net.yah.dev", "cloud").unwrap();
1123        assert_eq!(
1124            plan,
1125            DomainWorkerPlan {
1126                worker_name: "scrabcake-net-yah-dev".into(),
1127                custom_domain: "scrabcake.net.yah.dev".into(),
1128                asset_origin: "https://cdn.net.yah.dev/scrabcake/cloud".into(),
1129                bindings: vec![
1130                    (
1131                        "ASSET_ORIGIN".into(),
1132                        "https://cdn.net.yah.dev/scrabcake/cloud".into()
1133                    ),
1134                    ("UPLOAD_ORIGIN".into(), String::new()),
1135                    ("WORKER_MODE".into(), "static".into()),
1136                    ("SSR_ORIGIN".into(), String::new()),
1137                    ("SSR_PREFIXES".into(), "[]".into()),
1138                    ("ROUTE_HEADERS".into(), "[]".into()),
1139                ],
1140            }
1141        );
1142    }
1143
1144    /// R746: headers declared on a route reach the deployed Worker as the
1145    /// ROUTE_HEADERS binding. Without this the manifest could declare them and
1146    /// the Worker would serve without them — the exact silent gap the primitive
1147    /// exists to close.
1148    #[test]
1149    fn plan_carries_declared_route_headers_into_the_bindings() {
1150        let mut dom = net_tier_manifest();
1151        dom.routes[0].headers = [
1152            ("Cross-Origin-Opener-Policy".to_string(), "same-origin".to_string()),
1153            (
1154                "Cross-Origin-Embedder-Policy".to_string(),
1155                "require-corp".to_string(),
1156            ),
1157        ]
1158        .into_iter()
1159        .collect();
1160        let plan = plan_domain_worker(&dom, "https://cdn.net.yah.dev", "cloud").unwrap();
1161        let binding = plan
1162            .bindings
1163            .iter()
1164            .find(|(k, _)| k == "ROUTE_HEADERS")
1165            .expect("ROUTE_HEADERS binding");
1166        assert!(binding.1.contains("same-origin"), "{}", binding.1);
1167        assert!(binding.1.contains("require-corp"), "{}", binding.1);
1168        assert!(binding.1.contains("/*"), "{}", binding.1);
1169    }
1170
1171    #[test]
1172    fn plan_trims_trailing_slash_on_cdn_base() {
1173        let plan =
1174            plan_domain_worker(&net_tier_manifest(), "https://cdn.net.yah.dev/", "cloud").unwrap();
1175        assert_eq!(plan.asset_origin, "https://cdn.net.yah.dev/scrabcake/cloud");
1176    }
1177
1178    /// R594-F12: a manifest that declares a different front door must not
1179    /// get a Cloudflare Worker synthesized behind its back — the Worker
1180    /// would deploy and then never receive traffic.
1181    #[test]
1182    fn plan_bails_when_front_door_is_not_worker() {
1183        for door in [FrontDoor::BucketDirect, FrontDoor::Passway] {
1184            let mut dom = net_tier_manifest();
1185            dom.front_door = door;
1186            let err = plan_domain_worker(&dom, "https://cdn.net.yah.dev", "cloud").unwrap_err();
1187            let msg = format!("{err:#}");
1188            assert!(msg.contains("front_door"), "{msg}");
1189            assert!(msg.contains(door.as_str()), "{msg}");
1190        }
1191    }
1192
1193    #[test]
1194    fn plan_bails_when_no_static_route() {
1195        let mut dom = net_tier_manifest();
1196        dom.routes = vec![DomainRoute {
1197            headers: Default::default(),
1198            path: "/old".into(),
1199            mode: RouteMode::Redirect {
1200                target: "https://elsewhere".into(),
1201                status: 308,
1202            },
1203        }];
1204        let err = plan_domain_worker(&dom, "https://cdn.net.yah.dev", "cloud").unwrap_err();
1205        assert!(
1206            format!("{err:#}").contains("no `static` route"),
1207            "got: {err:#}"
1208        );
1209    }
1210
1211    // ── R561-F4: alias-tier registration ──
1212    use super::{plan_alias_claim, valid_subdomain_label, AliasTier};
1213    use std::collections::BTreeMap;
1214
1215    #[test]
1216    fn label_validation_rules() {
1217        assert!(valid_subdomain_label("scrabcake"));
1218        assert!(valid_subdomain_label("my-repo-1"));
1219        assert!(!valid_subdomain_label("")); // empty
1220        assert!(!valid_subdomain_label("-lead")); // leading hyphen
1221        assert!(!valid_subdomain_label("trail-")); // trailing hyphen
1222        assert!(!valid_subdomain_label("Caps")); // uppercase
1223        assert!(!valid_subdomain_label("under_score")); // underscore
1224    }
1225
1226    #[test]
1227    fn claim_builds_net_tier_manifest() {
1228        let dom = plan_alias_claim(
1229            AliasTier::Net,
1230            "scrabcake",
1231            "scrabcake/site",
1232            &BTreeMap::new(),
1233        )
1234        .unwrap();
1235        assert_eq!(dom.name, "scrabcake-net-yah-dev");
1236        assert_eq!(dom.domain, "scrabcake.net.yah.dev");
1237        assert_eq!(dom.cdn_bucket, "net-yah-dev");
1238        assert_eq!(dom.routes.len(), 1);
1239        assert!(
1240            matches!(&dom.routes[0].mode, RouteMode::Static { component } if component == "scrabcake/site")
1241        );
1242    }
1243
1244    #[test]
1245    fn claim_uses_com_tier_zone_and_bucket() {
1246        let dom = plan_alias_claim(AliasTier::Com, "acme", "acme/site", &BTreeMap::new()).unwrap();
1247        assert_eq!(dom.domain, "acme.com.yah.dev");
1248        assert_eq!(dom.cdn_bucket, "com-yah-dev");
1249    }
1250
1251    #[test]
1252    fn claim_bails_on_duplicate_host() {
1253        let existing: BTreeMap<String, DomainConfig> =
1254            [("scrabcake-net-yah-dev".to_string(), net_tier_manifest())]
1255                .into_iter()
1256                .collect();
1257        let err =
1258            plan_alias_claim(AliasTier::Net, "scrabcake", "scrabcake/site", &existing).unwrap_err();
1259        assert!(format!("{err:#}").contains("already"), "got: {err:#}");
1260    }
1261
1262    #[test]
1263    fn claim_bails_on_invalid_label() {
1264        let err =
1265            plan_alias_claim(AliasTier::Net, "Bad_Name", "x/y", &BTreeMap::new()).unwrap_err();
1266        assert!(
1267            format!("{err:#}").contains("invalid subdomain"),
1268            "got: {err:#}"
1269        );
1270    }
1271
1272    // ── R859-F1: sovereign apex (front_door = "passway") ──
1273    //
1274    // Everything here is the pure half. The blast radius of this arm is LIVE
1275    // DNS on a public apex, so the decision logic is exercised offline in full
1276    // and `deploy_domain_passway` only executes the diff these produce.
1277
1278    use super::{
1279        diff_apex_records, plan_domain_passway, public_origins, ApexRecordDiff, DomainPasswayPlan,
1280        LiveApexRecord, PasswayOrigin, ResolvedOrigins,
1281    };
1282    use crate::config::{ConnectSpec, MachineConfig};
1283    use std::net::Ipv4Addr;
1284
1285    /// Mirrors `.yah/domains/yah-dev.toml` — the only `front_door = "passway"`
1286    /// manifest in the camp today.
1287    fn passway_manifest() -> DomainConfig {
1288        DomainConfig {
1289            schema_version: 1,
1290            name: "yah-dev".into(),
1291            domain: "yah.dev".into(),
1292            front_door: FrontDoor::Passway,
1293            cdn_bucket: "yah-dev".into(),
1294            worker_bundle_path: None,
1295            routes: vec![DomainRoute {
1296                headers: Default::default(),
1297                path: "/*".into(),
1298                mode: RouteMode::Static {
1299                    component: "yah-marketing/site".into(),
1300                },
1301            }],
1302        }
1303    }
1304
1305    fn machine(name: &str, address: Option<&str>, taints: &[&str]) -> MachineConfig {
1306        MachineConfig {
1307            name: name.into(),
1308            provider: "ovh".into(),
1309            location: None,
1310            server_type: None,
1311            hosts_mirrors: vec![],
1312            mesh_tags: vec![],
1313            region: None,
1314            zone: None,
1315            arch: None,
1316            bucket: None,
1317            vendor: None,
1318            nickname: None,
1319            legacy_hostkey_fingerprint: None,
1320            registration: Default::default(),
1321            ssh_keys: vec![],
1322            cloudflared: None,
1323            hosts_operator_bridge: false,
1324            connect: address.map(|a| ConnectSpec {
1325                address: a.into(),
1326                ssh: format!("root@{a}"),
1327                yubaba_port: None,
1328                yubaba: None,
1329            }),
1330            allocatable: None,
1331            taints: taints.iter().map(|t| t.to_string()).collect(),
1332            sovereign_group: None,
1333            sovereign_role: None,
1334            ingress_floating_ip: None,
1335        }
1336    }
1337
1338    /// The live fleet shape: us-east-001 and us-west-001 both carry the
1339    /// `public-ip` taint with a public address (machines/*.toml), a third box
1340    /// fronts internal hostnames over the mesh and must not reach the apex.
1341    fn fleet() -> Vec<MachineConfig> {
1342        vec![
1343            machine("us-east-001", Some("51.81.85.145"), &["public-ip"]),
1344            machine("us-west-001", Some("15.204.89.240"), &["public-ip"]),
1345            machine("us-west-002", Some("100.64.0.4"), &[]),
1346        ]
1347    }
1348
1349    fn origins(names: &[&str]) -> ResolvedOrigins {
1350        origins_excluding(names, &[])
1351    }
1352
1353    /// R859-F2: the same resolution with `down` confirmed dead — declared and
1354    /// resolvable, but held out of the published set.
1355    fn origins_excluding(names: &[&str], down: &[&str]) -> ResolvedOrigins {
1356        let fleet = fleet();
1357        let names: Vec<String> = names.iter().map(|n| n.to_string()).collect();
1358        let down: Vec<String> = down.iter().map(|n| n.to_string()).collect();
1359        public_origins(&names, &fleet, &down).unwrap()
1360    }
1361
1362    /// A plan whose origin set is COMPLETE — the clean-collation case.
1363    fn plan_of(names: &[&str]) -> DomainPasswayPlan {
1364        plan_domain_passway(&passway_manifest(), origins(names), true).unwrap()
1365    }
1366
1367    /// The same plan, but built from a collation that reported problems, so an
1368    /// origin's absence cannot be read as a withdrawal.
1369    fn plan_of_incomplete(names: &[&str]) -> DomainPasswayPlan {
1370        plan_domain_passway(&passway_manifest(), origins(names), false).unwrap()
1371    }
1372
1373    fn live(records: &[(&str, bool)]) -> Vec<LiveApexRecord> {
1374        records
1375            .iter()
1376            .map(|(content, proxied)| LiveApexRecord {
1377                content: (*content).into(),
1378                proxied: *proxied,
1379            })
1380            .collect()
1381    }
1382
1383    #[test]
1384    fn public_origins_keeps_tainted_machines_and_skips_the_rest() {
1385        let got = origins(&["us-east-001", "us-west-001", "us-west-002"]);
1386        assert!(
1387            got.health_withdrawn.is_empty(),
1388            "no exclusions were passed, so nothing may be withheld"
1389        );
1390        assert_eq!(
1391            got.origins,
1392            vec![
1393                PasswayOrigin {
1394                    machine: "us-east-001".into(),
1395                    address: Ipv4Addr::new(51, 81, 85, 145),
1396                },
1397                PasswayOrigin {
1398                    machine: "us-west-001".into(),
1399                    address: Ipv4Addr::new(15, 204, 89, 240),
1400                },
1401            ],
1402            "an untainted mesh-only front door is not an apex origin"
1403        );
1404    }
1405
1406    /// The publicness cross-check nothing did before R859-F1: the `public-ip`
1407    /// taint and `connect.address` are independent declarations, and a machine
1408    /// dialled over its tailnet address would otherwise publish `100.64.0.x`
1409    /// at a public apex and black-hole its share of the round-robin.
1410    #[test]
1411    fn public_origins_rejects_a_non_public_address_and_names_the_machine() {
1412        for bad in ["100.64.0.4", "10.0.0.7", "192.168.1.20", "127.0.0.1"] {
1413            let fleet = vec![machine("us-west-002", Some(bad), &["public-ip"])];
1414            let err = public_origins(&["us-west-002".to_string()], &fleet, &[]).unwrap_err();
1415            let msg = format!("{err:#}");
1416            assert!(msg.contains("us-west-002"), "{msg}");
1417            assert!(msg.contains(bad), "{msg}");
1418            assert!(msg.contains("public"), "{msg}");
1419        }
1420    }
1421
1422    #[test]
1423    fn public_origins_rejects_an_unparseable_address_and_names_the_machine() {
1424        let fleet = vec![machine("us-east-001", Some("edge.example.net"), &["public-ip"])];
1425        let err = public_origins(&["us-east-001".to_string()], &fleet, &[]).unwrap_err();
1426        let msg = format!("{err:#}");
1427        assert!(msg.contains("us-east-001"), "{msg}");
1428        assert!(msg.contains("not an IPv4 address"), "{msg}");
1429    }
1430
1431    #[test]
1432    fn public_origins_rejects_a_tainted_machine_with_no_connect_block() {
1433        let fleet = vec![machine("us-east-001", None, &["public-ip"])];
1434        let err = public_origins(&["us-east-001".to_string()], &fleet, &[]).unwrap_err();
1435        let msg = format!("{err:#}");
1436        assert!(msg.contains("us-east-001"), "{msg}");
1437        assert!(msg.contains("[connect]"), "{msg}");
1438    }
1439
1440    #[test]
1441    fn public_origins_rejects_a_machine_with_no_toml() {
1442        let err = public_origins(&["ghost-001".to_string()], &fleet(), &[]).unwrap_err();
1443        let msg = format!("{err:#}");
1444        assert!(msg.contains("ghost-001"), "{msg}");
1445    }
1446
1447    #[test]
1448    fn plan_sorts_and_dedups_origins_and_resolves_the_apex_zone() {
1449        let mut o = origins(&["us-west-001", "us-east-001"]);
1450        o.origins.push(o.origins[0].clone()); // same machine collated twice
1451        let plan = plan_domain_passway(&passway_manifest(), o, true).unwrap();
1452        assert_eq!(plan.zone, "yah.dev");
1453        assert_eq!(plan.name, "yah.dev");
1454        assert_eq!(
1455            plan.origins
1456                .iter()
1457                .map(|o| o.address.to_string())
1458                .collect::<Vec<_>>(),
1459            vec!["15.204.89.240", "51.81.85.145"],
1460        );
1461    }
1462
1463    /// The live-DNS blast radius this arm exists to bound: an empty desired
1464    /// set is what the applier prunes against, so rendering it would withdraw
1465    /// every A record at the apex. Nothing about "no machine declared it"
1466    /// means "take the site down" — it must be an error, not a wipe.
1467    #[test]
1468    fn empty_origin_set_is_an_error_not_an_apex_wipe() {
1469        let err = plan_domain_passway(&passway_manifest(), ResolvedOrigins::default(), true).unwrap_err();
1470        let msg = format!("{err:#}");
1471        assert!(msg.contains("refusing to render an empty apex"), "{msg}");
1472        assert!(msg.contains("yah.dev"), "{msg}");
1473    }
1474
1475    #[test]
1476    fn plan_bails_when_front_door_is_not_passway() {
1477        for door in [FrontDoor::BucketDirect, FrontDoor::Worker] {
1478            let mut dom = passway_manifest();
1479            dom.front_door = door;
1480            let err = plan_domain_passway(&dom, origins(&["us-east-001"]), true).unwrap_err();
1481            let msg = format!("{err:#}");
1482            assert!(msg.contains("front_door"), "{msg}");
1483            assert!(msg.contains(door.as_str()), "{msg}");
1484        }
1485    }
1486
1487    #[test]
1488    fn converged_apex_is_a_no_op() {
1489        let plan = plan_of(&["us-east-001", "us-west-001"]);
1490        let diff = diff_apex_records(
1491            &plan,
1492            &live(&[("51.81.85.145", false), ("15.204.89.240", false)]),
1493        );
1494        assert_eq!(diff, ApexRecordDiff::default());
1495        assert!(diff.is_converged());
1496    }
1497
1498    #[test]
1499    fn adding_a_machine_yields_exactly_one_added_record_and_no_prune() {
1500        let plan = plan_of(&["us-east-001", "us-west-001"]);
1501        let diff = diff_apex_records(&plan, &live(&[("51.81.85.145", false)]));
1502        assert_eq!(diff.upsert, vec!["15.204.89.240".to_string()]);
1503        assert!(diff.prune.is_empty(), "{diff:?}");
1504    }
1505
1506    #[test]
1507    fn removing_a_machine_yields_exactly_one_prune_and_leaves_the_survivor() {
1508        let plan = plan_of(&["us-east-001"]);
1509        let diff = diff_apex_records(
1510            &plan,
1511            &live(&[("51.81.85.145", false), ("15.204.89.240", false)]),
1512        );
1513        assert_eq!(diff.prune, vec!["15.204.89.240".to_string()]);
1514        assert!(
1515            diff.upsert.is_empty(),
1516            "the survivor is already correct: {diff:?}"
1517        );
1518    }
1519
1520    /// `scripts/cf-apex-mode.sh orange` is break-glass; the manifest declares
1521    /// DNS-only. A proxied record carrying a desired address is therefore
1522    /// drift, and re-upserting it (`proxied = false`) is the convergence.
1523    #[test]
1524    fn a_proxied_record_at_a_desired_address_is_rewritten_not_left_alone() {
1525        let plan = plan_of(&["us-east-001"]);
1526        let diff = diff_apex_records(&plan, &live(&[("51.81.85.145", true)]));
1527        assert_eq!(diff.upsert, vec!["51.81.85.145".to_string()]);
1528        assert!(
1529            diff.prune.is_empty(),
1530            "the address is declared — it must not be pruned: {diff:?}"
1531        );
1532    }
1533
1534    /// **Fail-closed on withdrawal.** `collate_workspace_ingress` returns `Ok`
1535    /// while SKIPPING an edge whose declaration fails to plan, so a passway
1536    /// edge with a config typo silently drops its machine from `front_doors` —
1537    /// which, from inside the plan, is indistinguishable from the operator
1538    /// withdrawing that node. Without this gate a typo would render as a DNS
1539    /// withdrawal and take that origin offline. Upserts must still run: an
1540    /// unrelated service's broken declaration cannot be allowed to block
1541    /// growing the fleet, and an upsert can never make the apex worse.
1542    #[test]
1543    fn an_incomplete_collation_upserts_but_withholds_every_prune() {
1544        // us-west-001 is missing from the front-door set, and the collation
1545        // reported problems — so its absence is not trustworthy evidence of a
1546        // withdrawal. us-east-001 is newly declared and must still be written.
1547        let plan = plan_of_incomplete(&["us-east-001"]);
1548        let diff = diff_apex_records(&plan, &live(&[("15.204.89.240", false)]));
1549
1550        assert_eq!(
1551            diff.upsert,
1552            vec!["51.81.85.145".to_string()],
1553            "additions must still land through a dirty collation"
1554        );
1555        assert!(
1556            diff.prune.is_empty(),
1557            "a live record must never be withdrawn on an incomplete collation: {diff:?}"
1558        );
1559        assert_eq!(
1560            diff.withheld_prune,
1561            vec!["15.204.89.240".to_string()],
1562            "the withheld record is reported so the applier can name it"
1563        );
1564
1565        // Same inputs, clean collation: the prune is real. This is the
1566        // control that proves the gate is what changed the outcome.
1567        let trusted = plan_of(&["us-east-001"]);
1568        let diff = diff_apex_records(&trusted, &live(&[("15.204.89.240", false)]));
1569        assert_eq!(diff.prune, vec!["15.204.89.240".to_string()]);
1570        assert!(diff.withheld_prune.is_empty(), "{diff:?}");
1571    }
1572
1573    /// The empty-set guard stays AHEAD of the incompleteness gate: an empty
1574    /// origin set is unusable whether or not the collation was clean, so it
1575    /// remains the louder failure rather than degrading into a silent
1576    /// everything-withheld apply.
1577    #[test]
1578    fn empty_origin_set_still_errors_on_an_incomplete_collation() {
1579        let err = plan_domain_passway(&passway_manifest(), ResolvedOrigins::default(), false).unwrap_err();
1580        assert!(
1581            format!("{err:#}").contains("refusing to render an empty apex"),
1582            "got: {err:#}"
1583        );
1584    }
1585
1586    // ── R859-F2: health-excluded origins ──────────────────────────────────
1587
1588    /// The exclusion happens after full resolution, and reports what it held
1589    /// back rather than dropping it silently — `health_withdrawn` is what makes
1590    /// "declared but dead" distinguishable from "never declared" downstream.
1591    #[test]
1592    fn a_confirmed_down_machine_is_resolved_then_withheld_not_dropped() {
1593        let got = origins_excluding(&["us-east-001", "us-west-001"], &["us-east-001"]);
1594        assert_eq!(
1595            got.origins,
1596            vec![PasswayOrigin {
1597                machine: "us-west-001".into(),
1598                address: Ipv4Addr::new(15, 204, 89, 240),
1599            }]
1600        );
1601        assert_eq!(
1602            got.health_withdrawn,
1603            vec![PasswayOrigin {
1604                machine: "us-east-001".into(),
1605                address: Ipv4Addr::new(51, 81, 85, 145),
1606            }],
1607            "a withheld origin must still be reported, with the address it would have published"
1608        );
1609    }
1610
1611    /// An exclusion naming a machine that fronts nothing is not an error. The
1612    /// caller's liveness view covers the whole fleet and most of it never
1613    /// fronts anything, so requiring the sets to line up would make every
1614    /// unrelated node failure an apex-render failure.
1615    #[test]
1616    fn excluding_a_machine_that_fronts_nothing_is_a_no_op() {
1617        let got = origins_excluding(&["us-east-001"], &["us-west-001", "ghost-001"]);
1618        assert_eq!(got.origins.len(), 1);
1619        assert!(got.health_withdrawn.is_empty());
1620    }
1621
1622    /// The catastrophic case, and the reason the empty-set guard is checked
1623    /// against the *survivors*: "every front door is down" must not render as
1624    /// "withdraw every A record". A dead origin still in DNS is a partial
1625    /// outage; an empty apex is a total one.
1626    #[test]
1627    fn excluding_every_origin_is_an_error_not_an_apex_wipe() {
1628        let resolved = origins_excluding(
1629            &["us-east-001", "us-west-001"],
1630            &["us-east-001", "us-west-001"],
1631        );
1632        assert_eq!(resolved.health_withdrawn.len(), 2);
1633        let err = plan_domain_passway(&passway_manifest(), resolved, true).unwrap_err();
1634        assert!(
1635            format!("{err:#}").contains("refusing to render an empty apex"),
1636            "got: {err:#}"
1637        );
1638    }
1639
1640    /// Two machines can share one address (a floating IP mid-move, or two edges
1641    /// collated onto one box). Losing one of them must not withdraw a record
1642    /// the other still answers on.
1643    #[test]
1644    fn an_address_a_live_origin_still_serves_is_not_withdrawn() {
1645        let fleet = vec![
1646            machine("us-east-001", Some("51.81.85.145"), &["public-ip"]),
1647            machine("us-east-002", Some("51.81.85.145"), &["public-ip"]),
1648        ];
1649        let resolved = public_origins(
1650            &["us-east-001".to_string(), "us-east-002".to_string()],
1651            &fleet,
1652            &["us-east-001".to_string()],
1653        )
1654        .unwrap();
1655        let plan = plan_domain_passway(&passway_manifest(), resolved, true).unwrap();
1656        assert_eq!(
1657            plan.origins
1658                .iter()
1659                .map(|o| o.address.to_string())
1660                .collect::<Vec<_>>(),
1661            vec!["51.81.85.145"]
1662        );
1663        assert!(
1664            plan.health_withdrawn.is_empty(),
1665            "us-east-002 still answers on that address, so it must not be pruned"
1666        );
1667    }
1668
1669    /// **The cross-product.** `origins_complete` and `health_withdrawn` are two
1670    /// different facts, and the bottom-right cell is the whole point of R859-F2:
1671    /// an unrelated service's broken declaration must not veto a withdrawal
1672    /// resting on a box we watched go down.
1673    #[test]
1674    fn health_withdrawal_and_declaration_completeness_are_independent() {
1675        let live = live(&[("51.81.85.145", false), ("15.204.89.240", false)]);
1676
1677        // complete + healthy: an ordinary withdrawal from a trusted declaration.
1678        let complete_healthy = plan_domain_passway(
1679            &passway_manifest(),
1680            origins(&["us-west-001"]),
1681            true,
1682        )
1683        .unwrap();
1684        let d = diff_apex_records(&complete_healthy, &live);
1685        assert_eq!(d.prune, vec!["51.81.85.145"]);
1686        assert!(d.withheld_prune.is_empty());
1687
1688        // complete + down: the same prune, now on health grounds.
1689        let complete_down = plan_domain_passway(
1690            &passway_manifest(),
1691            origins_excluding(&["us-east-001", "us-west-001"], &["us-east-001"]),
1692            true,
1693        )
1694        .unwrap();
1695        let d = diff_apex_records(&complete_down, &live);
1696        assert_eq!(d.prune, vec!["51.81.85.145"]);
1697        assert!(d.withheld_prune.is_empty());
1698
1699        // incomplete + healthy: might be a withdrawal, might be a typo — withheld.
1700        let incomplete_healthy = plan_domain_passway(
1701            &passway_manifest(),
1702            origins(&["us-west-001"]),
1703            false,
1704        )
1705        .unwrap();
1706        let d = diff_apex_records(&incomplete_healthy, &live);
1707        assert!(
1708            d.prune.is_empty(),
1709            "an absence under an incomplete collation must never prune"
1710        );
1711        assert_eq!(d.withheld_prune, vec!["51.81.85.145"]);
1712
1713        // incomplete + down: PRUNED ANYWAY. We saw this machine declared and we
1714        // saw it die; the incompleteness is about a different declaration.
1715        let incomplete_down = plan_domain_passway(
1716            &passway_manifest(),
1717            origins_excluding(&["us-east-001", "us-west-001"], &["us-east-001"]),
1718            false,
1719        )
1720        .unwrap();
1721        let d = diff_apex_records(&incomplete_down, &live);
1722        assert_eq!(
1723            d.prune,
1724            vec!["51.81.85.145"],
1725            "a health withdrawal rests on a positive observation, not on an absence, so \
1726             origins_complete = false must not suppress it"
1727        );
1728        assert!(d.withheld_prune.is_empty());
1729    }
1730
1731    /// The two reasons for a prune coexist in one diff without merging: under an
1732    /// incomplete collation, the health-excluded address is pruned and the
1733    /// merely-absent one is still withheld.
1734    #[test]
1735    fn an_incomplete_collation_prunes_only_the_health_withdrawn_surplus() {
1736        let plan = plan_domain_passway(
1737            &passway_manifest(),
1738            origins_excluding(&["us-east-001", "us-west-001"], &["us-east-001"]),
1739            false,
1740        )
1741        .unwrap();
1742        let d = diff_apex_records(
1743            &plan,
1744            &live(&[
1745                ("51.81.85.145", false), // declared + confirmed down -> prune
1746                ("15.204.89.240", false), // the surviving origin -> kept
1747                ("203.0.113.9", false),  // never declared at all -> withheld
1748            ]),
1749        );
1750        assert_eq!(d.prune, vec!["51.81.85.145"]);
1751        assert_eq!(d.withheld_prune, vec!["203.0.113.9"]);
1752        assert!(d.upsert.is_empty());
1753    }
1754
1755    /// A withheld prune is not convergence — it is a write the applier
1756    /// declined — but it is also not a write, so `is_converged` stays false
1757    /// only when there is something to actually do.
1758    #[test]
1759    fn a_withheld_prune_alone_leaves_nothing_to_write() {
1760        let plan = plan_of_incomplete(&["us-east-001"]);
1761        let diff = diff_apex_records(
1762            &plan,
1763            &live(&[("51.81.85.145", false), ("15.204.89.240", false)]),
1764        );
1765        assert!(diff.upsert.is_empty(), "{diff:?}");
1766        assert!(diff.prune.is_empty(), "{diff:?}");
1767        assert!(diff.is_converged(), "no write to make: {diff:?}");
1768        assert_eq!(diff.withheld_prune, vec!["15.204.89.240".to_string()]);
1769    }
1770
1771    /// Ordering contract (`cf-apex-mode.sh:245-246,287-288`): the applier
1772    /// writes `upsert` before `prune`, so a full origin swap never leaves the
1773    /// apex without a routing record.
1774    #[test]
1775    fn a_full_origin_swap_writes_the_new_record_before_pruning_the_old() {
1776        let plan = plan_of(&["us-west-001"]);
1777        let diff = diff_apex_records(&plan, &live(&[("51.81.85.145", false)]));
1778        assert_eq!(diff.upsert, vec!["15.204.89.240".to_string()]);
1779        assert_eq!(diff.prune, vec!["51.81.85.145".to_string()]);
1780        assert!(!diff.is_converged());
1781    }
1782}