ryu-mesh 0.1.9

Mesh read/shape primitive for Ryu (#478 P5–P7): the read side of the optional Tailscale/Headscale plane. Owns `RYU_MESH_ENABLED` gating, the `GET /api/mesh/status` (Contract 6) shaping from `tailscale status --json`, the fail-closed shared-mesh-token bearer resolution for `GET /api/mesh/peers` (the node-admittance security model this crate anchors — `enforce_remote_auth` stays in Core and consults `is_insecure_auth_token_placeholder` here), and the Funnel helpers P6 consumes for public webhook ingress. An extracted Core capability crate; in-process by default and consumed as a NON-optional path dependency (the fail-closed startup gate reads `is_enabled()`/`is_insecure_auth_token_placeholder` unconditionally). The one kernel coupling — the `tailscale`/`tailscaled` process shell-outs (the 'what runs' half of the mesh, a Sidecar in Core) — inverts through the narrow `MeshHost` trait so this crate has ZERO dependency on apps/core.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
//! Mesh status + Funnel helpers (P5 of the unified-tool-gateway epic, #478).
//!
//! Extracted from `apps/core/src/mesh` into its own primitive crate (in-process
//! default preserved — every entry point is a plain function call, never IPC).
//!
//! Core owns **what runs** — the optional Tailscale/Headscale daemon (a `Sidecar`
//! managed by the `SidecarManager`, `apps/core/src/sidecar/tailscale.rs`). This
//! crate is the **read/shape side**: it shapes `tailscale status --json` into the
//! canonical `GET /api/mesh/status` contract (Appendix A Contract 6 of
//! `docs/unified-tool-gateway-spec.md`), resolves the fail-closed shared-mesh-token
//! bearer for `GET /api/mesh/peers`, and exposes the `ensure_funnel`/`funnel_url`
//! primitives P6 consumes for public webhook ingress.
//!
//! The one kernel coupling — the `tailscale`/`tailscaled` process shell-outs —
//! inverts through the narrow [`MeshHost`] trait (host shim implemented Core-side
//! in `apps/core/src/mesh_host.rs`, installed once at boot via [`set_global_host`],
//! mirroring the `CryptoHost`/`RecipesHost` precedent). So this crate has ZERO
//! dependency on apps/core.
//!
//! The mesh is **opt-in**. The enabled signal is `RYU_MESH_ENABLED` (env) OR the
//! `mesh-enabled` pref (seeded by Core at boot into [`set_pref_enabled`] — the
//! desktop's Gateway → Integrations toggle writes the pref through
//! `POST /api/mesh/config`). The env wins when set (operator override → pref,
//! matching the `mesh-login-server`/ingress-URL precedence). When off,
//! [`query_status`] returns the all-default object (HTTP 200, never 500) WITHOUT
//! touching the host, so a build with no host installed still behaves correctly
//! for the default (mesh-disabled) install.

use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, OnceLock};

use serde::Serialize;

// ── Host seam (the "what runs" half — tailscale daemon shell-outs) ────────────

/// The kernel-side couplings this crate needs but cannot own: the three
/// `tailscale`/`tailscaled` process shell-outs (the "what runs" half of the mesh,
/// a `Sidecar` in Core). Core implements this in `apps/core/src/mesh_host.rs` and
/// installs it once at boot via [`set_global_host`].
///
/// Every method is only ever called when the mesh is **enabled**
/// (`RYU_MESH_ENABLED`); the disabled paths short-circuit before the host is
/// consulted, so a process that never installs a host still runs the default
/// (mesh-off) install correctly.
#[async_trait::async_trait]
pub trait MeshHost: Send + Sync {
    /// Run `tailscale status --json` and return the parsed JSON. Errors when the
    /// daemon is absent or returns non-JSON (the caller maps that to an
    /// enabled-but-unreachable status).
    async fn status_json(&self) -> anyhow::Result<serde_json::Value>;

    /// Ensure a Tailscale Funnel is serving `port`, returning the public URL.
    async fn ensure_funnel(&self, port: u16) -> anyhow::Result<String>;

    /// The active public Funnel URL for `port`, or `None` when unreachable.
    async fn funnel_url(&self, port: u16) -> Option<String>;
}

fn host_slot() -> &'static OnceLock<Arc<dyn MeshHost>> {
    static HOST: OnceLock<Arc<dyn MeshHost>> = OnceLock::new();
    &HOST
}

/// Install the process-global [`MeshHost`]. Idempotent (a second call is a no-op).
/// Called once from Core's `main` at boot.
pub fn set_global_host(host: Arc<dyn MeshHost>) {
    let _ = host_slot().set(host);
}

/// The installed host, or `None` when none was installed. Only consulted on the
/// mesh-**enabled** paths, so `None` here means "mesh enabled but no daemon host
/// wired" — treated as unreachable, never a panic.
fn host() -> Option<Arc<dyn MeshHost>> {
    host_slot().get().cloned()
}

// ── Node-admittance security model (anchored here) ────────────────────────────

/// Whether an auth token is a well-known insecure placeholder. This is the
/// canonical home for the node-admittance placeholder check: [`resolve_mesh_bearer`]
/// refuses to hand out such a token as a peer bearer (a peer provisioned with a
/// placeholder refuses to start under mesh, so offering it would be a lie), and
/// Core's `enforce_remote_auth` startup gate consults the same predicate so both
/// agree on the same signal. Pure + const — no dependency on apps/core.
pub fn is_insecure_auth_token_placeholder(token: &str) -> bool {
    const PLACEHOLDERS: &[&str] = &[
        "CHANGE_ME",
        "CHANGEME",
        "REPLACE_ME",
        "REPLACEME",
        "YOUR_TOKEN_HERE",
        "TOKEN",
        "SECRET",
        "PASSWORD",
    ];

    let trimmed = token.trim();
    PLACEHOLDERS
        .iter()
        .any(|placeholder| trimmed.eq_ignore_ascii_case(placeholder))
}

// ── Mesh plane handle + enabled gate ──────────────────────────────────────────

/// Handle held by Core's `ServerState` for the mesh plane. Cheap to clone. Today
/// it is a stateless façade over the env-driven [`query_status`]/[`is_enabled`]
/// free functions (the daemon itself is a Sidecar managed by the
/// `SidecarManager`), but giving the server a typed handle keeps the call site
/// stable for when P6 wires Funnel-backed ingress through here.
#[derive(Clone, Default)]
pub struct MeshHandle;

impl MeshHandle {
    pub fn new() -> Self {
        Self
    }

    /// Live mesh status for `GET /api/mesh/status` (Contract 6).
    pub async fn status(&self) -> MeshStatus {
        query_status().await
    }

    /// Whether the mesh is enabled on this node.
    pub fn enabled(&self) -> bool {
        is_enabled()
    }
}

/// The pref-driven half of the mesh-enabled signal. Core seeds this once at boot
/// from its `mesh-enabled` preference (and `POST /api/mesh/config` updates it at
/// runtime), so [`is_enabled`] reads `env || pref` without an async store. Kept
/// in lockstep with the gateway's `tools::mesh_enabled()` so the loopback-trust
/// neutralization (B-9) and Core fail-closed gate agree on the same signal — the
/// gateway child is spawned with `RYU_MESH_ENABLED=1` whenever this reads true.
static MESH_PREF_ENABLED: AtomicBool = AtomicBool::new(false);

/// Seed the pref half of the mesh-enabled signal (env wins when set). Mirrors
/// the entitlement / claude-config / untrusted pref seeders in Core's `main`.
/// Also called by the runtime `POST /api/mesh/config` handler so an enable/disable
/// takes effect without a restart.
pub fn set_pref_enabled(enabled: bool) {
    MESH_PREF_ENABLED.store(enabled, Ordering::Relaxed);
}

/// Whether a string parses as a truthy mesh-enabled value — the SAME truthiness
/// [`is_enabled`] applies to `RYU_MESH_ENABLED`, exposed so Core can parse its
/// `mesh-enabled` pref with the identical semantics instead of a second copy.
pub fn parse_enabled(value: Option<&str>) -> bool {
    match value.map(|v| v.trim().to_ascii_lowercase()).as_deref() {
        None | Some("") | Some("0") | Some("false") | Some("no") => false,
        Some(_) => true,
    }
}

/// Whether the mesh is enabled for this node. Opt-in via `RYU_MESH_ENABLED`
/// (truthy = anything but empty/`0`/`false`/`no`) OR the `mesh-enabled` pref
/// (seeded into [`MESH_PREF_ENABLED`]). The env wins when SET — including an
/// explicit `RYU_MESH_ENABLED=0`, which overrides the pref — so an operator can
/// always force the mesh off; only when the env is unset does the pref decide.
/// Kept in lockstep with the gateway's `tools::mesh_enabled()` so the
/// loopback-trust neutralization (B-9) and Core fail-closed gate agree on the
/// same signal.
pub fn is_enabled() -> bool {
    match std::env::var("RYU_MESH_ENABLED").ok() {
        Some(v) => parse_enabled(Some(&v)),
        None => MESH_PREF_ENABLED.load(Ordering::Relaxed),
    }
}

/// A peer node on the tailnet, as surfaced in Contract 6. Carries both the P7
/// fields (`name`, `host_or_dns`) and the P5 fields (`magic_dns_name`,
/// `tailscale_ips`, `os`).
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
pub struct MeshPeer {
    pub name: String,
    pub host_or_dns: String,
    pub magic_dns_name: String,
    pub tailscale_ips: Vec<String>,
    pub online: bool,
    pub os: String,
}

/// The canonical `GET /api/mesh/status` superset (Contract 6). snake_case keys;
/// `reachable` and `up` are both present and equal. `enabled:false` ⇒ all-default.
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
pub struct MeshStatus {
    pub enabled: bool,
    pub reachable: bool,
    /// `up == reachable` — both present in the wire shape per Contract 6.
    pub up: bool,
    /// `"tailscale"` | `"headscale"` | `null`.
    pub backend: Option<String>,
    /// Raw `BackendState` string from `tailscale status --json` (e.g.
    /// `"Running"`, `"NeedsLogin"`, `"Stopped"`).
    pub backend_state: String,
    /// Control-plane server URL (Headscale → its login server; Tailscale SaaS →
    /// the coordination server). `null` when unknown.
    pub control_server: Option<String>,
    pub magic_dns_name: Option<String>,
    pub tailscale_ips: Vec<String>,
    pub peers: Vec<MeshPeer>,
    /// Independent of mesh — P7 reads the ingress mode from
    /// `/api/webhook-ingress/status`, not here. Always `null` in this object.
    pub webhook_ingress_mode: Option<String>,
}

impl Default for MeshStatus {
    fn default() -> Self {
        Self {
            enabled: false,
            reachable: false,
            up: false,
            backend: None,
            backend_state: "Stopped".to_owned(),
            control_server: None,
            magic_dns_name: None,
            tailscale_ips: Vec::new(),
            peers: Vec::new(),
            webhook_ingress_mode: None,
        }
    }
}

/// The default control server for Tailscale's SaaS coordination plane. A
/// `control_server` that is empty or this host classifies the backend as
/// `tailscale`; anything else (a self-hosted `--login-server`) is `headscale`.
const TAILSCALE_SAAS_CONTROL: &str = "controlplane.tailscale.com";

/// Classify the mesh backend from the control server URL. A Headscale install is
/// reached via `--login-server <url>`; Tailscale's SaaS uses its own coordination
/// server. When no control URL is reported (the caller passes `None` — the URL is
/// absent or was filtered out as empty), the backend stays `null`: a valid
/// Contract 6 value, since we cannot distinguish Tailscale from Headscale without
/// it.
fn classify_backend(control_url: Option<&str>) -> Option<String> {
    match control_url {
        None => None,
        Some(url) if url.contains(TAILSCALE_SAAS_CONTROL) => Some("tailscale".to_owned()),
        Some(_) => Some("headscale".to_owned()),
    }
}

/// Parse the JSON emitted by `tailscale status --json` into a [`MeshStatus`].
///
/// `enabled` is supplied by the caller (it reflects `RYU_MESH_ENABLED`, not the
/// daemon). The shape is defensive: missing fields degrade to the defaults so a
/// `NeedsLogin` daemon never panics this path.
pub fn parse_status_json(enabled: bool, raw: &serde_json::Value) -> MeshStatus {
    let backend_state = raw
        .get("BackendState")
        .and_then(|v| v.as_str())
        .unwrap_or("Stopped")
        .to_owned();
    let reachable = backend_state == "Running";

    // Control plane. The precedence here was originally written the other way
    // round — it read `ControlURL` and dismissed `CurrentTailnet` as "absent on
    // Headscale". Verified against a real Headscale v0.29 tailnet, it is the
    // exact opposite: `ControlURL` is absent (null under both `Self` and the top
    // level) while `CurrentTailnet.Name` carries the control host. The result was
    // that `control_server` — and therefore `backend`, which is classified from
    // it — came back null on every Headscale node, so the desktop could never
    // tell a self-hosted tailnet from Tailscale SaaS. Try both, ControlURL first
    // (it is the more specific value when a client does report it).
    let control_server = raw
        .get("Self")
        .and_then(|s| s.get("ControlURL"))
        .and_then(|v| v.as_str())
        .or_else(|| raw.get("ControlURL").and_then(|v| v.as_str()))
        .or_else(|| {
            raw.get("CurrentTailnet")
                .and_then(|t| t.get("Name"))
                .and_then(|v| v.as_str())
        })
        .filter(|s| !s.is_empty())
        .map(str::to_owned);

    let backend = if backend_state == "Stopped" || backend_state == "NoState" {
        None
    } else {
        classify_backend(control_server.as_deref())
    };

    let self_node = raw.get("Self");
    let magic_dns_name = self_node
        .and_then(|s| s.get("DNSName"))
        .and_then(|v| v.as_str())
        .map(|s| s.trim_end_matches('.').to_owned())
        .filter(|s| !s.is_empty());
    let tailscale_ips = self_node
        .and_then(|s| s.get("TailscaleIPs"))
        .and_then(|v| v.as_array())
        .map(|arr| {
            arr.iter()
                .filter_map(|v| v.as_str().map(str::to_owned))
                .collect()
        })
        .unwrap_or_default();

    let peers = raw
        .get("Peer")
        .and_then(|v| v.as_object())
        .map(|map| map.values().map(parse_peer).collect::<Vec<_>>())
        .unwrap_or_default();

    MeshStatus {
        enabled,
        reachable,
        up: reachable,
        backend,
        backend_state,
        control_server,
        magic_dns_name,
        tailscale_ips,
        peers,
        webhook_ingress_mode: None,
    }
}

/// Map one entry of the `Peer` map into a [`MeshPeer`]. The MagicDNS name has its
/// trailing `.` stripped; `host_or_dns` prefers the MagicDNS name and falls back
/// to the first Tailscale IP so P7 always has something to dial.
fn parse_peer(peer: &serde_json::Value) -> MeshPeer {
    let dns = peer
        .get("DNSName")
        .and_then(|v| v.as_str())
        .map(|s| s.trim_end_matches('.').to_owned())
        .unwrap_or_default();
    let host = peer
        .get("HostName")
        .and_then(|v| v.as_str())
        .unwrap_or_default()
        .to_owned();
    let tailscale_ips: Vec<String> = peer
        .get("TailscaleIPs")
        .and_then(|v| v.as_array())
        .map(|arr| {
            arr.iter()
                .filter_map(|v| v.as_str().map(str::to_owned))
                .collect()
        })
        .unwrap_or_default();
    let online = peer
        .get("Online")
        .and_then(|v| v.as_bool())
        .unwrap_or(false);
    let os = peer
        .get("OS")
        .and_then(|v| v.as_str())
        .unwrap_or_default()
        .to_owned();

    // host_or_dns: prefer MagicDNS, then the first Tailscale IP, then HostName.
    let host_or_dns = if !dns.is_empty() {
        dns.clone()
    } else if let Some(ip) = tailscale_ips.first() {
        ip.clone()
    } else {
        host.clone()
    };
    // name: prefer HostName, fall back to the leftmost MagicDNS label.
    let name = if !host.is_empty() {
        host
    } else {
        dns.split('.').next().unwrap_or_default().to_owned()
    };

    MeshPeer {
        name,
        host_or_dns,
        magic_dns_name: dns,
        tailscale_ips,
        online,
        os,
    }
}

/// Query the live mesh status. When the mesh is disabled this returns the
/// all-default object without shelling out (HTTP 200, never 500) and WITHOUT
/// consulting the host. When enabled but the daemon is absent/erroring (or no
/// host is installed), it returns an enabled-but-unreachable object so the
/// desktop can render an amber "configured but down" state.
pub async fn query_status() -> MeshStatus {
    let enabled = is_enabled();
    if !enabled {
        return MeshStatus::default();
    }
    let Some(h) = host() else {
        // Mesh enabled but no daemon host wired — treat as unreachable, never
        // panic. (Core installs the host at boot; this is the defensive path.)
        return MeshStatus {
            enabled: true,
            ..Default::default()
        };
    };
    match h.status_json().await {
        Ok(raw) => parse_status_json(true, &raw),
        Err(e) => {
            tracing::debug!("mesh: status query failed: {e}");
            MeshStatus {
                enabled: true,
                ..Default::default()
            }
        }
    }
}

/// Ensure a Tailscale Funnel is serving `port` to the public internet, returning
/// the public HTTPS URL. Consumed by P6's `TailscaleFunnelSource`.
///
/// Requires the mesh to be enabled and the daemon running with HTTPS certs
/// provisioned; otherwise returns a clear error so the ingress seam can fall back
/// or surface the reason.
pub async fn ensure_funnel(port: u16) -> anyhow::Result<String> {
    if !is_enabled() {
        anyhow::bail!("mesh disabled: set RYU_MESH_ENABLED to use Tailscale Funnel");
    }
    let h = host().ok_or_else(|| anyhow::anyhow!("mesh host not installed"))?;
    h.ensure_funnel(port).await
}

/// The public Funnel URL for `port` if one is active, else `None`. Cheap read
/// (no mutation) used by P6's status surface.
pub async fn funnel_url(port: u16) -> Option<String> {
    if !is_enabled() {
        return None;
    }
    host()?.funnel_url(port).await
}

// ── Peer token bridge (#478, P7 desktop NodeSelector handoff) ─────────────────
//
// Adding a mesh peer as a node is fail-closed: every exposed peer runs
// `enforce_remote_auth`, so its protected routes 401 without a valid bearer. The
// desktop's `addNode(name, url)` is tokenless, which is exactly why a freshly
// added peer's requests bounce. This seam provides the bearer WITHOUT weakening
// the peer's check: the peer still requires a valid token; we hand the caller one.
//
// The bearer we can offer is **this node's own `RYU_TOKEN`**. `require_auth` on the
// peer is a string compare (`provided == expected`), and `enforce_remote_auth` on
// the peer accepts any non-placeholder token at startup — so this node's token
// authenticates on a peer **iff that peer was provisioned with the same
// `RYU_TOKEN`** (the shared-fleet convention: a tailnet operator gives every node
// the same node-admittance secret). The code cannot verify the peer's token, so
// `bearer_source: "shared-mesh-token"` means "candidate bearer, valid on peers
// sharing this RYU_TOKEN"; a peer running a distinct token still 401s and the
// operator must supply that peer's token by hand. Returning this token is not a
// disclosure: `/api/mesh/peers` sits behind `require_auth`, so only a caller who
// already holds this node's `RYU_TOKEN` can read it back.

/// How the offered bearer was derived, surfaced so the desktop (and a human) know
/// whether the token is a real candidate or absent.
pub const BEARER_SOURCE_SHARED: &str = "shared-mesh-token";
pub const BEARER_SOURCE_NONE: &str = "none";

/// Provisioning guidance returned when no usable bearer exists on this node. Names
/// the EXACT secret a peer must share for the fail-closed check to pass.
pub const BEARER_NONE_NOTE: &str =
    "No usable RYU_TOKEN on this node. Provision every mesh node with the SAME strong \
     RYU_TOKEN (the shared node-admittance secret) so a peer's require_auth accepts it; \
     otherwise supply the target peer's own RYU_TOKEN when adding it.";

/// The default Core listen port peers are assumed to serve on (`127.0.0.1:7980`
/// default bind, reached over the tailnet on the same port). Overridable per
/// deployment via `RYU_MESH_PEER_PORT` when the fleet binds a non-default port.
const DEFAULT_CORE_PORT: u16 = 7980;

/// Resolve the port peers are dialed on: `RYU_MESH_PEER_PORT` when set to a valid
/// `u16`, else the default 7980.
fn peer_core_port() -> u16 {
    std::env::var("RYU_MESH_PEER_PORT")
        .ok()
        .and_then(|v| v.trim().parse::<u16>().ok())
        .unwrap_or(DEFAULT_CORE_PORT)
}

/// Build the URL the desktop should register for a peer. Prefers the MagicDNS
/// name (stable, resolvable inside the tailnet), falling back to `host_or_dns`
/// (which itself falls back to a Tailscale IP). `http://` is correct: the tailnet
/// wire is WireGuard-encrypted and Core does not serve TLS itself.
fn peer_url(peer: &MeshPeer, port: u16) -> String {
    let host = if peer.magic_dns_name.is_empty() {
        peer.host_or_dns.as_str()
    } else {
        peer.magic_dns_name.as_str()
    };
    format!("http://{host}:{port}")
}

/// Resolve the candidate bearer to hand the desktop from this node's node token
/// (`RYU_TOKEN`, passed in). Returns `None` — meaning "no usable bearer" — when the
/// token is absent, empty/whitespace, or a known insecure placeholder (a peer with
/// a placeholder token refuses to start under mesh, so offering it would be a lie).
///
/// Pure + unit-testable: the returned string, when a peer runs the same token, is
/// exactly what that peer's `enforce_remote_auth` accepts at startup and its
/// `require_auth` compares equal against.
pub fn resolve_mesh_bearer(node_token: Option<&str>) -> Option<String> {
    let token = node_token?.trim();
    if token.is_empty() || is_insecure_auth_token_placeholder(token) {
        return None;
    }
    Some(token.to_owned())
}

/// One peer entry in the `GET /api/mesh/peers` response.
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
pub struct MeshPeerEntry {
    pub name: String,
    /// The URL to register with `addNode` — `http://<magic_dns>:<port>`.
    pub url: String,
    pub magic_dns_name: String,
    pub host_or_dns: String,
    pub port: u16,
    pub online: bool,
    pub os: String,
    /// Whether a candidate bearer is obtainable for this peer (true when this node
    /// has a usable `RYU_TOKEN` under the shared-fleet convention).
    pub bearer_available: bool,
    /// The candidate bearer to attach when adding this peer, or `null`. Same shared
    /// token for every peer; valid only on peers provisioned with this `RYU_TOKEN`.
    pub bearer: Option<String>,
}

/// The `GET /api/mesh/peers` response (Contract 6 companion, P7). `enabled:false`
/// ⇒ empty `peers`, `bearer_source:"none"`.
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
pub struct MeshPeersResponse {
    pub enabled: bool,
    pub reachable: bool,
    pub peers: Vec<MeshPeerEntry>,
    /// `"shared-mesh-token"` when a candidate bearer is offered, else `"none"`.
    pub bearer_source: String,
    /// Present only when no bearer is available: names the exact secret to
    /// provision. `null` when a bearer is offered.
    pub note: Option<String>,
}

/// Build the peers response from a live [`MeshStatus`] and this node's token.
///
/// Pure so the token-resolution + URL shaping is unit-testable without shelling out
/// to `tailscale`. Every reported peer is returned with its `online` flag (the
/// desktop filters/labels), each carrying the same shared bearer when one exists.
pub fn build_peers_response(status: &MeshStatus, node_token: Option<&str>) -> MeshPeersResponse {
    let bearer = resolve_mesh_bearer(node_token);
    let bearer_available = bearer.is_some();
    let port = peer_core_port();

    let peers = status
        .peers
        .iter()
        .map(|p| MeshPeerEntry {
            name: p.name.clone(),
            url: peer_url(p, port),
            magic_dns_name: p.magic_dns_name.clone(),
            host_or_dns: p.host_or_dns.clone(),
            port,
            online: p.online,
            os: p.os.clone(),
            bearer_available,
            bearer: bearer.clone(),
        })
        .collect();

    MeshPeersResponse {
        enabled: status.enabled,
        reachable: status.reachable,
        peers,
        bearer_source: if bearer_available {
            BEARER_SOURCE_SHARED.to_owned()
        } else {
            BEARER_SOURCE_NONE.to_owned()
        },
        note: if bearer_available {
            None
        } else {
            Some(BEARER_NONE_NOTE.to_owned())
        },
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    // Serializes get/restore of RYU_MESH_ENABLED against parallel runs (this
    // crate's own module-local lock; env vars are process-global).
    static MESH_ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
    fn lock_env() -> std::sync::MutexGuard<'static, ()> {
        MESH_ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner())
    }

    struct EnvGuard {
        prev: Option<String>,
    }
    impl EnvGuard {
        fn set(key: &str, val: &str) -> Self {
            let prev = std::env::var(key).ok();
            std::env::set_var(key, val);
            Self { prev }
        }
    }
    impl Drop for EnvGuard {
        fn drop(&mut self) {
            match &self.prev {
                Some(v) => std::env::set_var("RYU_MESH_ENABLED", v),
                None => std::env::remove_var("RYU_MESH_ENABLED"),
            }
        }
    }

    fn running_status_json() -> serde_json::Value {
        serde_json::json!({
            "BackendState": "Running",
            "Self": {
                "DNSName": "ryu-host.tailnet-x.ts.net.",
                "TailscaleIPs": ["100.64.0.1", "fd7a:115c::1"],
                "ControlURL": "https://controlplane.tailscale.com"
            },
            "Peer": {
                "nodekey:abc": {
                    "HostName": "ryu-pi",
                    "DNSName": "ryu-pi.tailnet-x.ts.net.",
                    "TailscaleIPs": ["100.64.0.8"],
                    "Online": true,
                    "OS": "macOS"
                }
            }
        })
    }

    #[test]
    fn parse_status_json_running() {
        let status = parse_status_json(true, &running_status_json());
        assert!(status.enabled);
        assert!(status.reachable);
        assert!(status.up);
        assert_eq!(status.reachable, status.up);
        assert_eq!(status.backend.as_deref(), Some("tailscale"));
        assert_eq!(status.backend_state, "Running");
        assert_eq!(
            status.magic_dns_name.as_deref(),
            Some("ryu-host.tailnet-x.ts.net")
        );
        assert_eq!(status.tailscale_ips.len(), 2);
        assert_eq!(status.peers.len(), 1);
        let peer = &status.peers[0];
        assert_eq!(peer.name, "ryu-pi");
        assert_eq!(peer.host_or_dns, "ryu-pi.tailnet-x.ts.net");
        assert_eq!(peer.magic_dns_name, "ryu-pi.tailnet-x.ts.net");
        assert_eq!(peer.tailscale_ips, vec!["100.64.0.8".to_owned()]);
        assert!(peer.online);
        assert_eq!(peer.os, "macOS");
    }

    #[test]
    fn parse_status_json_needs_login() {
        let raw = serde_json::json!({ "BackendState": "NeedsLogin", "Self": {} });
        let status = parse_status_json(true, &raw);
        assert!(status.enabled);
        assert!(!status.reachable);
        assert!(!status.up);
        assert_eq!(status.backend_state, "NeedsLogin");
        // With no control URL the backend cannot be classified yet → None
        // (defensive: we never guess a backend we can't see).
        assert!(status.backend.is_none());
        assert!(status.peers.is_empty());
        assert!(status.tailscale_ips.is_empty());
    }

    #[test]
    fn parse_status_json_headscale_backend() {
        let mut raw = running_status_json();
        raw["Self"]["ControlURL"] = serde_json::json!("https://headscale.example.org");
        let status = parse_status_json(true, &raw);
        assert_eq!(status.backend.as_deref(), Some("headscale"));
        assert_eq!(
            status.control_server.as_deref(),
            Some("https://headscale.example.org")
        );
    }

    #[test]
    fn disabled_shape_is_all_default() {
        let status = MeshStatus::default();
        assert!(!status.enabled);
        assert!(!status.reachable);
        assert!(!status.up);
        assert!(status.backend.is_none());
        assert_eq!(status.backend_state, "Stopped");
        assert!(status.control_server.is_none());
        assert!(status.magic_dns_name.is_none());
        assert!(status.tailscale_ips.is_empty());
        assert!(status.peers.is_empty());
        assert!(status.webhook_ingress_mode.is_none());
    }

    #[test]
    fn disabled_shape_serializes_to_contract6() {
        let json = serde_json::to_value(MeshStatus::default()).unwrap();
        assert_eq!(json["enabled"], serde_json::json!(false));
        assert_eq!(json["reachable"], serde_json::json!(false));
        assert_eq!(json["up"], serde_json::json!(false));
        assert_eq!(json["backend"], serde_json::Value::Null);
        assert_eq!(json["backend_state"], serde_json::json!("Stopped"));
        assert_eq!(json["control_server"], serde_json::Value::Null);
        assert_eq!(json["magic_dns_name"], serde_json::Value::Null);
        assert_eq!(json["tailscale_ips"], serde_json::json!([]));
        assert_eq!(json["peers"], serde_json::json!([]));
        assert_eq!(json["webhook_ingress_mode"], serde_json::Value::Null);
    }

    #[test]
    fn is_enabled_default_off() {
        // In the test process RYU_MESH_ENABLED is unset → off (the pref global
        // is off by default and no prior test in this process turned it on).
        if std::env::var("RYU_MESH_ENABLED").is_err() {
            assert!(!is_enabled());
        }
    }

    /// A drop guard restoring the pref global, so a pref-flipping test never
    /// leaks its value into the parallel tests of this same process.
    struct PrefGuard {
        prev: bool,
    }
    impl PrefGuard {
        fn set(v: bool) -> Self {
            let prev = MESH_PREF_ENABLED.load(Ordering::Relaxed);
            set_pref_enabled(v);
            Self { prev }
        }
    }
    impl Drop for PrefGuard {
        fn drop(&mut self) {
            set_pref_enabled(self.prev);
        }
    }

    #[test]
    fn pref_enable_drives_is_enabled_when_env_unset() {
        let _lock = lock_env();
        if std::env::var("RYU_MESH_ENABLED").is_err() {
            let _p = PrefGuard::set(true);
            assert!(is_enabled());
            set_pref_enabled(false);
            assert!(!is_enabled());
        }
    }

    #[test]
    fn env_wins_over_pref() {
        let _lock = lock_env();
        let _p = PrefGuard::set(true);
        // Env set to an explicit off wins over a pref that says on.
        let _e = EnvGuard::set("RYU_MESH_ENABLED", "0");
        assert!(!is_enabled());
        // Env set to on wins over a pref that says off.
        let _e = EnvGuard::set("RYU_MESH_ENABLED", "1");
        assert!(is_enabled());
    }

    #[test]
    fn parse_enabled_matches_env_truthiness() {
        assert!(!parse_enabled(None));
        assert!(!parse_enabled(Some("")));
        assert!(!parse_enabled(Some("0")));
        assert!(!parse_enabled(Some("false")));
        assert!(!parse_enabled(Some("FALSE")));
        assert!(!parse_enabled(Some("no")));
        assert!(parse_enabled(Some("1")));
        assert!(parse_enabled(Some("true")));
        assert!(parse_enabled(Some("yes")));
        assert!(parse_enabled(Some(" 1 ")));
    }

    #[test]
    fn peer_host_or_dns_falls_back_to_ip() {
        let peer = serde_json::json!({
            "HostName": "",
            "DNSName": "",
            "TailscaleIPs": ["100.64.0.9"],
            "Online": false,
            "OS": "linux"
        });
        let parsed = parse_peer(&peer);
        assert_eq!(parsed.host_or_dns, "100.64.0.9");
        assert!(!parsed.online);
    }

    #[test]
    fn resolve_mesh_bearer_returns_real_token() {
        // A real (non-placeholder) token is handed back verbatim — this is the
        // exact bearer a peer provisioned with the same RYU_TOKEN accepts.
        assert_eq!(
            resolve_mesh_bearer(Some("ryu_shared_secret")).as_deref(),
            Some("ryu_shared_secret")
        );
    }

    #[test]
    fn resolve_mesh_bearer_is_fail_closed_without_a_real_token() {
        // Fail-closed (crate side): the bearer resolver NEVER fabricates a token.
        // Absent, empty/whitespace, and every known placeholder resolve to None,
        // so `/api/mesh/peers` reports `bearer_source:"none"` rather than handing
        // out a bearer that would not authenticate (offering one would be a lie).
        assert!(resolve_mesh_bearer(None).is_none());
        assert!(resolve_mesh_bearer(Some("")).is_none());
        assert!(resolve_mesh_bearer(Some("   ")).is_none());
        assert!(resolve_mesh_bearer(Some("CHANGE_ME")).is_none());
        assert!(resolve_mesh_bearer(Some("change_me")).is_none());
        assert!(resolve_mesh_bearer(Some("REPLACE_ME")).is_none());
        assert!(resolve_mesh_bearer(Some("SECRET")).is_none());
    }

    #[test]
    fn placeholder_predicate_matches_known_weak_tokens() {
        // The canonical node-admittance placeholder check (Core's
        // `enforce_remote_auth` startup gate consults this same predicate).
        assert!(is_insecure_auth_token_placeholder("CHANGE_ME"));
        assert!(is_insecure_auth_token_placeholder("  changeme  "));
        assert!(is_insecure_auth_token_placeholder("PASSWORD"));
        assert!(!is_insecure_auth_token_placeholder("ryu_strong_random"));
        assert!(!is_insecure_auth_token_placeholder(""));
    }

    #[test]
    fn peers_response_carries_shared_bearer_and_urls() {
        let status = parse_status_json(true, &running_status_json());
        let resp = build_peers_response(&status, Some("ryu_shared_secret"));
        assert!(resp.enabled);
        assert_eq!(resp.bearer_source, BEARER_SOURCE_SHARED);
        assert!(resp.note.is_none());
        assert_eq!(resp.peers.len(), 1);
        let peer = &resp.peers[0];
        assert_eq!(peer.name, "ryu-pi");
        assert_eq!(peer.url, "http://ryu-pi.tailnet-x.ts.net:7980");
        assert_eq!(peer.port, 7980);
        assert!(peer.bearer_available);
        assert_eq!(peer.bearer.as_deref(), Some("ryu_shared_secret"));
    }

    #[test]
    fn peers_response_without_token_is_honest_and_documents_secret() {
        let status = parse_status_json(true, &running_status_json());
        let resp = build_peers_response(&status, None);
        assert_eq!(resp.bearer_source, BEARER_SOURCE_NONE);
        assert_eq!(resp.note.as_deref(), Some(BEARER_NONE_NOTE));
        let peer = &resp.peers[0];
        assert!(!peer.bearer_available);
        assert!(peer.bearer.is_none());
        // The peer is still returned (URL usable) so the desktop can add it and the
        // operator can attach the peer's own token manually.
        assert_eq!(peer.url, "http://ryu-pi.tailnet-x.ts.net:7980");
    }

    #[test]
    fn disabled_mesh_yields_empty_peers() {
        let resp = build_peers_response(&MeshStatus::default(), Some("ryu_shared_secret"));
        assert!(!resp.enabled);
        assert!(resp.peers.is_empty());
        // A token exists, so the source still reflects a candidate bearer even with
        // no peers to attach it to yet.
        assert_eq!(resp.bearer_source, BEARER_SOURCE_SHARED);
    }

    #[tokio::test]
    async fn disabled_query_status_never_touches_host() {
        // With mesh disabled (default in the test process), query_status returns
        // the all-default object WITHOUT a host installed — the mesh-off install
        // path must never depend on the daemon host being wired.
        let _lock = lock_env();
        let _p = PrefGuard::set(false);
        if std::env::var("RYU_MESH_ENABLED").is_err() {
            let status = query_status().await;
            assert_eq!(status, MeshStatus::default());
            // ensure_funnel bails and funnel_url is None, both without a host.
            assert!(ensure_funnel(443).await.is_err());
            assert!(funnel_url(443).await.is_none());
        }
    }
}