Skip to main content

openrtc/client/
transport_upgrade_impl.rs

1//! Rust-owned custom Iroh carrier configuration, negotiation, and lifecycle.
2//!
3//! WebRTC and MoQ are subordinate packet carriers beneath one admitted logical
4//! Iroh connection. This module does not create independent application routes.
5
6use super::*;
7
8#[cfg(all(not(target_arch = "wasm32"), feature = "iroh-carrier-core"))]
9fn native_iroh_carrier_failure_code(error: &anyhow::Error) -> &'static str {
10    let message = format!("{error:#}");
11    if message.contains("base generation changed before candidate acknowledgement")
12        || message.contains("incumbent generation changed")
13        || message.contains("replacement incumbent is stale")
14    {
15        "carrier-base-generation-stale"
16    } else if message.contains("authorization epoch changed") {
17        "carrier-authorization-stale"
18    } else if message.contains("stale before atomic commit")
19        || message.contains("became stale during atomic commit")
20    {
21        "carrier-logical-generation-stale"
22    } else {
23        "carrier-proof-failed"
24    }
25}
26
27/// Authorization captured before a bilateral carrier commit starts.
28///
29/// The responder commits first and may close its incumbent as soon as the
30/// initiator receives the committed reply. That close invalidates the live
31/// route proofs on the initiator before its local atomic commit can run. The
32/// candidate proof is already encrypted and generation-bound, so the commit
33/// fence must retain the pre-proof authorization epoch while still rejecting
34/// any admission or application-security epoch change.
35#[cfg(all(not(target_arch = "wasm32"), feature = "iroh-carrier-core"))]
36#[derive(Debug, Clone)]
37pub(super) struct NativeIrohCarrierAuthorizationFence {
38    inbound_admission_was_current: bool,
39    remote_admission_proof: Option<RemoteSessionAdmissionProof>,
40    admission_fingerprint: Option<String>,
41    application_security_epoch_fingerprint: Option<String>,
42    policy_epoch: u64,
43}
44
45#[cfg(all(not(target_arch = "wasm32"), feature = "transport-moq"))]
46fn moq_retry_attempt(failure_code: &str, attempt: u8) -> Option<u8> {
47    (failure_code == "carrier-base-generation-stale" && attempt == 1).then_some(2)
48}
49
50#[cfg(all(not(target_arch = "wasm32"), feature = "transport-moq"))]
51fn native_moq_peer_failure_code(failure_code: Option<&str>) -> (&str, &'static str) {
52    let diagnostic = failure_code.unwrap_or("peer-rejected-carrier");
53    let lifecycle = match diagnostic {
54        "carrier-base-generation-stale" => "carrier-base-generation-stale",
55        "carrier-proof-failed" => "carrier-proof-failed",
56        _ => "peer-rejected-carrier",
57    };
58    (diagnostic, lifecycle)
59}
60
61#[cfg(all(not(target_arch = "wasm32"), feature = "transport-webrtc"))]
62use crate::iroh_carrier_bootstrap::{
63    webrtc_peer_failure_code, webrtc_retry_attempt, webrtc_retry_delay,
64};
65
66#[cfg(not(target_arch = "wasm32"))]
67type NativeMainSendFuture<'a> =
68    std::pin::Pin<Box<dyn std::future::Future<Output = Result<(), String>> + Send + 'a>>;
69
70#[cfg(not(target_arch = "wasm32"))]
71fn native_control_stream_rank(stream_id: iroh::endpoint::StreamId) -> u64 {
72    let initiator_rank = match stream_id.initiator() {
73        iroh::endpoint::Side::Client => 0,
74        iroh::endpoint::Side::Server => 1,
75    };
76    stream_id.index().saturating_mul(2) + initiator_rank
77}
78
79#[cfg(not(target_arch = "wasm32"))]
80fn should_replace_native_control_stream(
81    existing: NativeControlStreamOwner,
82    incoming: NativeControlStreamOwner,
83) -> bool {
84    existing.transport_stable_id != incoming.transport_stable_id
85        || incoming.stream_rank < existing.stream_rank
86}
87
88#[cfg(not(target_arch = "wasm32"))]
89const MAX_REPLACED_NATIVE_CONTROL_DRAIN_FRAMES: u8 = 16;
90
91#[cfg(not(target_arch = "wasm32"))]
92fn should_drain_replaced_native_control_frame(drained_frames: u8) -> bool {
93    drained_frames < MAX_REPLACED_NATIVE_CONTROL_DRAIN_FRAMES
94}
95
96#[cfg(not(target_arch = "wasm32"))]
97pub(crate) async fn retire_losing_native_control_candidate(
98    send: iroh::endpoint::SendStream,
99    recv: iroh::endpoint::RecvStream,
100) -> std::io::Result<()> {
101    let delivery = crate::application_crypto_streams::PeerSendStream::plain(send)
102        .finish_and_wait_for_peer(std::time::Duration::from_secs(2))
103        .await;
104    drop(recv);
105    delivery
106}
107
108#[cfg(not(target_arch = "wasm32"))]
109pub(crate) async fn retire_replaced_native_control_send(
110    send: Arc<tokio::sync::Mutex<iroh::endpoint::SendStream>>,
111) -> std::io::Result<()> {
112    let mut send = send.lock().await;
113    send.finish()
114        .map_err(|error| std::io::Error::other(format!("finish replaced stream: {error}")))?;
115    match tokio::time::timeout(std::time::Duration::from_secs(2), send.stopped()).await {
116        Ok(Ok(_)) => Ok(()),
117        Ok(Err(error)) => Err(std::io::Error::other(format!(
118            "wait for peer acknowledgement: {error}"
119        ))),
120        Err(_) => Err(std::io::Error::new(
121            std::io::ErrorKind::TimedOut,
122            "timed out waiting for peer acknowledgement",
123        )),
124    }
125}
126
127#[cfg(not(target_arch = "wasm32"))]
128#[derive(Debug, Clone, Copy, PartialEq, Eq)]
129enum NativeBleSwitchAdmission {
130    Ready,
131    Wait,
132    Reject,
133}
134
135#[cfg(not(target_arch = "wasm32"))]
136const NATIVE_BLE_MAX_UPGRADE_ATTEMPTS: u8 = 3;
137
138#[cfg(not(target_arch = "wasm32"))]
139const NATIVE_BLE_RETRY_BASE_DELAY_MS: u64 = 1_000;
140
141#[cfg(not(target_arch = "wasm32"))]
142const NATIVE_BLE_SWITCH_RESPONSE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(15);
143
144#[cfg(not(target_arch = "wasm32"))]
145const NATIVE_BLE_COMPLETION_GATE_PREFIX: &str = "completion:";
146
147#[cfg(not(target_arch = "wasm32"))]
148fn native_ble_retry_delay(attempt: u8, jitter_seed: u64) -> Option<std::time::Duration> {
149    if attempt >= NATIVE_BLE_MAX_UPGRADE_ATTEMPTS {
150        return None;
151    }
152    let exponent = u32::from(attempt.saturating_sub(1));
153    let base_ms = NATIVE_BLE_RETRY_BASE_DELAY_MS.saturating_mul(2_u64.saturating_pow(exponent));
154    let jitter_percent = i64::try_from(jitter_seed % 41).unwrap_or(0) - 20;
155    let jitter_ms = i128::from(base_ms) * i128::from(jitter_percent) / 100;
156    let delay_ms = (i128::from(base_ms) + jitter_ms).max(1) as u64;
157    Some(std::time::Duration::from_millis(delay_ms))
158}
159
160#[cfg(not(target_arch = "wasm32"))]
161fn native_ble_attempt_state_after_recovery_wake(
162    current: crate::client::NativeBleUpgradeAttemptState,
163    gate_active: bool,
164) -> Option<crate::client::NativeBleUpgradeAttemptState> {
165    gate_active.then_some(current)
166}
167
168#[cfg(not(target_arch = "wasm32"))]
169fn reserve_native_ble_attempt_state(
170    current: Option<crate::client::NativeBleUpgradeAttemptState>,
171    generation: crate::client::NativePeerDataGeneration,
172) -> (crate::client::NativeBleUpgradeAttemptState, Option<u8>) {
173    let mut state = current
174        .filter(|state| state.generation == generation)
175        .unwrap_or(crate::client::NativeBleUpgradeAttemptState {
176            generation,
177            attempts: 0,
178        });
179    if state.attempts >= NATIVE_BLE_MAX_UPGRADE_ATTEMPTS {
180        return (state, None);
181    }
182    state.attempts = state.attempts.saturating_add(1);
183    let attempt = state.attempts;
184    (state, Some(attempt))
185}
186
187#[cfg(not(target_arch = "wasm32"))]
188fn classify_native_ble_switch_admission(
189    block: Option<&(bool, String)>,
190) -> NativeBleSwitchAdmission {
191    match block {
192        None => NativeBleSwitchAdmission::Ready,
193        Some((true, _)) => NativeBleSwitchAdmission::Reject,
194        Some((false, _)) => NativeBleSwitchAdmission::Wait,
195    }
196}
197
198/// Native advertises the dedicated control route only after both application
199/// crypto and the current generation's admission proof are live. Keeping this
200/// as one predicate makes it difficult for a capability update to publish a
201/// stale crypto-only readiness bit during carrier recovery.
202fn dedicated_carrier_control_is_ready(
203    dedicated_carrier_control: bool,
204    application_crypto_confirmed: bool,
205    native_admission_route_ready: bool,
206) -> bool {
207    dedicated_carrier_control && application_crypto_confirmed && native_admission_route_ready
208}
209
210impl Client {
211    #[cfg(all(
212        not(target_arch = "wasm32"),
213        feature = "iroh-carrier-core",
214        any(feature = "test-harness", feature = "testing-endpoints")
215    ))]
216    pub fn native_iroh_carrier_debug_events(&self) -> Vec<serde_json::Value> {
217        self.native_iroh_carrier_debug_events
218            .read()
219            .map(|events| events.iter().cloned().collect())
220            .unwrap_or_default()
221    }
222
223    #[cfg(all(
224        not(target_arch = "wasm32"),
225        feature = "iroh-carrier-core",
226        any(feature = "test-harness", feature = "testing-endpoints")
227    ))]
228    pub(super) fn record_native_iroh_carrier_debug_event(
229        &self,
230        connection_id: &str,
231        upgrade_id: &str,
232        action: &'static str,
233        reason: Option<&str>,
234    ) {
235        // Physical multi-peer runs can emit more than 64 selector/proof events
236        // before their failure receipt is captured. Keep the original owner
237        // error available without introducing a second diagnostic registry.
238        const DEBUG_EVENT_LIMIT: usize = 256;
239        let Ok(mut events) = self.native_iroh_carrier_debug_events.write() else {
240            return;
241        };
242        if events.len() == DEBUG_EVENT_LIMIT {
243            events.pop_front();
244        }
245        events.push_back(serde_json::json!({
246            "connectionId": connection_id,
247            "upgradeId": upgrade_id,
248            "action": action,
249            "reason": reason,
250            "observedAtMs": std::time::SystemTime::now()
251                .duration_since(std::time::UNIX_EPOCH)
252                .map(|duration| duration.as_millis() as u64)
253                .unwrap_or_default(),
254        }));
255        if Self::admission_trace_enabled() {
256            eprintln!(
257                "[OpenRTC][Iroh carrier][candidate] connection_id={} upgrade_id={} action={} reason={}",
258                connection_id,
259                upgrade_id,
260                action,
261                reason.unwrap_or("none"),
262            );
263        }
264    }
265
266    #[cfg(all(
267        not(target_arch = "wasm32"),
268        feature = "iroh-carrier-core",
269        not(any(feature = "test-harness", feature = "testing-endpoints"))
270    ))]
271    pub(super) fn record_native_iroh_carrier_debug_event(
272        &self,
273        _connection_id: &str,
274        _upgrade_id: &str,
275        _action: &'static str,
276        _reason: Option<&str>,
277    ) {
278    }
279
280    #[cfg(not(target_arch = "wasm32"))]
281    pub(crate) fn send_native_main_frame<'a>(
282        &'a self,
283        connection_id: &'a str,
284        endpoint_id: iroh::EndpointId,
285        signal_type: &'a str,
286        frame_buf: &'a [u8],
287    ) -> NativeMainSendFuture<'a> {
288        Box::pin(self.send_native_main_frame_inner(
289            connection_id,
290            endpoint_id,
291            signal_type,
292            frame_buf,
293        ))
294    }
295
296    #[cfg(not(target_arch = "wasm32"))]
297    async fn send_native_main_frame_inner(
298        &self,
299        connection_id: &str,
300        endpoint_id: iroh::EndpointId,
301        signal_type: &str,
302        frame_buf: &[u8],
303    ) -> Result<(), String> {
304        use tokio::io::AsyncWriteExt;
305
306        let current_transport_stable_id = self
307            .get_connection(endpoint_id)
308            .await
309            .map(|connection| crate::transport_generation::for_connection(&connection))
310            .ok_or_else(|| format!("missing Iroh connection for endpoint {}", endpoint_id))?;
311
312        // Phase 1: use only a stream owned by the current physical generation.
313        let cached = {
314            let map = self.native_control_streams.lock().await;
315            map.get(connection_id).cloned()
316        };
317
318        if let Some(entry) = cached {
319            if entry.endpoint_id != endpoint_id
320                || entry.owner.transport_stable_id != current_transport_stable_id
321            {
322                self.evict_native_signal_stream_if_owner(
323                    connection_id,
324                    entry.owner,
325                    "cached-owner-stale",
326                )
327                .await;
328            } else {
329                let mut send_guard = entry.send.lock().await;
330                match send_guard.write_all(frame_buf).await {
331                    Ok(_) => {
332                        if let Err(flush_err) = send_guard.flush().await {
333                            eprintln!(
334                            "[SIGNAL-STREAM] flush-failed-on-cached connection_id={} signal_type={} error={}",
335                            connection_id, signal_type, flush_err,
336                        );
337                            drop(send_guard);
338                            self.evict_native_signal_stream_if_owner(
339                                connection_id,
340                                entry.owner,
341                                "cached-flush-failed",
342                            )
343                            .await;
344                        } else {
345                            eprintln!(
346                            "[SIGNAL-STREAM] wrote-on-cached connection_id={} signal_type={} bytes={}",
347                            connection_id,
348                            signal_type,
349                            frame_buf.len(),
350                        );
351                            return Ok(());
352                        }
353                    }
354                    Err(err) => {
355                        eprintln!(
356                        "[SIGNAL-STREAM] write-failed-on-cached connection_id={} signal_type={} error={}",
357                        connection_id, signal_type, err,
358                    );
359                        drop(send_guard);
360                        self.evict_native_signal_stream_if_owner(
361                            connection_id,
362                            entry.owner,
363                            "cached-write-failed",
364                        )
365                        .await;
366                    }
367                }
368            }
369        }
370
371        // Phase 2: open a fresh persistent stream.
372        let (transport_stable_id, mut send, recv) = self
373            .open_current_bi_internal_with_transport_stable_id(endpoint_id)
374            .await
375            .map_err(|e| format!("open_bi via iroh failed: {}", e))?;
376        if transport_stable_id != current_transport_stable_id {
377            return Err(format!(
378                "fresh native-main stream opened on a replacement transport generation expected={} actual={}",
379                current_transport_stable_id, transport_stable_id,
380            ));
381        }
382
383        eprintln!(
384            "[SIGNAL-STREAM] opened-persistent connection_id={} endpoint_id={} signal_type={} stream_id={}",
385            connection_id,
386            endpoint_id,
387            signal_type,
388            send.id().index(),
389        );
390
391        let label = b"main";
392        let label_len = (label.len() as u32).to_be_bytes();
393        let mut opening_buf = Vec::with_capacity(1 + 4 + label.len() + frame_buf.len());
394        opening_buf.push(0x00);
395        opening_buf.extend_from_slice(&label_len);
396        opening_buf.extend_from_slice(label);
397        opening_buf.extend_from_slice(frame_buf);
398
399        send.write_all(&opening_buf)
400            .await
401            .map_err(|e| format!("initial write failed: {}", e))?;
402        send.flush()
403            .await
404            .map_err(|e| format!("initial flush failed: {}", e))?;
405
406        eprintln!(
407            "[SIGNAL-STREAM] wrote-on-fresh connection_id={} signal_type={} bytes={}",
408            connection_id,
409            signal_type,
410            opening_buf.len(),
411        );
412
413        if !self
414            .install_native_control_stream(
415                connection_id,
416                endpoint_id,
417                transport_stable_id,
418                send,
419                recv,
420                "opened",
421            )
422            .await
423        {
424            return Err(format!(
425                "fresh native-main stream lost ownership race connection_id={} transport_stable_id={}",
426                connection_id, transport_stable_id
427            ));
428        }
429
430        Ok(())
431    }
432
433    #[cfg(not(target_arch = "wasm32"))]
434    pub(crate) async fn install_native_control_stream(
435        &self,
436        connection_id: &str,
437        endpoint_id: iroh::EndpointId,
438        transport_stable_id: u64,
439        send: iroh::endpoint::SendStream,
440        recv: iroh::endpoint::RecvStream,
441        source: &str,
442    ) -> bool {
443        let stream_id = send.id();
444        let owner = NativeControlStreamOwner {
445            transport_stable_id,
446            stream_rank: native_control_stream_rank(stream_id),
447        };
448        let current_transport_stable_id = self
449            .get_connection(endpoint_id)
450            .await
451            .map(|connection| crate::transport_generation::for_connection(&connection));
452        if current_transport_stable_id != Some(transport_stable_id) {
453            eprintln!(
454                "[OpenRTC][control] stale stream ignored connection_id={} endpoint_id={} transport_stable_id={} current_transport_stable_id={:?} stream_id={} source={}",
455                connection_id,
456                endpoint_id,
457                transport_stable_id,
458                current_transport_stable_id,
459                stream_id,
460                source,
461            );
462            return false;
463        }
464
465        // Keep ownership of a losing candidate until its already-buffered
466        // admission verdict is acknowledged. Concurrent bilateral admission
467        // can otherwise select the same canonical stream while the losing
468        // dialer observes EOF before its approval frame.
469        let mut candidate_send = Some(send);
470        let (installed, replaced_entry) = {
471            let mut streams = self.native_control_streams.lock().await;
472            let replace = streams
473                .get(connection_id)
474                .is_none_or(|existing| should_replace_native_control_stream(existing.owner, owner));
475            if replace {
476                let replaced = streams.insert(
477                    connection_id.to_string(),
478                    NativeControlStreamEntry {
479                        owner,
480                        endpoint_id,
481                        send: Arc::new(tokio::sync::Mutex::new(
482                            candidate_send
483                                .take()
484                                .expect("native control candidate must be available"),
485                        )),
486                    },
487                );
488                (true, replaced)
489            } else {
490                (false, None)
491            }
492        };
493        if !installed {
494            let delivery_result = retire_losing_native_control_candidate(
495                candidate_send
496                    .take()
497                    .expect("losing native control candidate must be available"),
498                recv,
499            )
500            .await;
501            eprintln!(
502                "[OpenRTC][control] duplicate stream retired connection_id={} endpoint_id={} transport_stable_id={} stream_id={} stream_rank={} source={} verdict_delivery={}",
503                connection_id,
504                endpoint_id,
505                transport_stable_id,
506                stream_id,
507                owner.stream_rank,
508                source,
509                if delivery_result.is_ok() { "acknowledged" } else { "unconfirmed" },
510            );
511            if let Err(error) = delivery_result {
512                eprintln!(
513                    "[OpenRTC][control] duplicate stream delivery wait ended connection_id={} endpoint_id={} transport_stable_id={} stream_id={} source={} error={}",
514                    connection_id,
515                    endpoint_id,
516                    transport_stable_id,
517                    stream_id,
518                    source,
519                    error,
520                );
521            }
522            return false;
523        }
524        let replaced_owner = replaced_entry.as_ref().map(|entry| entry.owner);
525        eprintln!(
526            "[OpenRTC][control] stream installed connection_id={} endpoint_id={} transport_stable_id={} stream_id={} stream_rank={} source={} replaced_owner={:?}",
527            connection_id,
528            endpoint_id,
529            transport_stable_id,
530            stream_id,
531            owner.stream_rank,
532            source,
533            replaced_owner,
534        );
535        if let Some(replaced) = replaced_entry {
536            let connection_id = connection_id.to_string();
537            tokio::spawn(async move {
538                let delivery_result = retire_replaced_native_control_send(replaced.send).await;
539                eprintln!(
540                    "[OpenRTC][control] replaced stream retired connection_id={} endpoint_id={} transport_stable_id={} stream_rank={} verdict_delivery={}",
541                    connection_id,
542                    replaced.endpoint_id,
543                    replaced.owner.transport_stable_id,
544                    replaced.owner.stream_rank,
545                    if delivery_result.is_ok() { "acknowledged" } else { "unconfirmed" },
546                );
547                if let Err(error) = delivery_result {
548                    eprintln!(
549                        "[OpenRTC][control] replaced stream delivery wait ended connection_id={} endpoint_id={} transport_stable_id={} stream_rank={} error={}",
550                        connection_id,
551                        replaced.endpoint_id,
552                        replaced.owner.transport_stable_id,
553                        replaced.owner.stream_rank,
554                        error,
555                    );
556                }
557            });
558        }
559        self.spawn_native_signal_stream_tasks(connection_id, endpoint_id, owner, recv);
560        true
561    }
562
563    #[cfg(not(target_arch = "wasm32"))]
564    fn spawn_native_signal_stream_tasks(
565        &self,
566        connection_id: &str,
567        endpoint_id: iroh::EndpointId,
568        owner: NativeControlStreamOwner,
569        recv: iroh::endpoint::RecvStream,
570    ) {
571        let (frame_tx, frame_rx) =
572            tokio::sync::mpsc::channel::<crate::native_protocol::ParsedMainFrame>(32);
573        let connection_id_owned = connection_id.to_string();
574        let endpoint_id_for_log = endpoint_id.to_string();
575
576        let reader_client = self.clone();
577        let reader_connection_id = connection_id_owned.clone();
578        let reader_endpoint_id = endpoint_id_for_log.clone();
579        tokio::spawn(async move {
580            reader_client
581                .read_signal_stream_loop(
582                    reader_connection_id,
583                    reader_endpoint_id,
584                    owner.transport_stable_id,
585                    recv,
586                    frame_tx,
587                )
588                .await;
589        });
590
591        let client = self.clone();
592        tokio::spawn(async move {
593            client
594                .dispatch_signal_stream_frames(
595                    connection_id_owned,
596                    endpoint_id_for_log,
597                    owner,
598                    frame_rx,
599                )
600                .await;
601        });
602    }
603
604    #[cfg(not(target_arch = "wasm32"))]
605    async fn dispatch_signal_stream_frames(
606        &self,
607        connection_id: String,
608        endpoint_id_for_log: String,
609        owner: NativeControlStreamOwner,
610        mut frame_rx: tokio::sync::mpsc::Receiver<crate::native_protocol::ParsedMainFrame>,
611    ) {
612        use crate::native_protocol::ParsedMainFrame;
613        let mut replaced_same_generation_frames = 0u8;
614        while let Some(parsed) = frame_rx.recv().await {
615            let owns_current_stream = self
616                .native_signal_stream_is_current(&connection_id, owner)
617                .await;
618            let same_transport_generation = self
619                .connection_manager
620                .current_transport_matches(&connection_id, Some(owner.transport_stable_id))
621                .await;
622            if !owns_current_stream && !same_transport_generation {
623                eprintln!(
624                    "[SIGNAL-STREAM] stale-generation dispatcher stopped connection_id={} transport_stable_id={} stream_rank={}",
625                    connection_id, owner.transport_stable_id, owner.stream_rank,
626                );
627                break;
628            }
629            if !owns_current_stream
630                && !should_drain_replaced_native_control_frame(replaced_same_generation_frames)
631            {
632                eprintln!(
633                    "[SIGNAL-STREAM] same-generation drain cap reached connection_id={} transport_stable_id={} stream_rank={} drained_frames={}",
634                    connection_id,
635                    owner.transport_stable_id,
636                    owner.stream_rank,
637                    replaced_same_generation_frames,
638                );
639                break;
640            }
641            match parsed {
642                ParsedMainFrame::TypeScriptJson(json) => {
643                    #[cfg(feature = "iroh-carrier-core")]
644                    if json.get("type").and_then(serde_json::Value::as_str)
645                        == Some(crate::iroh_carrier_bootstrap::CARRIER_BOOTSTRAP_FRAME_TYPE)
646                    {
647                        eprintln!(
648                            "[OpenRTC][Iroh carrier] dispatching bootstrap connection_id={} endpoint_id={} transport_stable_id={} stream_rank={} owns_current_stream={} same_transport_generation={}",
649                            connection_id,
650                            endpoint_id_for_log,
651                            owner.transport_stable_id,
652                            owner.stream_rank,
653                            owns_current_stream,
654                            same_transport_generation,
655                        );
656                    }
657                    self.maybe_handle_typescript_json_frame(
658                        &connection_id,
659                        Some(&endpoint_id_for_log),
660                        &json,
661                    )
662                    .await;
663                }
664                ParsedMainFrame::TypeScriptHandshake(handshake) => {
665                    // A TS handshake can legitimately arrive on the signal stream:
666                    // when a peer's main control writer is dead after a page reload,
667                    // the application-route recovery handshake is re-sent over a
668                    // fresh native-signal stream (see `refreshApplicationRouteHandshake`
669                    // "retrying over native signal route" on the TS side). Dispatch
670                    // it through the same capability / application-crypto handler the
671                    // main control reader uses (gated on existing admission) so the
672                    // app route re-establishes. Previously this frame was treated as
673                    // fatal and closed the signal stream, which tore down the WebRTC
674                    // session, stranded the peer on a transport-only route, and
675                    // blocked WebRTC promotion after a refresh.
676                    if let crate::session_token::SessionAdmission::Rejected { reason } =
677                        self.session_admission(&connection_id)
678                    {
679                        eprintln!(
680                            "[SIGNAL-STREAM] handshake-frame ignored connection_id={} endpoint_id={} reason=admission-rejected detail={}",
681                            connection_id, endpoint_id_for_log, reason,
682                        );
683                        continue;
684                    }
685                    eprintln!(
686                        "[SIGNAL-STREAM] handshake-frame dispatched connection_id={} endpoint_id={} action={}",
687                        connection_id,
688                        endpoint_id_for_log,
689                        handshake.action.as_deref().unwrap_or("<none>"),
690                    );
691                    if let Some(device_id) = handshake.claimed_device_id.as_deref() {
692                        let _ = self.bind_admitted_device(&connection_id, device_id).await;
693                    }
694                    self.maybe_handle_typescript_handshake_capabilities(
695                        &connection_id,
696                        Some(&endpoint_id_for_log),
697                        Some(owner.transport_stable_id),
698                        &handshake,
699                    )
700                    .await;
701                }
702                ParsedMainFrame::NativeMessage(message) => {
703                    let serialized = match serde_json::to_vec(&message) {
704                        Ok(serialized) => serialized,
705                        Err(error) => {
706                            eprintln!(
707                                "[SIGNAL-STREAM] native-message rejected connection_id={} endpoint_id={} reason=serialize-failed error={}",
708                                connection_id, endpoint_id_for_log, error,
709                            );
710                            continue;
711                        }
712                    };
713                    if let Err(error) = self
714                        .inspect_incoming_native_main_frame_for_transport(
715                            &connection_id,
716                            Some(&endpoint_id_for_log),
717                            None,
718                            Some(owner.transport_stable_id),
719                            &serialized,
720                        )
721                        .await
722                    {
723                        eprintln!(
724                            "[SIGNAL-STREAM] native-message rejected connection_id={} endpoint_id={} channel={} error={}",
725                            connection_id, endpoint_id_for_log, message.channel, error,
726                        );
727                    }
728                }
729                ParsedMainFrame::Opaque => {
730                    eprintln!(
731                        "[SIGNAL-STREAM] frame skipped connection_id={} endpoint_id={} reason=unsupported-on-signal-stream kind=opaque",
732                        connection_id, endpoint_id_for_log,
733                    );
734                }
735            }
736            if !owns_current_stream {
737                // Canonical-stream convergence can replace a same-generation
738                // stream while its reverse half still carries one SDP answer
739                // followed by several trickled ICE candidates. Retiring after
740                // only the first frame strands native WebRTC in ICE checking.
741                // The physical generation remains the security boundary and
742                // the replacement owner has already FINed this stream, so drain
743                // only a small, hard-bounded tail before retiring. This keeps
744                // one persistent control owner without discarding in-flight
745                // negotiation frames.
746                replaced_same_generation_frames = replaced_same_generation_frames.saturating_add(1);
747                eprintln!(
748                    "[SIGNAL-STREAM] same-generation buffered frame drained connection_id={} transport_stable_id={} stream_rank={} drained_frames={}",
749                    connection_id,
750                    owner.transport_stable_id,
751                    owner.stream_rank,
752                    replaced_same_generation_frames,
753                );
754            }
755        }
756
757        eprintln!(
758            "[SIGNAL-STREAM] dispatcher-exited connection_id={}",
759            connection_id,
760        );
761        self.evict_native_signal_stream_if_owner(&connection_id, owner, "control-stream-ended")
762            .await;
763    }
764
765    #[cfg(not(target_arch = "wasm32"))]
766    async fn read_native_main_payload_frame(
767        recv: &mut iroh::endpoint::RecvStream,
768        frame_len_buf: [u8; 4],
769        first_payload_byte: Option<u8>,
770    ) -> Result<Option<crate::native_protocol::ParsedMainFrame>, String> {
771        let frame_len = u32::from_be_bytes(frame_len_buf) as usize;
772        if frame_len == 0 {
773            return Ok(None);
774        }
775        if frame_len > 4 * 1024 * 1024 {
776            return Err(format!("frame-too-large: {}", frame_len));
777        }
778
779        let mut frame = Vec::with_capacity(frame_len);
780        if let Some(first) = first_payload_byte {
781            frame.push(first);
782        }
783        if frame.len() < frame_len {
784            let remaining = frame_len - frame.len();
785            let mut rest = vec![0u8; remaining];
786            recv.read_exact(&mut rest)
787                .await
788                .map_err(|error| format!("frame-read-failed: {}", error))?;
789            frame.extend_from_slice(&rest);
790        }
791
792        // The signal stream is a native-main stream; classify every frame and let
793        // the dispatcher decide how to handle each kind (webrtc JSON signals,
794        // recovery handshakes, etc.). Returning the parsed frame instead of erroring
795        // on non-signal kinds keeps the reader alive across a recovery handshake.
796        Ok(Some(crate::native_protocol::parse_main_frame(&frame)))
797    }
798
799    #[cfg(not(target_arch = "wasm32"))]
800    fn native_main_read_error_proves_transport_loss(error: &str) -> bool {
801        error.to_ascii_lowercase().contains("connection lost")
802    }
803
804    #[cfg(not(target_arch = "wasm32"))]
805    async fn read_native_main_json_frame(
806        recv: &mut iroh::endpoint::RecvStream,
807    ) -> Result<Option<crate::native_protocol::ParsedMainFrame>, String> {
808        let mut protocol_byte = [0u8; 1];
809        match recv.read_exact(&mut protocol_byte).await {
810            Ok(_) => {}
811            Err(error) => {
812                return Err(format!("protocol-byte-read-failed: {}", error));
813            }
814        }
815        if protocol_byte[0] != 0x00 {
816            return Err(format!("unexpected-protocol-byte: {}", protocol_byte[0]));
817        }
818
819        let mut label_len_buf = [0u8; 4];
820        recv.read_exact(&mut label_len_buf)
821            .await
822            .map_err(|error| format!("label-length-read-failed: {}", error))?;
823        let label_len = u32::from_be_bytes(label_len_buf) as usize;
824        if label_len == 0 || label_len > 256 {
825            // Persistent native-main streams write the `[0x00][u32 labelLen]["main"]`
826            // label once when the stream opens. Replies on that same stream are
827            // plain `[u32 frameLen][frame]`. If we land here, we consumed the
828            // first byte of the frame length as `protocol_byte` and the next
829            // four bytes as `label_len_buf`; rebuild the length and keep the
830            // fourth byte as the first payload byte.
831            let frame_len_buf = [
832                protocol_byte[0],
833                label_len_buf[0],
834                label_len_buf[1],
835                label_len_buf[2],
836            ];
837            return Self::read_native_main_payload_frame(
838                recv,
839                frame_len_buf,
840                Some(label_len_buf[3]),
841            )
842            .await;
843        }
844
845        let mut label_buf = vec![0u8; label_len];
846        recv.read_exact(&mut label_buf)
847            .await
848            .map_err(|error| format!("label-read-failed: {}", error))?;
849        if String::from_utf8_lossy(&label_buf) != "main" {
850            return Err(format!(
851                "unexpected-label: {}",
852                String::from_utf8_lossy(&label_buf)
853            ));
854        }
855
856        let mut frame_len_buf = [0u8; 4];
857        recv.read_exact(&mut frame_len_buf)
858            .await
859            .map_err(|error| format!("frame-length-read-failed: {}", error))?;
860        Self::read_native_main_payload_frame(recv, frame_len_buf, None).await
861    }
862
863    /// Persistent reader on the reverse half of an outbound signaling stream.
864    /// Reads [4-byte big-endian len][payload] frames, parses each as JSON, and
865    /// forwards them to the dispatcher task through a bounded channel.
866    #[cfg(not(target_arch = "wasm32"))]
867    async fn read_signal_stream_loop(
868        &self,
869        connection_id: String,
870        endpoint_id_for_log: String,
871        transport_stable_id: u64,
872        mut recv: iroh::endpoint::RecvStream,
873        frame_tx: tokio::sync::mpsc::Sender<crate::native_protocol::ParsedMainFrame>,
874    ) {
875        eprintln!(
876            "[SIGNAL-STREAM] reader-spawned connection_id={} endpoint_id={}",
877            connection_id, endpoint_id_for_log,
878        );
879
880        loop {
881            let parsed = match Self::read_native_main_json_frame(&mut recv).await {
882                Ok(Some(parsed)) => parsed,
883                Ok(None) => {
884                    eprintln!(
885                        "[SIGNAL-STREAM] reader-empty-frame connection_id={} endpoint_id={}",
886                        connection_id, endpoint_id_for_log,
887                    );
888                    continue;
889                }
890                Err(err) => {
891                    eprintln!(
892                        "[SIGNAL-STREAM] reader-closed connection_id={} endpoint_id={} reason=native-main-frame-read-failed error={}",
893                        connection_id, endpoint_id_for_log, err,
894                    );
895                    if Self::native_main_read_error_proves_transport_loss(&err) {
896                        let demoted = self
897                            .observe_native_transport_generation_lost(
898                                &connection_id,
899                                transport_stable_id,
900                                "native-main-frame-read-failed",
901                            )
902                            .await;
903                        eprintln!(
904                            "[SIGNAL-STREAM] transport-loss-observed connection_id={} transport_stable_id={} demoted={}",
905                            connection_id, transport_stable_id, demoted,
906                        );
907                    }
908                    return;
909                }
910            };
911
912            let frame_type = match &parsed {
913                crate::native_protocol::ParsedMainFrame::TypeScriptJson(json) => json
914                    .get("type")
915                    .and_then(|v| v.as_str())
916                    .unwrap_or("<none>"),
917                crate::native_protocol::ParsedMainFrame::TypeScriptHandshake(_) => "handshake",
918                crate::native_protocol::ParsedMainFrame::NativeMessage(_) => "native-message",
919                crate::native_protocol::ParsedMainFrame::Opaque => "opaque",
920            };
921            eprintln!(
922                "[SIGNAL-STREAM] reader-frame connection_id={} endpoint_id={} frame_type={}",
923                connection_id, endpoint_id_for_log, frame_type,
924            );
925            if !matches!(parsed, crate::native_protocol::ParsedMainFrame::Opaque) {
926                self.record_native_transport_protocol_activity(&connection_id, transport_stable_id);
927            }
928
929            if frame_tx.send(parsed).await.is_err() {
930                eprintln!(
931                    "[SIGNAL-STREAM] reader-closed connection_id={} endpoint_id={} reason=dispatcher-dropped",
932                    connection_id, endpoint_id_for_log,
933                );
934                return;
935            }
936        }
937    }
938
939    #[cfg(not(target_arch = "wasm32"))]
940    async fn native_signal_stream_is_current(
941        &self,
942        connection_id: &str,
943        expected_owner: NativeControlStreamOwner,
944    ) -> bool {
945        self.native_control_streams
946            .lock()
947            .await
948            .get(connection_id)
949            .is_some_and(|entry| entry.owner == expected_owner)
950    }
951
952    /// Conditional eviction is the control-stream generation barrier. Reader
953    /// tasks outlive physical replacement, so only the task that still owns the
954    /// registry entry may remove it.
955    #[cfg(not(target_arch = "wasm32"))]
956    async fn evict_native_signal_stream_if_owner(
957        &self,
958        connection_id: &str,
959        expected_owner: NativeControlStreamOwner,
960        reason: &str,
961    ) -> bool {
962        let removed = {
963            let mut map = self.native_control_streams.lock().await;
964            match map.get(connection_id) {
965                Some(entry) if entry.owner == expected_owner => {
966                    let removed = map
967                        .remove(connection_id)
968                        .expect("current native control owner must still exist");
969                    let proofs_invalidated =
970                        if self.native_admission_stream_is_persistent(connection_id) {
971                            // Keep proof invalidation atomic with exact-owner
972                            // removal. Admission installers use this same mutex
973                            // when committing their generation-bound proof, so a
974                            // duplicate reader cannot erase a newly installed
975                            // canonical route.
976                            self.invalidate_native_persistent_route_proofs_for_transport(
977                                connection_id,
978                                expected_owner.transport_stable_id,
979                            )
980                        } else {
981                            false
982                        };
983                    Some((removed.endpoint_id, proofs_invalidated))
984                }
985                _ => None,
986            }
987        };
988        if let Some((endpoint_id, proofs_invalidated)) = removed {
989            eprintln!(
990                "[SIGNAL-STREAM] evicted-current connection_id={} transport_stable_id={} stream_rank={} reason={}",
991                connection_id,
992                expected_owner.transport_stable_id,
993                expected_owner.stream_rank,
994                reason,
995            );
996            if proofs_invalidated {
997                // A persistent control-stream close is typed admission-loss
998                // input even when the physical Iroh generation remains live.
999                // Wake the existing desired-peer actor so it re-presents the
1000                // cached credential; the resulting proof callback re-enters
1001                // the single carrier selector. Do not repair this with a
1002                // control-stream-local retry or an app-owned reconnect.
1003                let remote_node_id = endpoint_id.to_string();
1004                let _ = self
1005                    .wake_native_external_auto_connect_for_replaced_route(remote_node_id.as_str())
1006                    .await;
1007            }
1008            true
1009        } else {
1010            eprintln!(
1011                "[SIGNAL-STREAM] stale-eviction-ignored connection_id={} transport_stable_id={} stream_rank={} reason={}",
1012                connection_id,
1013                expected_owner.transport_stable_id,
1014                expected_owner.stream_rank,
1015                reason,
1016            );
1017            false
1018        }
1019    }
1020
1021    /// Retire a native-main owner and directional proofs that belong to an
1022    /// older physical Iroh leg. The logical session grant remains accepted;
1023    /// the existing idempotent token presentation owns route resumption.
1024    #[cfg(not(target_arch = "wasm32"))]
1025    pub(crate) async fn retire_stale_native_main_route(
1026        &self,
1027        connection_id: &str,
1028        current_transport_stable_id: u64,
1029    ) -> bool {
1030        // The logical grant survives replacement, but both directional route
1031        // proofs belong to the retired physical generation. Keeping either
1032        // proof current would suppress reciprocal token presentation and can
1033        // leave both peers waiting forever for a fresh inbound proof.
1034        let mut retired = false;
1035        let stale_owner = self
1036            .native_control_streams
1037            .lock()
1038            .await
1039            .get(connection_id)
1040            .map(|entry| entry.owner)
1041            .filter(|owner| owner.transport_stable_id != current_transport_stable_id);
1042
1043        if let Some(owner) = stale_owner {
1044            retired |= self
1045                .evict_native_signal_stream_if_owner(
1046                    connection_id,
1047                    owner,
1048                    "transport-generation-replaced",
1049                )
1050                .await;
1051        }
1052        retired |= self.invalidate_native_main_route_proofs_except_transport(
1053            connection_id,
1054            current_transport_stable_id,
1055        );
1056        if retired {
1057            // Application keys may remain valid across an in-process network
1058            // migration, but confirmation belongs to the physical route that
1059            // proved possession. A relaunched peer can reuse the deterministic
1060            // connection id with a new ephemeral agreement. Until the existing
1061            // handshake owner confirms the replacement, keep SDK control open
1062            // and product traffic closed instead of decrypting with stale key
1063            // material.
1064            self.clear_connection_application_crypto_confirmation(connection_id);
1065            eprintln!(
1066                "[OpenRTC][KEY-AGREEMENT] replacement requires fresh confirmation connection_id={} transport_stable_id={}",
1067                connection_id, current_transport_stable_id,
1068            );
1069        }
1070        retired
1071    }
1072
1073    pub async fn update_transport_config(
1074        &self,
1075        transport_config: TransportConfig,
1076    ) -> anyhow::Result<()> {
1077        if let Some(moq) = transport_config.moq.as_ref() {
1078            anyhow::ensure!(
1079                !moq.relay_url.trim().is_empty(),
1080                "transports.moq.relayUrl is required because OpenRTC has no anonymous MoQ relay default"
1081            );
1082        }
1083        #[cfg(not(target_arch = "wasm32"))]
1084        let ble_available = self
1085            .native_custom_transport_kinds
1086            .read()
1087            .await
1088            .values()
1089            .any(|kind| matches!(kind, IrohPathKind::Ble));
1090        #[cfg(target_arch = "wasm32")]
1091        let ble_available = false;
1092        #[cfg(not(target_arch = "wasm32"))]
1093        let was_ble_enabled = ble_available
1094            && self
1095                .transport_config
1096                .read()
1097                .await
1098                .ble
1099                .as_ref()
1100                .is_some_and(|config| config.enabled);
1101        let transport_config =
1102            transport_config.sanitize_for_runtime_with_ble_available(ble_available);
1103        #[cfg(not(target_arch = "wasm32"))]
1104        let will_enable_ble = transport_config
1105            .ble
1106            .as_ref()
1107            .is_some_and(|config| config.enabled);
1108
1109        #[cfg(not(target_arch = "wasm32"))]
1110        if self.iroh_endpoint.read().await.is_some() {
1111            let current = self.transport_config.read().await;
1112            anyhow::ensure!(
1113                !current.endpoint_rebind_required(&transport_config),
1114                "irohRelayOnly, irohRelayTransportPolicy, and irohLan must be configured before Iroh endpoint initialization"
1115            );
1116        }
1117
1118        #[cfg(feature = "iroh-carrier-core")]
1119        let carrier_gate_guard = self.native_transport_upgrade_gates.lock().await;
1120        let mut guard = self.transport_config.write().await;
1121        #[cfg(feature = "iroh-carrier-core")]
1122        if *guard != transport_config {
1123            self.bump_iroh_carrier_policy_epoch();
1124        }
1125        *guard = transport_config;
1126        drop(guard);
1127        #[cfg(feature = "iroh-carrier-core")]
1128        drop(carrier_gate_guard);
1129        #[cfg(not(target_arch = "wasm32"))]
1130        if !was_ble_enabled && will_enable_ble {
1131            self.wake_native_ble_recovery("capability-enabled").await;
1132        }
1133        #[cfg(all(
1134            not(target_arch = "wasm32"),
1135            any(feature = "transport-webrtc", feature = "transport-moq")
1136        ))]
1137        self.reconcile_native_iroh_carrier_policy().await?;
1138        Ok(())
1139    }
1140
1141    /// Reconcile global carrier policy through the existing native peer owner.
1142    /// The caller supplies no peer-specific route decision: Rust ranks mutual
1143    /// capabilities and prepares a changed custom-carrier winner over the live
1144    /// incumbent. Atomic candidate commit retires the old generation. A close
1145    /// without replacement is reserved for the case where no custom carrier
1146    /// remains eligible.
1147    #[cfg(all(
1148        not(target_arch = "wasm32"),
1149        any(feature = "transport-webrtc", feature = "transport-moq")
1150    ))]
1151    async fn reconcile_native_iroh_carrier_policy(&self) -> anyhow::Result<usize> {
1152        let records = self.connection_manager.list_active().await;
1153        let capabilities = self.native_peer_transport_capabilities.read().await.clone();
1154        let config = self.transport_config.read().await.clone();
1155        #[cfg(feature = "transport-webrtc")]
1156        let local_webrtc = self.is_webrtc_carrier_enabled().await;
1157        #[cfg(not(feature = "transport-webrtc"))]
1158        let local_webrtc = false;
1159        #[cfg(feature = "transport-moq")]
1160        let local_moq = self.is_moq_carrier_enabled().await;
1161        #[cfg(not(feature = "transport-moq"))]
1162        let local_moq = false;
1163        let mut reconciled = 0usize;
1164
1165        for record in records {
1166            let current = match record.active_transport.as_str() {
1167                crate::transport_label::WEBRTC => IrohPathKind::WebRtc,
1168                crate::transport_label::MOQ => IrohPathKind::Moq,
1169                _ => {
1170                    // Policy/capability enablement must also revisit an admitted
1171                    // baseline relay, not only already-selected custom carriers.
1172                    if let Some(remote_node_id) =
1173                        record.endpoint_id.as_deref().or(record.node_id.as_deref())
1174                    {
1175                        if self.iroh_path_kind(remote_node_id).await == IrohPathKind::Relay {
1176                            self.revisit_iroh_route_after_admission(&record.connection_id)
1177                                .await;
1178                        }
1179                    }
1180                    continue;
1181                }
1182            };
1183            // Missing capability state is unknown, not an explicit withdrawal.
1184            // Keep a healthy proven carrier until a canonical handshake says
1185            // otherwise; key-only handshakes and replacement-loop timing must
1186            // never manufacture a policy close.
1187            let Some(remote) = capabilities.get(&record.connection_id).cloned() else {
1188                continue;
1189            };
1190            let ranked = config.ranked_iroh_carriers(
1191                local_webrtc && remote.contains(&NativePeerTransportCapability::WebRtc),
1192                local_moq && remote.contains(&NativePeerTransportCapability::Moq),
1193            );
1194            let desired = ranked.first().copied();
1195            if desired.is_some_and(|route| route.as_str() == current.transport_label()) {
1196                continue;
1197            }
1198            let Some(remote_node_id) = record.endpoint_id.as_deref().or(record.node_id.as_deref())
1199            else {
1200                continue;
1201            };
1202            match desired {
1203                #[cfg(feature = "transport-webrtc")]
1204                Some(crate::route_policy::KnownRoute::WebRtc) => {
1205                    self.maybe_start_native_webrtc_iroh_carrier(
1206                        &record.connection_id,
1207                        Some(remote_node_id),
1208                    )
1209                    .await?;
1210                    reconciled = reconciled.saturating_add(1);
1211                }
1212                #[cfg(feature = "transport-moq")]
1213                Some(crate::route_policy::KnownRoute::Moq) => {
1214                    self.maybe_start_native_moq_iroh_carrier(
1215                        &record.connection_id,
1216                        Some(remote_node_id),
1217                    )
1218                    .await?;
1219                    reconciled = reconciled.saturating_add(1);
1220                }
1221                _ => {
1222                    let Ok(remote_endpoint_id) = remote_node_id.parse::<iroh::EndpointId>() else {
1223                        continue;
1224                    };
1225                    if self
1226                        .close_current_iroh_carrier_generation_with_reason(
1227                            &record.connection_id,
1228                            remote_endpoint_id,
1229                            current,
1230                            record.transport_generation,
1231                            crate::lifecycle_reason::REASON_IROH_CARRIER_POLICY_CHANGED,
1232                        )
1233                        .await
1234                    {
1235                        reconciled = reconciled.saturating_add(1);
1236                    }
1237                }
1238            }
1239        }
1240        Ok(reconciled)
1241    }
1242
1243    pub async fn transport_config(&self) -> TransportConfig {
1244        self.transport_config.read().await.clone()
1245    }
1246
1247    pub async fn is_webrtc_transport_enabled(&self) -> bool {
1248        self.transport_config.read().await.webrtc.is_some()
1249    }
1250
1251    pub async fn is_webrtc_external_transport_enabled(&self) -> bool {
1252        false
1253    }
1254
1255    #[cfg(not(target_arch = "wasm32"))]
1256    pub async fn is_webrtc_carrier_enabled(&self) -> bool {
1257        self.transport_config.read().await.webrtc.as_ref().is_some()
1258            && self
1259                .native_transport_upgrade_available(IrohPathKind::WebRtc)
1260                .await
1261    }
1262
1263    pub async fn is_moq_transport_enabled(&self) -> bool {
1264        self.transport_config.read().await.moq.is_some()
1265    }
1266
1267    pub async fn is_moq_external_transport_enabled(&self) -> bool {
1268        false
1269    }
1270
1271    /// Capability used by the shared TypeScript/native-main handshake.
1272    ///
1273    /// Native advertises only a live compiled mechanism. Browser JavaScript
1274    /// prepares its configured carrier actor before auto-connect, so WASM can
1275    /// advertise stable configured intent without depending on which peer wins
1276    /// deterministic initiation.
1277    async fn local_typescript_webrtc_capability(&self) -> bool {
1278        #[cfg(all(feature = "transport-webrtc", target_arch = "wasm32"))]
1279        let enabled = self.is_webrtc_transport_enabled().await;
1280        #[cfg(all(feature = "transport-webrtc", not(target_arch = "wasm32")))]
1281        let enabled = self.is_webrtc_carrier_enabled().await;
1282        #[cfg(not(feature = "transport-webrtc"))]
1283        let enabled = false;
1284        enabled
1285    }
1286
1287    async fn local_typescript_moq_capability(&self) -> bool {
1288        #[cfg(all(feature = "transport-moq", target_arch = "wasm32"))]
1289        let enabled = self.is_moq_transport_enabled().await;
1290        #[cfg(all(feature = "transport-moq", not(target_arch = "wasm32")))]
1291        let enabled = self.is_moq_carrier_enabled().await;
1292        #[cfg(not(feature = "transport-moq"))]
1293        let enabled = false;
1294        enabled
1295    }
1296
1297    #[cfg(not(target_arch = "wasm32"))]
1298    pub async fn is_moq_carrier_enabled(&self) -> bool {
1299        self.transport_config.read().await.moq.as_ref().is_some()
1300            && self
1301                .native_transport_upgrade_available(IrohPathKind::Moq)
1302                .await
1303    }
1304
1305    #[cfg(not(target_arch = "wasm32"))]
1306    pub async fn is_ble_transport_enabled(&self) -> bool {
1307        self.transport_config
1308            .read()
1309            .await
1310            .ble
1311            .as_ref()
1312            .is_some_and(|config| config.enabled)
1313            && self
1314                .native_transport_upgrade_available(IrohPathKind::Ble)
1315                .await
1316    }
1317
1318    #[cfg(target_arch = "wasm32")]
1319    pub async fn is_ble_transport_enabled(&self) -> bool {
1320        false
1321    }
1322
1323    pub(crate) async fn maybe_handle_typescript_handshake_capabilities(
1324        &self,
1325        connection_id: &str,
1326        remote_node_id: Option<&str>,
1327        transport_stable_id: Option<u64>,
1328        handshake: &crate::native_protocol::TypeScriptHandshake,
1329    ) {
1330        let uses_canonical_carrier_schema = handshake
1331            .capabilities
1332            .as_ref()
1333            .is_some_and(|caps| caps.carrier_schema == Some(2));
1334        let remote_supports_webrtc = uses_canonical_carrier_schema
1335            && handshake
1336                .capabilities
1337                .as_ref()
1338                .and_then(|caps| caps.webrtc)
1339                .unwrap_or(false);
1340        let remote_supports_moq = uses_canonical_carrier_schema
1341            && handshake
1342                .capabilities
1343                .as_ref()
1344                .and_then(|caps| caps.moq)
1345                .unwrap_or(false);
1346        let remote_supports_ble = handshake
1347            .capabilities
1348            .as_ref()
1349            .and_then(|caps| caps.ble)
1350            .unwrap_or(false);
1351        #[cfg(all(not(target_arch = "wasm32"), feature = "iroh-carrier-core"))]
1352        let mut carrier_capabilities_changed = false;
1353        #[cfg(all(not(target_arch = "wasm32"), feature = "iroh-carrier-core"))]
1354        let mut carrier_capabilities_were_known = false;
1355        #[cfg(all(not(target_arch = "wasm32"), feature = "iroh-carrier-core"))]
1356        let carrier_gate_guard = if uses_canonical_carrier_schema {
1357            // Capability facts and their per-peer invalidation revision are a
1358            // single lifecycle mutation. Proof/commit checks use this same
1359            // gate lock, so a withdrawal cannot become visible while a stale
1360            // attempt still passes its old revision.
1361            Some(self.native_transport_upgrade_gates.lock().await)
1362        } else {
1363            None
1364        };
1365        #[cfg(not(target_arch = "wasm32"))]
1366        {
1367            // Key-only transcript frames intentionally omit carrierSchema.
1368            // They must not erase transport capabilities learned from the
1369            // canonical carrier advertisement on the same connection.
1370            if uses_canonical_carrier_schema {
1371                let mut capabilities = self.native_peer_transport_capabilities.write().await;
1372                #[cfg(feature = "iroh-carrier-core")]
1373                {
1374                    carrier_capabilities_were_known = capabilities.contains_key(connection_id);
1375                }
1376                let peer_capabilities = capabilities.entry(connection_id.to_string()).or_default();
1377                #[cfg(feature = "iroh-carrier-core")]
1378                let previous = peer_capabilities.clone();
1379                if remote_supports_webrtc {
1380                    peer_capabilities.insert(NativePeerTransportCapability::WebRtc);
1381                } else {
1382                    peer_capabilities.remove(&NativePeerTransportCapability::WebRtc);
1383                }
1384                if remote_supports_moq {
1385                    peer_capabilities.insert(NativePeerTransportCapability::Moq);
1386                } else {
1387                    peer_capabilities.remove(&NativePeerTransportCapability::Moq);
1388                }
1389                if remote_supports_ble {
1390                    peer_capabilities.insert(NativePeerTransportCapability::Ble);
1391                } else {
1392                    peer_capabilities.remove(&NativePeerTransportCapability::Ble);
1393                }
1394                #[cfg(feature = "iroh-carrier-core")]
1395                {
1396                    carrier_capabilities_changed = *peer_capabilities != previous;
1397                }
1398                // Retain an explicit empty set. Absence means capability state
1399                // is not known for this logical connection; an empty set means
1400                // a canonical carrier advertisement explicitly withdrew all
1401                // custom carriers. Logical retirement removes the entry.
1402            }
1403        }
1404        #[cfg(not(target_arch = "wasm32"))]
1405        let (remote_supports_webrtc, remote_supports_moq, remote_supports_ble) = {
1406            // Response/ACK key transcript frames intentionally omit the
1407            // carrier schema. Retaining the canonical facts only in the map
1408            // is insufficient: application-key confirmation is itself the
1409            // event that re-enters the selector, so that call must consume the
1410            // retained facts instead of manufacturing false withdrawals.
1411            let known = self
1412                .native_peer_transport_capabilities
1413                .read()
1414                .await
1415                .get(connection_id)
1416                .cloned()
1417                .unwrap_or_default();
1418            (
1419                effective_typescript_transport_capability(
1420                    uses_canonical_carrier_schema,
1421                    remote_supports_webrtc,
1422                    known.contains(&NativePeerTransportCapability::WebRtc),
1423                ),
1424                effective_typescript_transport_capability(
1425                    uses_canonical_carrier_schema,
1426                    remote_supports_moq,
1427                    known.contains(&NativePeerTransportCapability::Moq),
1428                ),
1429                effective_typescript_transport_capability(
1430                    uses_canonical_carrier_schema,
1431                    remote_supports_ble,
1432                    known.contains(&NativePeerTransportCapability::Ble),
1433                ),
1434            )
1435        };
1436        #[cfg(all(not(target_arch = "wasm32"), feature = "iroh-carrier-core"))]
1437        let matching_first_attempt = carrier_capabilities_changed
1438            && !carrier_capabilities_were_known
1439            && carrier_gate_guard.as_ref().is_some_and(|gates| {
1440                let global_policy_epoch = self.current_iroh_carrier_policy_epoch();
1441                let peer_policy_epoch = self.current_iroh_carrier_peer_policy_epoch(connection_id);
1442                gates.iter().any(|((candidate_connection_id, kind), gate)| {
1443                    candidate_connection_id == connection_id
1444                        && gate.policy_epoch == global_policy_epoch
1445                        && gate.peer_policy_epoch == peer_policy_epoch
1446                        && match kind {
1447                            IrohPathKind::WebRtc => remote_supports_webrtc,
1448                            IrohPathKind::Moq => remote_supports_moq,
1449                            IrohPathKind::Ble => remote_supports_ble,
1450                            _ => false,
1451                        }
1452                })
1453            });
1454        #[cfg(all(not(target_arch = "wasm32"), feature = "iroh-carrier-core"))]
1455        if crate::client::should_bump_remote_carrier_peer_policy_epoch(
1456            carrier_capabilities_were_known,
1457            carrier_capabilities_changed,
1458            matching_first_attempt,
1459        ) {
1460            self.bump_iroh_carrier_peer_policy_epoch(connection_id);
1461        }
1462        #[cfg(all(not(target_arch = "wasm32"), feature = "iroh-carrier-core"))]
1463        drop(carrier_gate_guard);
1464        #[cfg(all(
1465            not(target_arch = "wasm32"),
1466            any(feature = "transport-webrtc", feature = "transport-moq")
1467        ))]
1468        if carrier_capabilities_changed {
1469            // Remote capabilities are scoped to this logical connection. The
1470            // policy epoch is global local configuration authority; advancing
1471            // it here would invalidate in-flight proofs for unrelated peers.
1472            // A first canonical advertisement can race behind its already-
1473            // authenticated inbound carrier request; preserve that matching
1474            // attempt, but invalidate every later change for this peer.
1475            if let Err(error) = self.reconcile_native_iroh_carrier_policy().await {
1476                eprintln!(
1477                    "[OpenRTC][Iroh carrier] capability reconciliation failed connection_id={} error={}",
1478                    connection_id, error,
1479                );
1480            }
1481        }
1482        let application_crypto_was_confirmed =
1483            self.connection_application_crypto_is_confirmed(connection_id, transport_stable_id);
1484        let local_application_key_agreement_public_key = self
1485            .maybe_negotiate_typescript_application_crypto(
1486                connection_id,
1487                remote_node_id,
1488                transport_stable_id,
1489                handshake,
1490            )
1491            .await;
1492
1493        let local_webrtc_enabled = self.local_typescript_webrtc_capability().await;
1494        let local_moq_enabled = self.local_typescript_moq_capability().await;
1495        let local_ble_enabled = self.is_ble_transport_enabled().await;
1496        let received_application_key = handshake.application_key_agreement_public_key.is_some();
1497
1498        if let Some(reply_action) = typescript_application_key_reply_action(
1499            handshake.action.as_deref(),
1500            received_application_key,
1501            local_application_key_agreement_public_key.is_some(),
1502            application_crypto_was_confirmed,
1503        ) {
1504            let public_key = local_application_key_agreement_public_key
1505                .or_else(|| self.connection_key_agreement_public_key(connection_id));
1506            if let Err(error) = self
1507                .send_typescript_handshake_update(
1508                    connection_id,
1509                    "application-key-confirmation",
1510                    public_key,
1511                    reply_action,
1512                )
1513                .await
1514            {
1515                eprintln!(
1516                    "[OpenRTC][capability] key acknowledgement send failed connection_id={} remote_node_id={:?} error={}",
1517                    connection_id, remote_node_id, error,
1518                );
1519            }
1520        }
1521        eprintln!(
1522            "[OpenRTC][transport] TS handshake capabilities connection_id={} remote_node_id={:?} remote_webrtc={} local_webrtc={} remote_moq={} local_moq={} remote_ble={} local_ble={}",
1523            connection_id,
1524            remote_node_id,
1525            remote_supports_webrtc,
1526            local_webrtc_enabled,
1527            remote_supports_moq,
1528            local_moq_enabled,
1529            remote_supports_ble,
1530            local_ble_enabled,
1531        );
1532        let should_send_capability_update = should_send_typescript_capability_update(
1533            local_webrtc_enabled,
1534            local_moq_enabled,
1535            local_ble_enabled,
1536            local_application_key_agreement_public_key.is_some() && !received_application_key,
1537            handshake.action.as_deref(),
1538        );
1539
1540        if should_send_capability_update {
1541            // Capability publication describes this endpoint, so it must not wait
1542            // for the peer to advertise the same transport first. Remote support
1543            // gates upgrade initiation below. Application key agreement shares the
1544            // same handshake because native explicit streams use its ORTCE1 key.
1545            if let Err(error) = self
1546                .send_typescript_capability_update(
1547                    connection_id,
1548                    if local_webrtc_enabled || local_moq_enabled || local_ble_enabled {
1549                        "local-capability"
1550                    } else {
1551                        "application-key-agreement"
1552                    },
1553                    local_application_key_agreement_public_key,
1554                )
1555                .await
1556            {
1557                eprintln!(
1558                    "[OpenRTC][capability] update send failed connection_id={} remote_node_id={:?} error={}",
1559                    connection_id,
1560                    remote_node_id,
1561                    error,
1562                );
1563            }
1564        } else if handshake.action.as_deref() == Some("capability-update") {
1565            eprintln!(
1566                "[OpenRTC][capability] update received without echo connection_id={} remote_node_id={:?}",
1567                connection_id, remote_node_id,
1568            );
1569        }
1570
1571        #[cfg(not(target_arch = "wasm32"))]
1572        if let Err(error) = self
1573            .maybe_start_preferred_native_iroh_carrier(
1574                connection_id,
1575                remote_node_id,
1576                remote_supports_webrtc,
1577                remote_supports_moq,
1578            )
1579            .await
1580        {
1581            eprintln!(
1582                "[OpenRTC][Iroh carrier] preferred start failed connection_id={} remote_node_id={:?} error={}",
1583                connection_id, remote_node_id, error,
1584            );
1585        }
1586
1587        #[cfg(not(target_arch = "wasm32"))]
1588        if remote_supports_ble && local_ble_enabled {
1589            let _ = self
1590                .maybe_start_native_ble_upgrade(connection_id, remote_node_id)
1591                .await;
1592        }
1593
1594        #[cfg(not(target_arch = "wasm32"))]
1595        if self.connection_application_crypto_is_confirmed(connection_id, transport_stable_id) {
1596            let _ = self
1597                .confirm_managed_connection_readiness(connection_id)
1598                .await;
1599        }
1600    }
1601
1602    /// Select one carrier after privacy/capability filtering. Iroh direct/LAN
1603    /// path gating remains inside each mechanism attempt and no second carrier
1604    /// is started from this decision.
1605    #[cfg(all(
1606        not(target_arch = "wasm32"),
1607        any(feature = "transport-webrtc", feature = "transport-moq")
1608    ))]
1609    pub(super) async fn maybe_start_preferred_native_iroh_carrier(
1610        &self,
1611        connection_id: &str,
1612        remote_node_id: Option<&str>,
1613        remote_supports_webrtc: bool,
1614        remote_supports_moq: bool,
1615    ) -> anyhow::Result<Option<crate::route_policy::KnownRoute>> {
1616        #[cfg(feature = "transport-webrtc")]
1617        if remote_supports_webrtc {
1618            let attempt = self
1619                .native_webrtc_carrier_attempts
1620                .read()
1621                .await
1622                .get(connection_id)
1623                .cloned();
1624            if let Some(attempt) = attempt {
1625                let refreshing = attempt.ice_refresh.try_lock().is_err()
1626                    || attempt
1627                        .ice_refresh_baseline
1628                        .try_lock()
1629                        .map(|loan| loan.is_some())
1630                        .unwrap_or(true);
1631                if refreshing
1632                    && self
1633                        .native_ice_refresh_is_current(connection_id, &attempt)
1634                        .await
1635                {
1636                    return Ok(None);
1637                }
1638            }
1639        }
1640        let Some(base_generation) = self
1641            .current_native_peer_data_generation(connection_id, None)
1642            .await
1643        else {
1644            self.record_native_iroh_carrier_debug_event(
1645                connection_id,
1646                "",
1647                "selector-deferred",
1648                Some("base-generation-unavailable"),
1649            );
1650            return Ok(None);
1651        };
1652        if let Some((_rejected, reason)) = self.session_admission_block_reason_for_transport(
1653            connection_id,
1654            Some(base_generation.transport_stable_id),
1655        ) {
1656            // Path observation can precede reciprocal token presentation on a
1657            // replacement Iroh leg. Starting a carrier against that provisional
1658            // generation lets the admission commit supersede the attempt while
1659            // the peer still owns its old upgrade fence. The admission/key
1660            // callbacks already revisit this selector, so wait for that logical
1661            // event instead of adding a timer or another recovery owner.
1662            eprintln!(
1663                "[OpenRTC][Iroh carrier] preferred start deferred connection_id={} transport_stable_id={} reason={}",
1664                connection_id, base_generation.transport_stable_id, reason,
1665            );
1666            self.record_native_iroh_carrier_debug_event(
1667                connection_id,
1668                "",
1669                "selector-deferred",
1670                Some(&reason),
1671            );
1672            return Ok(None);
1673        }
1674        #[cfg(feature = "transport-webrtc")]
1675        let local_webrtc = self.is_webrtc_carrier_enabled().await;
1676        #[cfg(not(feature = "transport-webrtc"))]
1677        let local_webrtc = false;
1678        #[cfg(feature = "transport-moq")]
1679        let local_moq = self.is_moq_carrier_enabled().await;
1680        #[cfg(not(feature = "transport-moq"))]
1681        let local_moq = false;
1682        let config = self.transport_config.read().await.clone();
1683        let ranked = config.ranked_iroh_carriers(
1684            remote_supports_webrtc && local_webrtc,
1685            remote_supports_moq && local_moq,
1686        );
1687        let Some(selected) = ranked.first().copied() else {
1688            let reason = format!(
1689                "no-eligible-carrier:remote-webrtc={remote_supports_webrtc}:local-webrtc={local_webrtc}:remote-moq={remote_supports_moq}:local-moq={local_moq}:privacy-mode={}:webrtc-relay-only={}",
1690                config.privacy_mode,
1691                config.webrtc.as_ref().is_some_and(|webrtc| webrtc.privacy_mode),
1692            );
1693            self.record_native_iroh_carrier_debug_event(
1694                connection_id,
1695                "",
1696                "selector-deferred",
1697                Some(&reason),
1698            );
1699            return Ok(None);
1700        };
1701        self.record_native_iroh_carrier_debug_event(
1702            connection_id,
1703            "",
1704            "selector-selected",
1705            Some(selected.as_str()),
1706        );
1707        match selected {
1708            #[cfg(feature = "transport-webrtc")]
1709            crate::route_policy::KnownRoute::WebRtc => {
1710                self.maybe_start_native_webrtc_iroh_carrier(connection_id, remote_node_id)
1711                    .await?;
1712            }
1713            #[cfg(feature = "transport-moq")]
1714            crate::route_policy::KnownRoute::Moq => {
1715                self.maybe_start_native_moq_iroh_carrier(connection_id, remote_node_id)
1716                    .await?;
1717            }
1718            _ => return Ok(None),
1719        }
1720        Ok(Some(selected))
1721    }
1722
1723    /// Re-enter the single carrier selector after an admission proof or local
1724    /// admission transition. These are logical lifecycle events, so recovery
1725    /// remains event-driven and cannot create gateway polling traffic.
1726    #[cfg(all(
1727        not(target_arch = "wasm32"),
1728        any(feature = "transport-webrtc", feature = "transport-moq")
1729    ))]
1730    pub(super) async fn revisit_iroh_route_after_admission(&self, connection_id: &str) {
1731        let Some(remote_node_id) = self
1732            .connection_manager
1733            .get_by_connection_id(connection_id)
1734            .await
1735            .and_then(|record| record.endpoint_id.or(record.node_id))
1736        else {
1737            return;
1738        };
1739        let remote_capabilities = self
1740            .native_peer_transport_capabilities
1741            .read()
1742            .await
1743            .get(connection_id)
1744            .cloned()
1745            .unwrap_or_default();
1746        let local_is_responder = self
1747            .current_node_id()
1748            .await
1749            .is_some_and(|local| local.as_str() <= remote_node_id.as_str());
1750        let selected = match self
1751            .maybe_start_preferred_native_iroh_carrier(
1752                connection_id,
1753                Some(&remote_node_id),
1754                remote_capabilities.contains(&NativePeerTransportCapability::WebRtc),
1755                remote_capabilities.contains(&NativePeerTransportCapability::Moq),
1756            )
1757            .await
1758        {
1759            Ok(selected) => selected,
1760            Err(error) => {
1761                eprintln!(
1762                    "[OpenRTC][Iroh carrier] admission-event start failed connection_id={} remote_node_id={} error={}",
1763                    connection_id, remote_node_id, error,
1764                );
1765                None
1766            }
1767        };
1768        if selected.is_some() && local_is_responder {
1769            // The deterministic initiator can be a browser. Publish the newly
1770            // actionable control capability once so its existing handshake
1771            // observer re-enters the Rust-owned selector without a timer.
1772            if let Err(error) = self
1773                .send_typescript_capability_update(connection_id, "native-admission-ready", None)
1774                .await
1775            {
1776                eprintln!(
1777                    "[OpenRTC][Iroh carrier] admission-ready advertisement failed connection_id={} remote_node_id={} error={}",
1778                    connection_id, remote_node_id, error,
1779                );
1780            }
1781        }
1782    }
1783
1784    #[cfg(all(
1785        not(target_arch = "wasm32"),
1786        not(any(feature = "transport-webrtc", feature = "transport-moq"))
1787    ))]
1788    pub(super) async fn maybe_start_preferred_native_iroh_carrier(
1789        &self,
1790        _connection_id: &str,
1791        _remote_node_id: Option<&str>,
1792        _remote_supports_webrtc: bool,
1793        _remote_supports_moq: bool,
1794    ) -> anyhow::Result<Option<crate::route_policy::KnownRoute>> {
1795        Ok(None)
1796    }
1797
1798    #[cfg(all(
1799        not(target_arch = "wasm32"),
1800        any(feature = "transport-webrtc", feature = "transport-moq")
1801    ))]
1802    fn maybe_start_next_native_iroh_carrier<'a>(
1803        &'a self,
1804        connection_id: &'a str,
1805        remote_node_id: &'a str,
1806        failed: crate::route_policy::KnownRoute,
1807    ) -> std::pin::Pin<Box<dyn std::future::Future<Output = bool> + Send + 'a>> {
1808        Box::pin(async move {
1809            let should_initiate = self
1810                .current_node_id()
1811                .await
1812                .is_some_and(|local| local.as_str() > remote_node_id);
1813            if !should_initiate {
1814                return false;
1815            }
1816            let remote_capabilities = self
1817                .native_peer_transport_capabilities
1818                .read()
1819                .await
1820                .get(connection_id)
1821                .cloned()
1822                .unwrap_or_default();
1823            #[cfg(feature = "transport-webrtc")]
1824            let local_webrtc = self.is_webrtc_carrier_enabled().await;
1825            #[cfg(not(feature = "transport-webrtc"))]
1826            let local_webrtc = false;
1827            #[cfg(feature = "transport-moq")]
1828            let local_moq = self.is_moq_carrier_enabled().await;
1829            #[cfg(not(feature = "transport-moq"))]
1830            let local_moq = false;
1831            let config = self.transport_config.read().await.clone();
1832            let ranked = config.ranked_iroh_carriers(
1833                local_webrtc
1834                    && remote_capabilities.contains(&NativePeerTransportCapability::WebRtc),
1835                local_moq && remote_capabilities.contains(&NativePeerTransportCapability::Moq),
1836            );
1837            let Some(next) = ranked
1838                .iter()
1839                .position(|route| *route == failed)
1840                .and_then(|index| ranked.get(index + 1))
1841                .copied()
1842            else {
1843                return false;
1844            };
1845            let result = match next {
1846                #[cfg(feature = "transport-webrtc")]
1847                crate::route_policy::KnownRoute::WebRtc => {
1848                    self.maybe_start_native_webrtc_iroh_carrier(connection_id, Some(remote_node_id))
1849                        .await
1850                }
1851                #[cfg(feature = "transport-moq")]
1852                crate::route_policy::KnownRoute::Moq => {
1853                    self.maybe_start_native_moq_iroh_carrier(connection_id, Some(remote_node_id))
1854                        .await
1855                }
1856                _ => return false,
1857            };
1858            if let Err(error) = result {
1859                eprintln!(
1860                "[OpenRTC][Iroh carrier] fallback start failed connection_id={} route={} error={}",
1861                connection_id,
1862                next.as_str(),
1863                error,
1864            );
1865            }
1866            true
1867        })
1868    }
1869
1870    #[cfg(not(target_arch = "wasm32"))]
1871    async fn send_native_ble_upgrade_control(
1872        &self,
1873        connection_id: &str,
1874        action: &str,
1875        upgrade_id: &str,
1876        proof_generation: crate::client::NativePeerDataGeneration,
1877    ) -> anyhow::Result<()> {
1878        let frame = serde_json::json!({
1879            "type": "transport-upgrade",
1880            "transport": "ble",
1881            "action": action,
1882            "upgradeId": upgrade_id,
1883            // This tuple is process-local authority owned by the initiator.
1884            // The responder only echoes it and binds the encrypted candidate
1885            // proof to it; it never installs these numbers as local state.
1886            "base": proof_generation,
1887        });
1888        self.send_typescript_handshake_over_native_main(connection_id, &frame)
1889            .await
1890    }
1891
1892    #[cfg(all(
1893        not(target_arch = "wasm32"),
1894        any(feature = "transport-webrtc", feature = "transport-moq")
1895    ))]
1896    async fn send_iroh_carrier_control(
1897        &self,
1898        connection_id: &str,
1899        frame: &crate::iroh_carrier_bootstrap::CarrierBootstrapFrame,
1900    ) -> anyhow::Result<()> {
1901        self.send_typescript_handshake_over_native_main(
1902            connection_id,
1903            &serde_json::to_value(frame)?,
1904        )
1905        .await
1906    }
1907
1908    #[cfg(all(
1909        not(target_arch = "wasm32"),
1910        any(feature = "transport-webrtc", feature = "transport-moq")
1911    ))]
1912    async fn reject_native_iroh_carrier_request_if_base_blocked(
1913        &self,
1914        connection_id: &str,
1915        bootstrap: &crate::iroh_carrier_bootstrap::CarrierBootstrapFrame,
1916        transport_stable_id: u64,
1917        carrier_name: &str,
1918    ) -> bool {
1919        let Some((terminal, reason)) = self
1920            .session_admission_block_reason_for_transport(connection_id, Some(transport_stable_id))
1921        else {
1922            return false;
1923        };
1924        let failure_code = if terminal {
1925            "carrier-authorization-rejected"
1926        } else {
1927            "carrier-base-not-ready"
1928        };
1929        if let Ok(failed) = crate::iroh_carrier_bootstrap::CarrierBootstrapFrame::failed_from(
1930            bootstrap,
1931            failure_code,
1932        ) {
1933            let _ = self.send_iroh_carrier_control(connection_id, &failed).await;
1934        }
1935        eprintln!(
1936            "[OpenRTC][{carrier_name} carrier] responder rejected connection_id={connection_id} transport_stable_id={transport_stable_id} failure_code={failure_code} reason={reason}",
1937        );
1938        true
1939    }
1940
1941    #[cfg(all(not(target_arch = "wasm32"), feature = "transport-webrtc"))]
1942    async fn create_native_webrtc_carrier_attempt(
1943        &self,
1944        connection_id: &str,
1945        remote_node_id: &str,
1946        bootstrap: crate::iroh_carrier_bootstrap::CarrierBootstrapFrame,
1947        generation: crate::client::NativePeerDataGeneration,
1948        role: crate::transport::NativeWebRTCRole,
1949        start: bool,
1950    ) -> anyhow::Result<Arc<crate::client::NativeWebRtcCarrierAttempt>> {
1951        let config = self
1952            .transport_config
1953            .read()
1954            .await
1955            .webrtc
1956            .clone()
1957            .ok_or_else(|| anyhow::anyhow!("WebRTC carrier config is unavailable"))?;
1958        let local_node_id = self
1959            .current_node_id()
1960            .await
1961            .ok_or_else(|| anyhow::anyhow!("local Iroh node id is unavailable"))?;
1962        let remote_endpoint_id = remote_node_id.parse::<iroh::EndpointId>()?;
1963        // A fresh admitted base generation may negotiate the same carrier
1964        // before the old DataChannel reports terminal. Retire that exact
1965        // packet session first so the mechanism-neutral carrier can attach
1966        // the replacement instead of rejecting it as a duplicate.
1967        if let Some(previous) = self
1968            .native_webrtc_carrier_attempts
1969            .write()
1970            .await
1971            .remove(connection_id)
1972        {
1973            self.record_native_iroh_carrier_debug_event(
1974                connection_id,
1975                &previous.upgrade_id,
1976                "close-requested",
1977                Some("superseded-before-carrier-activation"),
1978            );
1979            previous.channel.close();
1980            let _ = previous.pump.lock().await.take();
1981        }
1982        let packet_session = self
1983            .activate_native_iroh_packet_carrier(IrohPathKind::WebRtc, remote_endpoint_id)
1984            .await?;
1985        let expected = bootstrap.frame_expectation()?;
1986        let application_key = self
1987            .application_crypto_key_for_connection(Some(connection_id))
1988            .ok_or_else(|| {
1989                anyhow::anyhow!("WebRTC carrier requires the admitted application crypto key")
1990            })?;
1991        let client = self.clone();
1992        let signal_connection_id = connection_id.to_string();
1993        let signal_upgrade_id = bootstrap.upgrade_id.clone();
1994        let signal_sender: crate::transport::WebRtcSignalSender =
1995            Arc::new(move |frame: serde_json::Value| {
1996                let client = client.clone();
1997                let connection_id = signal_connection_id.clone();
1998                let upgrade_id = signal_upgrade_id.clone();
1999                Box::pin(async move {
2000                    let current = client
2001                        .native_webrtc_carrier_attempts
2002                        .read()
2003                        .await
2004                        .get(&connection_id)
2005                        .is_some_and(|attempt| attempt.upgrade_id == upgrade_id);
2006                    if current {
2007                        let _ = client
2008                            .send_typescript_handshake_over_native_main(&connection_id, &frame)
2009                            .await;
2010                    }
2011                })
2012            });
2013        let channel = Arc::new(
2014            crate::transport::WebRtcDataChannel::new_packet_carrier(
2015                &local_node_id,
2016                remote_node_id,
2017                &config,
2018                signal_sender,
2019                &bootstrap.upgrade_id,
2020                role,
2021            )
2022            .await?,
2023        );
2024        let pump = crate::native_webrtc_carrier::NativeWebRtcCarrierSession::attach(
2025            channel.clone(),
2026            packet_session,
2027            expected,
2028            application_key,
2029        )
2030        .await?;
2031        let attempt = Arc::new(crate::client::NativeWebRtcCarrierAttempt {
2032            upgrade_id: bootstrap.upgrade_id.clone(),
2033            bootstrap,
2034            generation,
2035            remote_endpoint_id,
2036            channel: channel.clone(),
2037            pump: tokio::sync::Mutex::new(Some(pump)),
2038            completion_started: std::sync::atomic::AtomicBool::new(false),
2039            ice_refresh: tokio::sync::Mutex::new(Default::default()),
2040            ice_refresh_baseline: tokio::sync::Mutex::new(None),
2041        });
2042        if let Some(previous) = self
2043            .native_webrtc_carrier_attempts
2044            .write()
2045            .await
2046            .insert(connection_id.to_string(), attempt.clone())
2047        {
2048            self.record_native_iroh_carrier_debug_event(
2049                connection_id,
2050                &previous.upgrade_id,
2051                "close-requested",
2052                Some("superseded-during-carrier-install"),
2053            );
2054            previous.channel.close();
2055        }
2056        self.record_native_iroh_carrier_debug_event(
2057            connection_id,
2058            &attempt.upgrade_id,
2059            "attempt-created",
2060            Some(match role {
2061                crate::transport::NativeWebRTCRole::Initiator => "initiator",
2062                crate::transport::NativeWebRTCRole::Responder => "responder",
2063                crate::transport::NativeWebRTCRole::Auto => "auto",
2064            }),
2065        );
2066        if start {
2067            if let Err(error) = channel.start().await {
2068                self.native_webrtc_carrier_attempts
2069                    .write()
2070                    .await
2071                    .remove(connection_id);
2072                return Err(error);
2073            }
2074        }
2075        Ok(attempt)
2076    }
2077
2078    #[cfg(all(not(target_arch = "wasm32"), feature = "transport-webrtc"))]
2079    async fn wait_native_webrtc_carrier_connected(
2080        &self,
2081        connection_id: &str,
2082        upgrade_id: &str,
2083    ) -> anyhow::Result<Arc<crate::client::NativeWebRtcCarrierAttempt>> {
2084        let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(25);
2085        loop {
2086            let attempt = self
2087                .native_webrtc_carrier_attempts
2088                .read()
2089                .await
2090                .get(connection_id)
2091                .filter(|attempt| attempt.upgrade_id == upgrade_id)
2092                .cloned()
2093                .ok_or_else(|| anyhow::anyhow!("WebRTC carrier attempt was retired"))?;
2094            match attempt.channel.state() {
2095                crate::transport::NativeWebRTCState::Connected => return Ok(attempt),
2096                crate::transport::NativeWebRTCState::Failed
2097                | crate::transport::NativeWebRTCState::Closed => {
2098                    anyhow::bail!("WebRTC carrier negotiation failed")
2099                }
2100                _ if tokio::time::Instant::now() >= deadline => {
2101                    anyhow::bail!("WebRTC carrier negotiation timed out")
2102                }
2103                _ => tokio::time::sleep(std::time::Duration::from_millis(25)).await,
2104            }
2105        }
2106    }
2107
2108    #[cfg(all(not(target_arch = "wasm32"), feature = "transport-webrtc"))]
2109    fn spawn_native_webrtc_carrier_terminal_watcher(
2110        &self,
2111        connection_id: String,
2112        remote_endpoint_id: iroh::EndpointId,
2113        attempt: Arc<crate::client::NativeWebRtcCarrierAttempt>,
2114    ) {
2115        let client = self.clone();
2116        tokio::spawn(async move {
2117            loop {
2118                let current = client
2119                    .native_webrtc_carrier_attempts
2120                    .read()
2121                    .await
2122                    .get(&connection_id)
2123                    .is_some_and(|value| Arc::ptr_eq(value, &attempt));
2124                if !current {
2125                    return;
2126                }
2127                {
2128                    let mut baseline = attempt.ice_refresh_baseline.lock().await;
2129                    if baseline.as_ref().is_some_and(|loan| {
2130                        (loan.started
2131                            && loan.since.elapsed().as_secs() >= 1
2132                            && !attempt.channel.ice_refresh_in_progress())
2133                            || loan.since.elapsed().as_secs() >= 30
2134                            || loan.policy_epoch != client.current_iroh_carrier_policy_epoch()
2135                            || loan.peer_policy_epoch
2136                                != client.current_iroh_carrier_peer_policy_epoch(&connection_id)
2137                    }) {
2138                        baseline.take();
2139                    }
2140                }
2141                let terminal_reason = attempt
2142                    .pump
2143                    .lock()
2144                    .await
2145                    .as_ref()
2146                    .and_then(|pump| pump.take_terminal_reason());
2147                let terminal_reason = terminal_reason.or_else(|| {
2148                    matches!(
2149                        attempt.channel.state(),
2150                        crate::transport::NativeWebRTCState::Failed
2151                            | crate::transport::NativeWebRTCState::Closed
2152                    )
2153                    .then_some("datachannel-terminal")
2154                });
2155                if let Some(reason) = terminal_reason {
2156                    client.record_native_iroh_carrier_debug_event(
2157                        &connection_id,
2158                        &attempt.upgrade_id,
2159                        "terminal-observed",
2160                        Some(reason),
2161                    );
2162                    // A packet-carrier terminal event belongs to this exact
2163                    // logical upgrade.  Retire its attempt and reservation
2164                    // before releasing the selected Iroh generation, so an
2165                    // admitted replacement cannot mistake the retired
2166                    // bootstrap for a live duplicate.
2167                    let retired = client
2168                        .retire_native_webrtc_carrier_attempt_if_current(
2169                            &connection_id,
2170                            &attempt,
2171                            reason,
2172                        )
2173                        .await;
2174                    if retired {
2175                        remove_native_transport_upgrade_gate_for_generation(
2176                            &client.native_transport_upgrade_gates,
2177                            &(connection_id.clone(), IrohPathKind::WebRtc),
2178                            &attempt.upgrade_id,
2179                            attempt.generation,
2180                        )
2181                        .await;
2182                    }
2183                    let _ = client
2184                        .close_current_iroh_carrier_generation_with_reason(
2185                            &connection_id,
2186                            remote_endpoint_id,
2187                            IrohPathKind::WebRtc,
2188                            attempt.generation.transport_generation.saturating_add(1),
2189                            reason,
2190                        )
2191                        .await;
2192                    return;
2193                }
2194                tokio::time::sleep(std::time::Duration::from_millis(250)).await;
2195            }
2196        });
2197    }
2198
2199    #[cfg(all(not(target_arch = "wasm32"), feature = "transport-webrtc"))]
2200    async fn retire_native_webrtc_carrier_attempt(
2201        &self,
2202        connection_id: &str,
2203        upgrade_id: &str,
2204        reason: &str,
2205    ) -> bool {
2206        let removed = {
2207            let mut attempts = self.native_webrtc_carrier_attempts.write().await;
2208            if attempts
2209                .get(connection_id)
2210                .is_some_and(|attempt| attempt.upgrade_id == upgrade_id)
2211            {
2212                attempts.remove(connection_id)
2213            } else {
2214                None
2215            }
2216        };
2217        if let Some(attempt) = removed {
2218            self.record_native_iroh_carrier_debug_event(
2219                connection_id,
2220                &attempt.upgrade_id,
2221                "close-requested",
2222                Some(reason),
2223            );
2224            attempt.channel.close();
2225            let _ = attempt.pump.lock().await.take();
2226            true
2227        } else {
2228            false
2229        }
2230    }
2231
2232    #[cfg(all(not(target_arch = "wasm32"), feature = "transport-webrtc"))]
2233    async fn retire_native_webrtc_carrier_attempt_if_current(
2234        &self,
2235        connection_id: &str,
2236        expected: &Arc<crate::client::NativeWebRtcCarrierAttempt>,
2237        reason: &str,
2238    ) -> bool {
2239        let removed = {
2240            let mut attempts = self.native_webrtc_carrier_attempts.write().await;
2241            if attempts
2242                .get(connection_id)
2243                .is_some_and(|attempt| Arc::ptr_eq(attempt, expected))
2244            {
2245                attempts.remove(connection_id)
2246            } else {
2247                None
2248            }
2249        };
2250        if let Some(attempt) = removed {
2251            self.record_native_iroh_carrier_debug_event(
2252                connection_id,
2253                &attempt.upgrade_id,
2254                "close-requested",
2255                Some(reason),
2256            );
2257            attempt.channel.close();
2258            let _ = attempt.pump.lock().await.take();
2259            true
2260        } else {
2261            false
2262        }
2263    }
2264
2265    #[cfg(all(not(target_arch = "wasm32"), feature = "transport-webrtc"))]
2266    async fn fail_native_webrtc_carrier_attempt(
2267        &self,
2268        connection_id: &str,
2269        remote_node_id: &str,
2270        request: &crate::iroh_carrier_bootstrap::CarrierBootstrapFrame,
2271        failure_code: &'static str,
2272        notify_peer: bool,
2273    ) -> Option<crate::client::NativeTransportUpgradeGate> {
2274        // Claim failure under the same gate lock used by atomic commit. Once
2275        // commit consumes this reservation, no late failure can close its carrier.
2276        let gate = {
2277            let key = (connection_id.to_string(), IrohPathKind::WebRtc);
2278            let mut gates = self.native_transport_upgrade_gates.lock().await;
2279            let gate = gates
2280                .get(&key)
2281                .filter(|gate| gate.upgrade_id == request.upgrade_id)
2282                .cloned()?;
2283            gates.remove(&key);
2284            gate
2285        };
2286        let retry_pending = webrtc_retry_attempt(failure_code, request.attempt).is_some();
2287        self.retire_native_webrtc_carrier_attempt(connection_id, &request.upgrade_id, failure_code)
2288            .await;
2289        if notify_peer {
2290            if let Ok(failed) = crate::iroh_carrier_bootstrap::CarrierBootstrapFrame::failed_from(
2291                request,
2292                failure_code,
2293            ) {
2294                let _ = self.send_iroh_carrier_control(connection_id, &failed).await;
2295            }
2296        }
2297        eprintln!(
2298            "[OpenRTC][WebRTC carrier] attempt failed connection_id={} upgrade_id={} failure_code={}",
2299            connection_id, request.upgrade_id, failure_code,
2300        );
2301        if !retry_pending {
2302            let _ = self
2303                .maybe_start_next_native_iroh_carrier(
2304                    connection_id,
2305                    remote_node_id,
2306                    crate::route_policy::KnownRoute::WebRtc,
2307                )
2308                .await;
2309        }
2310        Some(gate)
2311    }
2312
2313    #[cfg(all(not(target_arch = "wasm32"), feature = "transport-webrtc"))]
2314    async fn schedule_failed_webrtc_retry(
2315        &self,
2316        connection_id: &str,
2317        remote_node_id: &str,
2318        request: &crate::iroh_carrier_bootstrap::CarrierBootstrapFrame,
2319        failure_code: &str,
2320        failed_gate: crate::client::NativeTransportUpgradeGate,
2321    ) -> bool {
2322        let Some(attempt) = webrtc_retry_attempt(failure_code, request.attempt) else {
2323            return false;
2324        };
2325        // Retry once through the same Rust lifecycle owner. A new bootstrap
2326        // and generation fence prevent delayed failure callbacks from either
2327        // reviving the retired mechanism or starting a competing attempt.
2328        let client = self.clone();
2329        let connection_id = connection_id.to_string();
2330        let remote_node_id = remote_node_id.to_string();
2331        let policy_epoch = failed_gate.policy_epoch;
2332        let peer_policy_epoch = failed_gate.peer_policy_epoch;
2333        let delay = webrtc_retry_delay(&request.upgrade_id);
2334        // Capture the current base after retiring this failed attempt. A later
2335        // physical replacement/capability event owns its own promotion wake.
2336        let generation = client
2337            .current_native_peer_data_generation(&connection_id, None)
2338            .await;
2339        tokio::spawn(async move {
2340            tokio::time::sleep(delay).await;
2341            if generation.is_none()
2342                || client
2343                    .current_native_peer_data_generation(&connection_id, None)
2344                    .await
2345                    != generation
2346                || !client.iroh_carrier_policy_epoch_is_current(policy_epoch)
2347                || client.current_iroh_carrier_peer_policy_epoch(&connection_id)
2348                    != peer_policy_epoch
2349                || client
2350                    .native_webrtc_carrier_attempts
2351                    .read()
2352                    .await
2353                    .contains_key(&connection_id)
2354            {
2355                return;
2356            }
2357            if let Err(error) = Box::pin(client.maybe_start_native_webrtc_iroh_carrier_attempt(
2358                &connection_id,
2359                Some(&remote_node_id),
2360                attempt.saturating_sub(1),
2361            ))
2362            .await
2363            {
2364                eprintln!(
2365                    "[OpenRTC][WebRTC carrier] bounded retry failed connection_id={} attempt={} error={error:#}",
2366                    connection_id, attempt,
2367                );
2368            }
2369        });
2370        true
2371    }
2372
2373    #[cfg(all(not(target_arch = "wasm32"), feature = "transport-webrtc"))]
2374    pub(super) async fn maybe_start_native_webrtc_iroh_carrier(
2375        &self,
2376        connection_id: &str,
2377        remote_node_id: Option<&str>,
2378    ) -> anyhow::Result<()> {
2379        self.maybe_start_native_webrtc_iroh_carrier_attempt(connection_id, remote_node_id, 0)
2380            .await
2381    }
2382
2383    #[cfg(all(not(target_arch = "wasm32"), feature = "transport-webrtc"))]
2384    fn maybe_start_native_webrtc_iroh_carrier_attempt<'a>(
2385        &'a self,
2386        connection_id: &'a str,
2387        remote_node_id: Option<&'a str>,
2388        retry_count: u8,
2389    ) -> std::pin::Pin<Box<dyn std::future::Future<Output = anyhow::Result<()>> + Send + 'a>> {
2390        Box::pin(async move {
2391            if !self.is_webrtc_carrier_enabled().await {
2392                self.record_native_iroh_carrier_debug_event(
2393                    connection_id,
2394                    "",
2395                    "start-deferred",
2396                    Some("carrier-disabled"),
2397                );
2398                return Ok(());
2399            }
2400            let remote_node_id = remote_node_id
2401                .map(str::trim)
2402                .filter(|value| !value.is_empty())
2403                .ok_or_else(|| anyhow::anyhow!("WebRTC carrier requires remote node id"))?;
2404            let local_node_id = self
2405                .current_node_id()
2406                .await
2407                .ok_or_else(|| anyhow::anyhow!("WebRTC carrier requires local node id"))?;
2408            if local_node_id.as_str() <= remote_node_id {
2409                self.record_native_iroh_carrier_debug_event(
2410                    connection_id,
2411                    "",
2412                    "start-deferred",
2413                    Some("deterministic-responder"),
2414                );
2415                return Ok(());
2416            }
2417            let endpoint_id = remote_node_id.parse::<iroh::EndpointId>()?;
2418            let generation = self
2419                .current_native_peer_data_generation(connection_id, None)
2420                .await
2421                .ok_or_else(|| anyhow::anyhow!("WebRTC carrier generation is unavailable"))?;
2422            if let Some((_terminal, reason)) = self.session_admission_block_reason_for_transport(
2423                connection_id,
2424                Some(generation.transport_stable_id),
2425            ) {
2426                self.record_native_iroh_carrier_debug_event(
2427                    connection_id,
2428                    "",
2429                    "start-deferred",
2430                    Some(&reason),
2431                );
2432                return Ok(());
2433            }
2434            let selected_attempt = self
2435                .native_webrtc_carrier_attempts
2436                .read()
2437                .await
2438                .get(connection_id)
2439                .filter(|attempt| {
2440                    attempt.remote_endpoint_id == endpoint_id
2441                        && attempt.generation.transport_generation.saturating_add(1)
2442                            == generation.transport_generation
2443                })
2444                .cloned();
2445            let selected_record = self
2446                .connection_manager
2447                .get_by_connection_id(connection_id)
2448                .await
2449                .is_some_and(|record| {
2450                    record.transport_stable_id == Some(generation.transport_stable_id)
2451                        && record.active_transport == crate::transport_label::WEBRTC
2452                });
2453            let refreshing = selected_attempt.as_ref().is_some_and(|attempt| {
2454                attempt.ice_refresh.try_lock().is_err()
2455                    || attempt
2456                        .ice_refresh_baseline
2457                        .try_lock()
2458                        .map(|baseline| baseline.is_some())
2459                        .unwrap_or(true)
2460            });
2461            let healthy = selected_attempt.as_ref().is_some_and(|attempt| {
2462                attempt.channel.state() == crate::transport::NativeWebRTCState::Connected
2463            });
2464            if selected_attempt.is_some() && (refreshing || (selected_record && healthy)) {
2465                // The path watcher can briefly observe Relay while Iroh converges
2466                // on an already-proven custom path. The selected carrier owns the
2467                // current physical generation; preparing another attempt would
2468                // close that healthy DataChannel and manufacture LocallyClosed.
2469                self.record_native_iroh_carrier_debug_event(
2470                    connection_id,
2471                    "",
2472                    "start-deferred",
2473                    Some("carrier-already-selected"),
2474                );
2475                return Ok(());
2476            }
2477            let base_path = self.iroh_path_kind(remote_node_id).await;
2478            if !crate::iroh_connection_policy::custom_carrier_base_allows(
2479                base_path,
2480                IrohPathKind::WebRtc,
2481                false,
2482            ) {
2483                let reason = format!("base-path-not-relay:{base_path:?}");
2484                self.record_native_iroh_carrier_debug_event(
2485                    connection_id,
2486                    "",
2487                    "start-deferred",
2488                    Some(&reason),
2489                );
2490                return Ok(());
2491            }
2492            self.get_connection(endpoint_id)
2493                .await
2494                .ok_or_else(|| anyhow::anyhow!("WebRTC carrier base is unavailable"))?;
2495            let kind = IrohPathKind::WebRtc;
2496            let gate_key = (connection_id.to_string(), kind);
2497            let bootstrap = crate::iroh_carrier_bootstrap::CarrierBootstrapFrame::request(
2498                crate::iroh_carrier_bootstrap::CarrierBootstrapKind::WebRtc,
2499                generation.into(),
2500                retry_count.saturating_add(1),
2501            )?;
2502            {
2503                let mut gates = self.native_transport_upgrade_gates.lock().await;
2504                if !reserve_native_transport_upgrade_gate(
2505                    &mut gates,
2506                    gate_key.clone(),
2507                    &bootstrap.upgrade_id,
2508                    generation,
2509                    self.current_iroh_carrier_policy_epoch(),
2510                    self.current_iroh_carrier_peer_policy_epoch(connection_id),
2511                    false,
2512                ) {
2513                    self.record_native_iroh_carrier_debug_event(
2514                        connection_id,
2515                        &bootstrap.upgrade_id,
2516                        "start-deferred",
2517                        Some("upgrade-gate-active"),
2518                    );
2519                    return Ok(());
2520                }
2521            }
2522            let result = async {
2523                let attempt = self
2524                    .create_native_webrtc_carrier_attempt(
2525                        connection_id,
2526                        remote_node_id,
2527                        bootstrap.clone(),
2528                        generation,
2529                        crate::transport::NativeWebRTCRole::Initiator,
2530                        false,
2531                    )
2532                    .await?;
2533                self.send_iroh_carrier_control(connection_id, &bootstrap)
2534                    .await?;
2535                attempt.channel.start().await?;
2536                Ok::<(), anyhow::Error>(())
2537            }
2538            .await;
2539            if let Err(error) = result {
2540                eprintln!(
2541                "[OpenRTC][WebRTC carrier] start failed connection_id={} upgrade_id={} retry_count={} error={error:#}",
2542                connection_id, bootstrap.upgrade_id, retry_count,
2543            );
2544                let retired = self
2545                    .fail_native_webrtc_carrier_attempt(
2546                        connection_id,
2547                        remote_node_id,
2548                        &bootstrap,
2549                        "carrier-start-failed",
2550                        true,
2551                    )
2552                    .await;
2553                if let Some(failed_gate) = retired {
2554                    if self
2555                        .schedule_failed_webrtc_retry(
2556                            connection_id,
2557                            remote_node_id,
2558                            &bootstrap,
2559                            "carrier-start-failed",
2560                            failed_gate,
2561                        )
2562                        .await
2563                    {
2564                        return Ok(());
2565                    }
2566                }
2567                return Err(error);
2568            }
2569            let client = self.clone();
2570            let connection_id = connection_id.to_string();
2571            let remote_node_id = remote_node_id.to_string();
2572            tokio::spawn(async move {
2573                let mut completion_phase_observed = false;
2574                loop {
2575                    let timeout = if completion_phase_observed { 45 } else { 30 };
2576                    tokio::time::sleep(std::time::Duration::from_secs(timeout)).await;
2577                    let still_current = client
2578                        .native_transport_upgrade_gates
2579                        .lock()
2580                        .await
2581                        .get(&(connection_id.clone(), kind))
2582                        .is_some_and(|gate| {
2583                            native_transport_upgrade_gate_matches(gate, &bootstrap.upgrade_id)
2584                        });
2585                    if !still_current {
2586                        return;
2587                    }
2588                    let proving = client
2589                        .native_webrtc_carrier_attempts
2590                        .read()
2591                        .await
2592                        .get(&connection_id)
2593                        .filter(|attempt| attempt.upgrade_id == bootstrap.upgrade_id)
2594                        .is_some_and(|attempt| {
2595                            attempt
2596                                .completion_started
2597                                .load(std::sync::atomic::Ordering::Acquire)
2598                        });
2599                    if proving && !completion_phase_observed {
2600                        completion_phase_observed = true;
2601                        continue;
2602                    }
2603                    {
2604                        let retired = client
2605                            .fail_native_webrtc_carrier_attempt(
2606                                &connection_id,
2607                                &remote_node_id,
2608                                &bootstrap,
2609                                "carrier-timeout",
2610                                true,
2611                            )
2612                            .await;
2613                        if let Some(failed_gate) = retired {
2614                            let _ = client
2615                                .schedule_failed_webrtc_retry(
2616                                    &connection_id,
2617                                    &remote_node_id,
2618                                    &bootstrap,
2619                                    "carrier-timeout",
2620                                    failed_gate,
2621                                )
2622                                .await;
2623                        }
2624                    }
2625                    return;
2626                }
2627            });
2628            Ok(())
2629        })
2630    }
2631
2632    /// Thin native hosts submit availability facts here, not carrier choices.
2633    /// A brief visibility change is insufficient to refresh ICE. This does not
2634    /// alter execution grants, app-wide background state or logical readiness.
2635    #[cfg(all(not(target_arch = "wasm32"), feature = "transport-webrtc"))]
2636    pub async fn observe_webrtc_availability(&self, observation: &str) -> usize {
2637        if !self
2638            .webrtc_availability
2639            .lock()
2640            .unwrap_or_else(|error| error.into_inner())
2641            .observe(observation)
2642        {
2643            return 0;
2644        }
2645        let mut refreshed = 0;
2646        for record in self.connection_manager.list_active().await {
2647            match self.refresh_native_webrtc_ice(&record.connection_id, false).await {
2648                Ok(true) => refreshed += 1,
2649                Ok(false) => {},
2650                Err(error) => eprintln!("[OpenRTC][WebRTC carrier] availability refresh failed connection_id={} error={error}", record.connection_id),
2651            }
2652        }
2653        refreshed
2654    }
2655
2656    #[cfg(all(not(target_arch = "wasm32"), feature = "transport-webrtc"))]
2657    async fn native_ice_refresh_is_current(
2658        &self,
2659        connection_id: &str,
2660        attempt: &Arc<NativeWebRtcCarrierAttempt>,
2661    ) -> bool {
2662        if !self.is_webrtc_carrier_enabled().await
2663            || !self
2664                .native_webrtc_carrier_attempts
2665                .read()
2666                .await
2667                .get(connection_id)
2668                .is_some_and(|live| Arc::ptr_eq(live, attempt))
2669        {
2670            return false;
2671        }
2672        let Some(connection) = self.get_connection(attempt.remote_endpoint_id).await else {
2673            return false;
2674        };
2675        let stable_id = crate::transport_generation::for_connection(&connection);
2676        if attempt
2677            .ice_refresh_baseline
2678            .lock()
2679            .await
2680            .as_ref()
2681            .is_some_and(|loan| {
2682                loan.policy_epoch != self.current_iroh_carrier_policy_epoch()
2683                    || loan.peer_policy_epoch
2684                        != self.current_iroh_carrier_peer_policy_epoch(connection_id)
2685            })
2686        {
2687            return false;
2688        }
2689        self.current_native_peer_data_generation(connection_id, Some(stable_id))
2690            .await
2691            .is_some_and(|generation| {
2692                generation.transport_generation
2693                    == attempt.generation.transport_generation.saturating_add(1)
2694                    && self
2695                        .session_admission_block_reason_for_transport(
2696                            connection_id,
2697                            Some(stable_id),
2698                        )
2699                        .is_none()
2700            })
2701    }
2702
2703    /// Refresh only a committed, admitted WebRTC mechanism. The same
2704    /// deterministic initiator handles local and remote network changes; no
2705    /// replacement, logical disconnect, admission, or gateway work is created.
2706    #[cfg(all(not(target_arch = "wasm32"), feature = "transport-webrtc"))]
2707    pub(super) async fn refresh_native_webrtc_ice(
2708        &self,
2709        connection_id: &str,
2710        peer_requested: bool,
2711    ) -> anyhow::Result<bool> {
2712        let attempt = self
2713            .native_webrtc_carrier_attempts
2714            .read()
2715            .await
2716            .get(connection_id)
2717            .cloned();
2718        let Some(attempt) = attempt else {
2719            return Ok(false);
2720        };
2721        if peer_requested && !attempt.channel.is_initiator() {
2722            return Ok(false);
2723        }
2724        let Some(record) = self
2725            .connection_manager
2726            .get_by_connection_id(connection_id)
2727            .await
2728        else {
2729            return Ok(false);
2730        };
2731        if !self.is_webrtc_carrier_enabled().await
2732            || record.state != crate::connection_manager::ConnectionState::Connected
2733            || record.active_transport != crate::transport_label::WEBRTC
2734            || record.transport_generation
2735                != attempt.generation.transport_generation.saturating_add(1)
2736            || record.endpoint_id.as_deref().or(record.node_id.as_deref())
2737                != Some(attempt.remote_endpoint_id.to_string().as_str())
2738            || self
2739                .session_admission_block_reason_for_transport(
2740                    connection_id,
2741                    record.transport_stable_id,
2742                )
2743                .is_some()
2744            || attempt.channel.state() != crate::transport::NativeWebRTCState::Connected
2745        {
2746            return Ok(false);
2747        }
2748        let Some(current) = self.get_connection(attempt.remote_endpoint_id).await else {
2749            return Ok(false);
2750        };
2751        if record.transport_stable_id != Some(crate::transport_generation::for_connection(&current))
2752        {
2753            return Ok(false);
2754        }
2755        let mut gate = attempt.ice_refresh.lock().await;
2756        if !self
2757            .native_webrtc_carrier_attempts
2758            .read()
2759            .await
2760            .get(connection_id)
2761            .is_some_and(|live| Arc::ptr_eq(live, &attempt))
2762            || !gate.reserve(web_time::Instant::now())
2763        {
2764            return Ok(false);
2765        }
2766        self.record_native_iroh_carrier_debug_event(
2767            connection_id,
2768            &attempt.upgrade_id,
2769            "ice-refresh",
2770            Some(if peer_requested {
2771                "peer-network-change"
2772            } else {
2773                "host-network-change"
2774            }),
2775        );
2776        if attempt.channel.is_initiator() {
2777            let policy_epoch = self.current_iroh_carrier_policy_epoch();
2778            let peer_policy_epoch = self.current_iroh_carrier_peer_policy_epoch(connection_id);
2779            let node = self.iroh_node.read().await.as_ref().cloned();
2780            let Some(node) = node else { return Ok(false) };
2781            let Some(baseline) = node
2782                .baseline_for_ice_refresh(attempt.remote_endpoint_id)
2783                .await
2784            else {
2785                self.record_native_iroh_carrier_debug_event(
2786                    connection_id,
2787                    &attempt.upgrade_id,
2788                    "ice-refresh-deferred",
2789                    Some("independent-control-path-unavailable"),
2790                );
2791                return Ok(false);
2792            };
2793            if policy_epoch != self.current_iroh_carrier_policy_epoch()
2794                || peer_policy_epoch != self.current_iroh_carrier_peer_policy_epoch(connection_id)
2795                || !self
2796                    .native_ice_refresh_is_current(connection_id, &attempt)
2797                    .await
2798            {
2799                return Ok(false);
2800            }
2801            let Some(refresh_id) = crate::packet_carrier_transport::new_ice_refresh_id() else {
2802                return Ok(false);
2803            };
2804            *attempt.ice_refresh_baseline.lock().await =
2805                Some(crate::packet_carrier_transport::WebRtcIceRefreshPathLoan {
2806                    _preference: baseline,
2807                    since: web_time::Instant::now(),
2808                    refresh_id: refresh_id.clone(),
2809                    policy_epoch,
2810                    peer_policy_epoch,
2811                    started: false,
2812                });
2813            // Only an updated peer can acknowledge an independent control
2814            // path. An older SDK ignores this request and keeps its carrier.
2815            let sent = self.send_typescript_handshake_over_native_main(connection_id, &serde_json::json!({
2816                "type": "#pluto-signal", "content": {
2817                    "transport": "webrtc", "type": "refresh-prepare", "negotiationId": attempt.upgrade_id, "refreshId": refresh_id,
2818                }
2819            })).await;
2820            if let Err(error) = sent {
2821                attempt.ice_refresh_baseline.lock().await.take();
2822                return Err(error);
2823            }
2824        } else {
2825            self.send_typescript_handshake_over_native_main(
2826                connection_id,
2827                &serde_json::json!({
2828                    "type": "#pluto-signal",
2829                    "content": {
2830                        "transport": "webrtc",
2831                        "type": "refresh-ice",
2832                        "negotiationId": attempt.upgrade_id,
2833                    }
2834                }),
2835            )
2836            .await?;
2837        }
2838        Ok(true)
2839    }
2840
2841    #[cfg(all(not(target_arch = "wasm32"), feature = "transport-webrtc"))]
2842    async fn handle_native_webrtc_iroh_carrier_control(
2843        &self,
2844        connection_id: &str,
2845        remote_node_id: Option<&str>,
2846        frame: &serde_json::Value,
2847    ) -> bool {
2848        if let Some(signal) = parse_webrtc_signal_message_for_transport(frame, "webrtc") {
2849            let attempt = self
2850                .native_webrtc_carrier_attempts
2851                .read()
2852                .await
2853                .get(connection_id)
2854                .cloned();
2855            if let Some(attempt) = attempt {
2856                if iroh_webrtc_signal_matches_upgrade(&signal, &attempt.upgrade_id) {
2857                    let refresh_id = frame
2858                        .get("content")
2859                        .and_then(|content| content.get("refreshId"))
2860                        .and_then(serde_json::Value::as_str)
2861                        .filter(|id| id.len() == 32 && id.bytes().all(|b| b.is_ascii_hexdigit()));
2862                    if matches!(
2863                        signal.signal_type.as_str(),
2864                        "refresh-prepare" | "refresh-ready"
2865                    ) && !self
2866                        .native_ice_refresh_is_current(connection_id, &attempt)
2867                        .await
2868                    {
2869                        return true;
2870                    }
2871                    if signal.signal_type == "refresh-prepare" {
2872                        let Some(refresh_id) = refresh_id else {
2873                            return true;
2874                        };
2875                        if attempt.channel.is_initiator() {
2876                            return true;
2877                        }
2878                        let current = self
2879                            .current_native_peer_data_generation(connection_id, None)
2880                            .await;
2881                        if !current.is_some_and(|current| {
2882                            current.transport_generation
2883                                == attempt.generation.transport_generation.saturating_add(1)
2884                                && self
2885                                    .session_admission_block_reason_for_transport(
2886                                        connection_id,
2887                                        Some(current.transport_stable_id),
2888                                    )
2889                                    .is_none()
2890                        }) {
2891                            return true;
2892                        }
2893                        let _refresh = attempt.ice_refresh.lock().await;
2894                        if attempt.ice_refresh_baseline.lock().await.is_none() {
2895                            let policy_epoch = self.current_iroh_carrier_policy_epoch();
2896                            let peer_policy_epoch =
2897                                self.current_iroh_carrier_peer_policy_epoch(connection_id);
2898                            let node = self.iroh_node.read().await.as_ref().cloned();
2899                            let Some(node) = node else { return true };
2900                            let Some(baseline) = node
2901                                .baseline_for_ice_refresh(attempt.remote_endpoint_id)
2902                                .await
2903                            else {
2904                                return true;
2905                            };
2906                            if policy_epoch != self.current_iroh_carrier_policy_epoch()
2907                                || peer_policy_epoch
2908                                    != self.current_iroh_carrier_peer_policy_epoch(connection_id)
2909                                || !self
2910                                    .native_ice_refresh_is_current(connection_id, &attempt)
2911                                    .await
2912                            {
2913                                return true;
2914                            }
2915                            *attempt.ice_refresh_baseline.lock().await =
2916                                Some(crate::packet_carrier_transport::WebRtcIceRefreshPathLoan {
2917                                    _preference: baseline,
2918                                    since: web_time::Instant::now(),
2919                                    refresh_id: refresh_id.to_owned(),
2920                                    policy_epoch,
2921                                    peer_policy_epoch,
2922                                    started: false,
2923                                });
2924                        }
2925                        if !attempt
2926                            .ice_refresh_baseline
2927                            .lock()
2928                            .await
2929                            .as_ref()
2930                            .is_some_and(|loan| loan.refresh_id == refresh_id)
2931                        {
2932                            return true;
2933                        }
2934                        let _ = self.send_typescript_handshake_over_native_main(connection_id, &serde_json::json!({
2935                            "type": "#pluto-signal", "content": {
2936                                "transport": "webrtc", "type": "refresh-ready", "negotiationId": attempt.upgrade_id, "refreshId": refresh_id,
2937                            }
2938                        })).await;
2939                    } else if signal.signal_type == "refresh-ready" {
2940                        if !attempt.channel.is_initiator() {
2941                            return true;
2942                        }
2943                        let start = {
2944                            let mut baseline = attempt.ice_refresh_baseline.lock().await;
2945                            baseline
2946                                .as_mut()
2947                                .filter(|loan| {
2948                                    !loan.started
2949                                        && loan.since.elapsed().as_secs() < 30
2950                                        && Some(loan.refresh_id.as_str()) == refresh_id
2951                                })
2952                                .map(|loan| {
2953                                    loan.started = true;
2954                                })
2955                                .is_some()
2956                        };
2957                        if start && !matches!(attempt.channel.refresh_ice().await, Ok(true)) {
2958                            attempt.ice_refresh_baseline.lock().await.take();
2959                        }
2960                    } else if signal.signal_type == "refresh-ice" {
2961                        let _ = self.refresh_native_webrtc_ice(connection_id, true).await;
2962                    } else {
2963                        let restart_offer = signal.signal_type == "sdp"
2964                            && signal.sdp.as_ref().is_some_and(|sdp| {
2965                                sdp.get("type").and_then(serde_json::Value::as_str) == Some("offer")
2966                            })
2967                            && attempt.channel.state()
2968                                == crate::transport::NativeWebRTCState::Connected;
2969                        // Keep the opportunistic selector from starting a
2970                        // competing candidate during this baseline-path loan.
2971                        let _refresh = if restart_offer {
2972                            Some(attempt.ice_refresh.lock().await)
2973                        } else {
2974                            None
2975                        };
2976                        if restart_offer {
2977                            let mut baseline = attempt.ice_refresh_baseline.lock().await;
2978                            // Legacy disconnection recovery may send an offer
2979                            // without a path loan; preserve its existing handler.
2980                            if let Some(loan) = baseline.as_mut() {
2981                                loan.started = true;
2982                                attempt.channel.mark_remote_ice_refresh();
2983                            }
2984                        }
2985                        if attempt.channel.handle_signal(signal).await.is_err() && restart_offer {
2986                            attempt.ice_refresh_baseline.lock().await.take();
2987                        }
2988                    }
2989                } else {
2990                    eprintln!(
2991                        "[OpenRTC][WebRTC carrier] ignored stale signal connection_id={} current_upgrade_id={} signal_upgrade_id={:?}",
2992                        connection_id, attempt.upgrade_id, signal.negotiation_id,
2993                    );
2994                }
2995            }
2996            return true;
2997        }
2998        let Some(bootstrap) =
2999            crate::iroh_carrier_bootstrap::CarrierBootstrapFrame::from_json(frame)
3000        else {
3001            if frame.get("type").and_then(serde_json::Value::as_str)
3002                == Some(crate::iroh_carrier_bootstrap::CARRIER_BOOTSTRAP_FRAME_TYPE)
3003            {
3004                eprintln!("[OpenRTC][WebRTC carrier] rejected invalid bootstrap envelope={frame}");
3005            }
3006            return false;
3007        };
3008        if bootstrap.carrier != crate::iroh_carrier_bootstrap::CarrierBootstrapKind::WebRtc {
3009            return false;
3010        }
3011        let Some(remote_node_id) = remote_node_id
3012            .map(str::trim)
3013            .filter(|value| !value.is_empty())
3014        else {
3015            return true;
3016        };
3017        let kind = IrohPathKind::WebRtc;
3018        let endpoint_id = match remote_node_id.parse::<iroh::EndpointId>() {
3019            Ok(value) => value,
3020            Err(_) => return true,
3021        };
3022        match bootstrap.action {
3023            crate::iroh_carrier_bootstrap::CarrierBootstrapAction::Request => {
3024                let local_node_id = match self.current_node_id().await {
3025                    Some(value) => value,
3026                    None => {
3027                        eprintln!("[OpenRTC][WebRTC carrier] responder skipped connection_id={connection_id} reason=local-endpoint-unavailable");
3028                        return true;
3029                    }
3030                };
3031                if local_node_id.as_str() >= remote_node_id {
3032                    eprintln!("[OpenRTC][WebRTC carrier] responder skipped connection_id={connection_id} reason=deterministic-initiator");
3033                    return true;
3034                }
3035                if !self.is_webrtc_carrier_enabled().await {
3036                    eprintln!("[OpenRTC][WebRTC carrier] responder skipped connection_id={connection_id} reason=carrier-disabled");
3037                    return true;
3038                }
3039                let duplicate_attempt = self
3040                    .native_webrtc_carrier_attempts
3041                    .read()
3042                    .await
3043                    .get(connection_id)
3044                    .filter(|attempt| {
3045                        native_carrier_duplicate_request_is_live(
3046                            &bootstrap,
3047                            &attempt.bootstrap,
3048                            matches!(
3049                                attempt.channel.state(),
3050                                crate::transport::NativeWebRTCState::Failed
3051                                    | crate::transport::NativeWebRTCState::Closed
3052                            ),
3053                        )
3054                    })
3055                    .cloned();
3056                if let Some(attempt) = duplicate_attempt {
3057                    if attempt.channel.state() == crate::transport::NativeWebRTCState::Connected {
3058                        if let Ok(ready) =
3059                            crate::iroh_carrier_bootstrap::CarrierBootstrapFrame::ready_from(
3060                                &bootstrap,
3061                            )
3062                        {
3063                            let _ = self.send_iroh_carrier_control(connection_id, &ready).await;
3064                        }
3065                    }
3066                    eprintln!("[OpenRTC][WebRTC carrier] responder reused connection_id={connection_id} reason=duplicate-live-bootstrap");
3067                    return true;
3068                }
3069                if !crate::iroh_connection_policy::custom_carrier_base_allows(
3070                    self.iroh_path_kind(remote_node_id).await,
3071                    IrohPathKind::WebRtc,
3072                    false,
3073                ) {
3074                    eprintln!("[OpenRTC][WebRTC carrier] responder skipped connection_id={connection_id} reason=carrier-already-selected");
3075                    return true;
3076                }
3077                if self.get_connection(endpoint_id).await.is_none() {
3078                    eprintln!("[OpenRTC][WebRTC carrier] responder skipped connection_id={connection_id} reason=base-unavailable");
3079                    return true;
3080                }
3081                // `bootstrap.base` is the initiator's process-local fence. The
3082                // responder owns a distinct lifecycle and fences this attempt
3083                // with its own current generation; Ready echoes the initiator's
3084                // fence so the initiator can validate it on return.
3085                let generation = match self
3086                    .current_native_peer_data_generation(connection_id, None)
3087                    .await
3088                {
3089                    Some(value) => value,
3090                    None => {
3091                        eprintln!("[OpenRTC][WebRTC carrier] responder skipped connection_id={connection_id} reason=generation-unavailable");
3092                        return true;
3093                    }
3094                };
3095                if self
3096                    .reject_native_iroh_carrier_request_if_base_blocked(
3097                        connection_id,
3098                        &bootstrap,
3099                        generation.transport_stable_id,
3100                        "WebRTC",
3101                    )
3102                    .await
3103                {
3104                    return true;
3105                }
3106                let node = match self.iroh_node.read().await.as_ref().cloned() {
3107                    Some(value) => value,
3108                    None => {
3109                        eprintln!("[OpenRTC][WebRTC carrier] responder skipped connection_id={connection_id} reason=node-unavailable");
3110                        return true;
3111                    }
3112                };
3113                let provider = match self.native_transport_upgrade_provider(kind).await {
3114                    Some(value) => value,
3115                    None => {
3116                        eprintln!("[OpenRTC][WebRTC carrier] responder skipped connection_id={connection_id} reason=provider-unavailable");
3117                        return true;
3118                    }
3119                };
3120                let gate_key = (connection_id.to_string(), kind);
3121                let current_attempt = self
3122                    .native_webrtc_carrier_attempts
3123                    .read()
3124                    .await
3125                    .get(connection_id)
3126                    .cloned();
3127                {
3128                    let mut gates = self.native_transport_upgrade_gates.lock().await;
3129                    if let Some(current) = gates.get(&gate_key).filter(|current| {
3130                        self.iroh_carrier_policy_epoch_is_current(current.policy_epoch)
3131                            && current.peer_policy_epoch
3132                                == self.current_iroh_carrier_peer_policy_epoch(connection_id)
3133                    }) {
3134                        if native_webrtc_duplicate_bootstrap_is_reserved_or_live(
3135                            current,
3136                            &bootstrap.upgrade_id,
3137                            generation,
3138                            current_attempt.as_deref(),
3139                        ) {
3140                            eprintln!("[OpenRTC][WebRTC carrier] responder reused connection_id={connection_id} reason=duplicate-bootstrap");
3141                            return true;
3142                        } else if native_transport_upgrade_gate_matches(
3143                            current,
3144                            &bootstrap.upgrade_id,
3145                        ) {
3146                            // The same bootstrap ID can be replayed after its
3147                            // prior DataChannel or base generation retired.
3148                            // It is no longer a duplicate of a live attempt.
3149                            gates.remove(&gate_key);
3150                        } else {
3151                            eprintln!("[OpenRTC][WebRTC carrier] responder skipped connection_id={connection_id} reason=competing-upgrade");
3152                            return true;
3153                        }
3154                    }
3155                    if !reserve_native_transport_upgrade_gate(
3156                        &mut gates,
3157                        gate_key.clone(),
3158                        &bootstrap.upgrade_id,
3159                        generation,
3160                        self.current_iroh_carrier_policy_epoch(),
3161                        self.current_iroh_carrier_peer_policy_epoch(connection_id),
3162                        true,
3163                    ) {
3164                        eprintln!("[OpenRTC][WebRTC carrier] responder skipped connection_id={connection_id} reason=reservation-rejected");
3165                        return true;
3166                    }
3167                }
3168                let authorization_expiry = node
3169                    .authorize_inbound_replacement(
3170                        endpoint_id,
3171                        provider.transport_id(),
3172                        std::time::Duration::from_secs(45),
3173                    )
3174                    .await;
3175
3176                // The native-main signal dispatcher is ordered, but the
3177                // initiator can enqueue its SDP offer and ICE candidates
3178                // immediately after the bootstrap request. Install and start
3179                // the responder attempt before returning to that dispatcher;
3180                // spawning attempt creation here would let those already-
3181                // buffered frames run first and be discarded because no
3182                // attempt existed yet. The long-lived proof/commit wait stays
3183                // asynchronous below.
3184                let attempt = match self
3185                    .create_native_webrtc_carrier_attempt(
3186                        connection_id,
3187                        remote_node_id,
3188                        bootstrap.clone(),
3189                        generation,
3190                        crate::transport::NativeWebRTCRole::Responder,
3191                        true,
3192                    )
3193                    .await
3194                {
3195                    Ok(attempt) => attempt,
3196                    Err(error) => {
3197                        node.revoke_inbound_replacement_if_current(
3198                            endpoint_id,
3199                            provider.transport_id(),
3200                            authorization_expiry,
3201                        )
3202                        .await;
3203                        let failure_code = native_iroh_carrier_failure_code(&error);
3204                        eprintln!(
3205                            "[OpenRTC][WebRTC carrier] responder start failed connection_id={} upgrade_id={} failure_code={} error={:#}",
3206                            connection_id, bootstrap.upgrade_id, failure_code, error,
3207                        );
3208                        self.fail_native_webrtc_carrier_attempt(
3209                            connection_id,
3210                            remote_node_id,
3211                            &bootstrap,
3212                            failure_code,
3213                            true,
3214                        )
3215                        .await;
3216                        return true;
3217                    }
3218                };
3219                let client = self.clone();
3220                let connection_id = connection_id.to_string();
3221                let remote_node_id = remote_node_id.to_string();
3222                tokio::spawn(async move {
3223                    let result = async {
3224                        client
3225                            .wait_native_webrtc_carrier_connected(
3226                                &connection_id,
3227                                &bootstrap.upgrade_id,
3228                            )
3229                            .await?;
3230                        let ready =
3231                            crate::iroh_carrier_bootstrap::CarrierBootstrapFrame::ready_from(
3232                                &bootstrap,
3233                            )?;
3234                        // Ready means this endpoint's relay session is prepared.
3235                        // The browser opens its directed subscription in response;
3236                        // waiting for that subscription before Ready deadlocks the
3237                        // browser-initiated role.
3238                        client
3239                            .send_iroh_carrier_control(&connection_id, &ready)
3240                            .await?;
3241                        client
3242                            .complete_inbound_native_iroh_carrier_upgrade(
3243                                &connection_id,
3244                                &remote_node_id,
3245                                &bootstrap.upgrade_id,
3246                                generation,
3247                                bootstrap.base.transport_generation,
3248                                bootstrap.base.route_generation,
3249                                kind,
3250                                bootstrap.carrier.transport_id(),
3251                                authorization_expiry,
3252                            )
3253                            .await?;
3254                        let _ = attempt;
3255                        Ok::<(), anyhow::Error>(())
3256                    }
3257                    .await;
3258                    node.revoke_inbound_replacement_if_current(
3259                        endpoint_id,
3260                        provider.transport_id(),
3261                        authorization_expiry,
3262                    )
3263                    .await;
3264                    if let Err(error) = result {
3265                        let failure_code = native_iroh_carrier_failure_code(&error);
3266                        eprintln!(
3267                            "[OpenRTC][WebRTC carrier] responder completion failed connection_id={} upgrade_id={} failure_code={} error={:#}",
3268                            connection_id, bootstrap.upgrade_id, failure_code, error,
3269                        );
3270                        client
3271                            .fail_native_webrtc_carrier_attempt(
3272                                &connection_id,
3273                                &remote_node_id,
3274                                &bootstrap,
3275                                failure_code,
3276                                true,
3277                            )
3278                            .await;
3279                    } else {
3280                        remove_native_transport_upgrade_gate(
3281                            &client.native_transport_upgrade_gates,
3282                            &(connection_id.clone(), kind),
3283                            &bootstrap.upgrade_id,
3284                        )
3285                        .await;
3286                    }
3287                });
3288            }
3289            crate::iroh_carrier_bootstrap::CarrierBootstrapAction::Ready => {
3290                let attempt = self
3291                    .native_webrtc_carrier_attempts
3292                    .read()
3293                    .await
3294                    .get(connection_id)
3295                    .filter(|attempt| bootstrap.is_response_to(&attempt.bootstrap))
3296                    .cloned();
3297                let Some(attempt) = attempt else {
3298                    return true;
3299                };
3300                if attempt
3301                    .completion_started
3302                    .swap(true, std::sync::atomic::Ordering::AcqRel)
3303                {
3304                    return true;
3305                }
3306                let client = self.clone();
3307                let connection_id = connection_id.to_string();
3308                let remote_node_id = remote_node_id.to_string();
3309                tokio::spawn(async move {
3310                    let result = async {
3311                        client
3312                            .wait_native_webrtc_carrier_connected(
3313                                &connection_id,
3314                                &bootstrap.upgrade_id,
3315                            )
3316                            .await?;
3317                        client
3318                            .complete_native_iroh_carrier_upgrade(
3319                                &connection_id,
3320                                &remote_node_id,
3321                                &bootstrap.upgrade_id,
3322                                attempt.generation,
3323                                kind,
3324                            )
3325                            .await
3326                    }
3327                    .await;
3328                    if let Err(error) = result {
3329                        let failure_code = native_iroh_carrier_failure_code(&error);
3330                        let error_detail = format!("{error:#}");
3331                        client.record_native_iroh_carrier_debug_event(
3332                            &connection_id,
3333                            &bootstrap.upgrade_id,
3334                            "completion-failed",
3335                            Some(&error_detail),
3336                        );
3337                        eprintln!(
3338                            "[OpenRTC][WebRTC carrier] initiator completion failed connection_id={} upgrade_id={} failure_code={} error={error:#}",
3339                            connection_id, bootstrap.upgrade_id, failure_code,
3340                        );
3341                        let retired = client
3342                            .fail_native_webrtc_carrier_attempt(
3343                                &connection_id,
3344                                &remote_node_id,
3345                                &attempt.bootstrap,
3346                                failure_code,
3347                                true,
3348                            )
3349                            .await;
3350                        if let Some(failed_gate) = retired {
3351                            let _ = client
3352                                .schedule_failed_webrtc_retry(
3353                                    &connection_id,
3354                                    &remote_node_id,
3355                                    &attempt.bootstrap,
3356                                    failure_code,
3357                                    failed_gate,
3358                                )
3359                                .await;
3360                        }
3361                    } else {
3362                        remove_native_transport_upgrade_gate(
3363                            &client.native_transport_upgrade_gates,
3364                            &(connection_id.clone(), kind),
3365                            &bootstrap.upgrade_id,
3366                        )
3367                        .await;
3368                    }
3369                });
3370            }
3371            crate::iroh_carrier_bootstrap::CarrierBootstrapAction::Failed => {
3372                let attempt = self
3373                    .native_webrtc_carrier_attempts
3374                    .read()
3375                    .await
3376                    .get(connection_id)
3377                    .filter(|attempt| bootstrap.is_response_to(&attempt.bootstrap))
3378                    .cloned();
3379                if let Some(attempt) = attempt {
3380                    let failure_code = webrtc_peer_failure_code(bootstrap.failure_code.as_deref());
3381                    let retired = self
3382                        .fail_native_webrtc_carrier_attempt(
3383                            connection_id,
3384                            remote_node_id,
3385                            &attempt.bootstrap,
3386                            failure_code,
3387                            false,
3388                        )
3389                        .await;
3390                    if let Some(failed_gate) = retired {
3391                        let _ = self
3392                            .schedule_failed_webrtc_retry(
3393                                connection_id,
3394                                remote_node_id,
3395                                &attempt.bootstrap,
3396                                failure_code,
3397                                failed_gate,
3398                            )
3399                            .await;
3400                    }
3401                }
3402            }
3403        }
3404        true
3405    }
3406
3407    #[cfg(all(not(target_arch = "wasm32"), feature = "transport-moq"))]
3408    async fn create_native_moq_carrier_attempt(
3409        &self,
3410        connection_id: &str,
3411        remote_node_id: &str,
3412        bootstrap: crate::iroh_carrier_bootstrap::CarrierBootstrapFrame,
3413        generation: crate::client::NativePeerDataGeneration,
3414    ) -> anyhow::Result<Arc<crate::client::NativeMoqCarrierAttempt>> {
3415        let config = self
3416            .transport_config
3417            .read()
3418            .await
3419            .moq
3420            .clone()
3421            .ok_or_else(|| anyhow::anyhow!("MoQ carrier config is unavailable"))?;
3422        let local_node_id = self
3423            .current_node_id()
3424            .await
3425            .ok_or_else(|| anyhow::anyhow!("local Iroh node id is unavailable"))?;
3426        let remote_endpoint_id = remote_node_id.parse::<iroh::EndpointId>()?;
3427        // Retire the previous physical attempt before activating its
3428        // replacement. Packet-carrier activation is keyed by carrier kind and
3429        // endpoint, so deactivating the old attempt after activation would
3430        // accidentally remove the newly installed session.
3431        if let Some(previous) = self
3432            .native_moq_carrier_attempts
3433            .write()
3434            .await
3435            .remove(connection_id)
3436        {
3437            previous.session.close();
3438            let _ = previous.pump.lock().await.take();
3439        }
3440        let packet_session = self
3441            .activate_native_iroh_packet_carrier(IrohPathKind::Moq, remote_endpoint_id)
3442            .await?;
3443        let session = Arc::new(
3444            crate::transport::NativeMoQSession::new_packet_carrier(
3445                &local_node_id,
3446                remote_node_id,
3447                &config,
3448                &bootstrap.carrier_session_id,
3449            )
3450            .await?,
3451        );
3452        // Start the relay session before attaching Iroh's outbound packet
3453        // consumer. Recovery and re-share can produce a QUIC Initial as soon
3454        // as the custom transport is activated; attaching first lets that
3455        // packet observe an Idle MoQ session and permanently exits the pump.
3456        if let Err(error) = session.start().await {
3457            return Err(error);
3458        }
3459        let application_key = self
3460            .application_crypto_key_for_connection(Some(connection_id))
3461            .ok_or_else(|| anyhow::anyhow!("MoQ carrier requires an installed application key"))?;
3462        let pump = match crate::native_moq_carrier::NativeMoqCarrierSession::attach(
3463            session.clone(),
3464            packet_session,
3465            bootstrap.frame_expectation()?,
3466            application_key,
3467        )
3468        .await
3469        {
3470            Ok(pump) => pump,
3471            Err(error) => {
3472                session.close_gracefully().await;
3473                return Err(error);
3474            }
3475        };
3476        let attempt = Arc::new(crate::client::NativeMoqCarrierAttempt {
3477            upgrade_id: bootstrap.upgrade_id.clone(),
3478            bootstrap,
3479            generation,
3480            remote_endpoint_id,
3481            session: session.clone(),
3482            pump: tokio::sync::Mutex::new(Some(pump)),
3483        });
3484        if let Some(previous) = self
3485            .native_moq_carrier_attempts
3486            .write()
3487            .await
3488            .insert(connection_id.to_string(), attempt.clone())
3489        {
3490            // A competing creator should be fenced by the upgrade gate. If
3491            // one still wins the map race, close only its mechanism; the
3492            // provider now belongs to the attempt inserted above.
3493            previous.session.close();
3494            let _ = previous.pump.lock().await.take();
3495        }
3496        Ok(attempt)
3497    }
3498
3499    #[cfg(all(not(target_arch = "wasm32"), feature = "transport-moq"))]
3500    async fn wait_native_moq_carrier_ready(
3501        &self,
3502        connection_id: &str,
3503        upgrade_id: &str,
3504    ) -> anyhow::Result<Arc<crate::client::NativeMoqCarrierAttempt>> {
3505        let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(25);
3506        loop {
3507            let attempt = self
3508                .native_moq_carrier_attempts
3509                .read()
3510                .await
3511                .get(connection_id)
3512                .filter(|attempt| attempt.upgrade_id == upgrade_id)
3513                .cloned()
3514                .ok_or_else(|| anyhow::anyhow!("MoQ carrier attempt was retired"))?;
3515            match attempt.session.state() {
3516                crate::transport::NativeMoQState::Connected
3517                    if attempt.session.is_peer_data_bidirectionally_ready().await =>
3518                {
3519                    // The protected Ready exchange proves that the peer has
3520                    // attached its packet pumps. Native still waits for the
3521                    // relay to acknowledge both directed tracks before dialing
3522                    // Iroh so the first request and response cannot race a
3523                    // one-sided MoQ publication.
3524                    return Ok(attempt);
3525                }
3526                crate::transport::NativeMoQState::Failed
3527                | crate::transport::NativeMoQState::Closed => {
3528                    anyhow::bail!(
3529                        "MoQ Draft 14 reliable object-stream carrier negotiation failed: {}",
3530                        attempt.session.peer_data_readiness_diagnostic().await,
3531                    )
3532                }
3533                _ if tokio::time::Instant::now() >= deadline => {
3534                    anyhow::bail!(
3535                        "MoQ Draft 14 reliable object-stream carrier negotiation timed out: {}",
3536                        attempt.session.peer_data_readiness_diagnostic().await,
3537                    )
3538                }
3539                _ => tokio::time::sleep(std::time::Duration::from_millis(25)).await,
3540            }
3541        }
3542    }
3543
3544    #[cfg(all(not(target_arch = "wasm32"), feature = "transport-moq"))]
3545    fn spawn_native_moq_carrier_terminal_watcher(
3546        &self,
3547        connection_id: String,
3548        attempt: Arc<crate::client::NativeMoqCarrierAttempt>,
3549    ) {
3550        let client = self.clone();
3551        tokio::spawn(async move {
3552            loop {
3553                let current = client
3554                    .native_moq_carrier_attempts
3555                    .read()
3556                    .await
3557                    .get(&connection_id)
3558                    .is_some_and(|value| Arc::ptr_eq(value, &attempt));
3559                if !current {
3560                    return;
3561                }
3562                if matches!(
3563                    attempt.session.state(),
3564                    crate::transport::NativeMoQState::Failed
3565                        | crate::transport::NativeMoQState::Closed
3566                ) {
3567                    let _ = client
3568                        .close_current_iroh_carrier_generation_with_reason(
3569                            &connection_id,
3570                            attempt.remote_endpoint_id,
3571                            IrohPathKind::Moq,
3572                            attempt.generation.transport_generation.saturating_add(1),
3573                            "iroh-carrier-ended",
3574                        )
3575                        .await;
3576                    return;
3577                }
3578                let terminal_reason = attempt
3579                    .pump
3580                    .lock()
3581                    .await
3582                    .as_ref()
3583                    .and_then(|pump| pump.take_terminal_reason());
3584                if let Some(reason) = terminal_reason {
3585                    let _ = client
3586                        .close_current_iroh_carrier_generation_with_reason(
3587                            &connection_id,
3588                            attempt.remote_endpoint_id,
3589                            IrohPathKind::Moq,
3590                            attempt.generation.transport_generation.saturating_add(1),
3591                            reason,
3592                        )
3593                        .await;
3594                    return;
3595                }
3596                tokio::time::sleep(std::time::Duration::from_millis(250)).await;
3597            }
3598        });
3599    }
3600
3601    #[cfg(all(not(target_arch = "wasm32"), feature = "transport-moq"))]
3602    async fn retire_native_moq_carrier_attempt(&self, connection_id: &str, upgrade_id: &str) {
3603        let removed = {
3604            let mut attempts = self.native_moq_carrier_attempts.write().await;
3605            if attempts
3606                .get(connection_id)
3607                .is_some_and(|attempt| attempt.upgrade_id == upgrade_id)
3608            {
3609                attempts.remove(connection_id)
3610            } else {
3611                None
3612            }
3613        };
3614        if let Some(attempt) = removed {
3615            attempt.session.close();
3616            let _ = attempt.pump.lock().await.take();
3617        }
3618    }
3619
3620    #[cfg(all(not(target_arch = "wasm32"), feature = "transport-moq"))]
3621    async fn fail_native_moq_carrier_attempt(
3622        &self,
3623        connection_id: &str,
3624        remote_node_id: &str,
3625        request: &crate::iroh_carrier_bootstrap::CarrierBootstrapFrame,
3626        failure_code: &'static str,
3627        notify_peer: bool,
3628    ) {
3629        let retry_pending = moq_retry_attempt(failure_code, request.attempt).is_some();
3630        self.retire_native_moq_carrier_attempt(connection_id, &request.upgrade_id)
3631            .await;
3632        remove_native_transport_upgrade_gate(
3633            &self.native_transport_upgrade_gates,
3634            &(connection_id.to_string(), IrohPathKind::Moq),
3635            &request.upgrade_id,
3636        )
3637        .await;
3638        if notify_peer {
3639            if let Ok(failed) = crate::iroh_carrier_bootstrap::CarrierBootstrapFrame::failed_from(
3640                request,
3641                failure_code,
3642            ) {
3643                let _ = self.send_iroh_carrier_control(connection_id, &failed).await;
3644            }
3645        }
3646        eprintln!(
3647            "[OpenRTC][MoQ carrier] attempt failed connection_id={} upgrade_id={} failure_code={}",
3648            connection_id, request.upgrade_id, failure_code,
3649        );
3650        if !retry_pending {
3651            let _ = self
3652                .maybe_start_next_native_iroh_carrier(
3653                    connection_id,
3654                    remote_node_id,
3655                    crate::route_policy::KnownRoute::Moq,
3656                )
3657                .await;
3658        }
3659    }
3660
3661    #[cfg(all(not(target_arch = "wasm32"), feature = "transport-moq"))]
3662    async fn retry_stale_moq(
3663        &self,
3664        connection_id: &str,
3665        remote_node_id: &str,
3666        request: &crate::iroh_carrier_bootstrap::CarrierBootstrapFrame,
3667        failure_code: &str,
3668    ) {
3669        let Some(attempt) = moq_retry_attempt(failure_code, request.attempt) else {
3670            return;
3671        };
3672        // An admitted replacement can supersede the base while the carrier
3673        // proves itself. Retry once on that new generation.
3674        tokio::time::sleep(std::time::Duration::from_millis(500)).await;
3675        if let Err(error) = self
3676            .maybe_start_native_moq_iroh_carrier_attempt(
3677                connection_id,
3678                Some(remote_node_id),
3679                attempt,
3680            )
3681            .await
3682        {
3683            eprintln!(
3684                "[OpenRTC][MoQ carrier] stale-base retry failed connection_id={} attempt={} error={error:#}",
3685                connection_id, attempt,
3686            );
3687        }
3688    }
3689
3690    #[cfg(all(not(target_arch = "wasm32"), feature = "transport-moq"))]
3691    pub(super) async fn maybe_start_native_moq_iroh_carrier(
3692        &self,
3693        connection_id: &str,
3694        remote_node_id: Option<&str>,
3695    ) -> anyhow::Result<()> {
3696        self.maybe_start_native_moq_iroh_carrier_attempt(connection_id, remote_node_id, 1)
3697            .await
3698    }
3699
3700    #[cfg(all(not(target_arch = "wasm32"), feature = "transport-moq"))]
3701    async fn maybe_start_native_moq_iroh_carrier_attempt(
3702        &self,
3703        connection_id: &str,
3704        remote_node_id: Option<&str>,
3705        attempt_number: u8,
3706    ) -> anyhow::Result<()> {
3707        if !self.is_moq_carrier_enabled().await {
3708            return Ok(());
3709        }
3710        let remote_node_id = remote_node_id
3711            .map(str::trim)
3712            .filter(|value| !value.is_empty())
3713            .ok_or_else(|| anyhow::anyhow!("MoQ carrier requires remote node id"))?;
3714        let local_node_id = self
3715            .current_node_id()
3716            .await
3717            .ok_or_else(|| anyhow::anyhow!("MoQ carrier requires local node id"))?;
3718        if local_node_id.as_str() <= remote_node_id {
3719            return Ok(());
3720        }
3721        let endpoint_id = remote_node_id.parse::<iroh::EndpointId>()?;
3722        let generation = self
3723            .current_native_peer_data_generation(connection_id, None)
3724            .await
3725            .ok_or_else(|| anyhow::anyhow!("MoQ carrier generation is unavailable"))?;
3726        let selected_attempt = self
3727            .native_moq_carrier_attempts
3728            .read()
3729            .await
3730            .get(connection_id)
3731            .filter(|attempt| {
3732                attempt.remote_endpoint_id == endpoint_id
3733                    && attempt.session.state() == crate::transport::NativeMoQState::Connected
3734                    && attempt.generation.transport_generation.saturating_add(1)
3735                        == generation.transport_generation
3736            })
3737            .cloned();
3738        let selected_record = self
3739            .connection_manager
3740            .get_by_connection_id(connection_id)
3741            .await
3742            .is_some_and(|record| {
3743                record.transport_stable_id == Some(generation.transport_stable_id)
3744                    && record.active_transport == crate::transport_label::MOQ
3745            });
3746        if selected_attempt.is_some() && selected_record {
3747            return Ok(());
3748        }
3749        // The path observation can briefly remain `Moq` after attempt 1
3750        // is retired for a replaced base generation. Only the bounded stale-
3751        // base retry may cross that transient observation; ordinary starts
3752        // still require a relay or unknown Iroh base path.
3753        if !crate::iroh_connection_policy::custom_carrier_base_allows(
3754            self.iroh_path_kind(remote_node_id).await,
3755            IrohPathKind::Moq,
3756            attempt_number == 2,
3757        ) {
3758            return Ok(());
3759        }
3760        self.get_connection(endpoint_id)
3761            .await
3762            .ok_or_else(|| anyhow::anyhow!("MoQ carrier base is unavailable"))?;
3763        let gate_key = (connection_id.to_string(), IrohPathKind::Moq);
3764        let bootstrap = crate::iroh_carrier_bootstrap::CarrierBootstrapFrame::request(
3765            crate::iroh_carrier_bootstrap::CarrierBootstrapKind::MoqDraft14,
3766            generation.into(),
3767            attempt_number,
3768        )?;
3769        {
3770            let mut gates = self.native_transport_upgrade_gates.lock().await;
3771            if !reserve_native_transport_upgrade_gate(
3772                &mut gates,
3773                gate_key.clone(),
3774                &bootstrap.upgrade_id,
3775                generation,
3776                self.current_iroh_carrier_policy_epoch(),
3777                self.current_iroh_carrier_peer_policy_epoch(connection_id),
3778                false,
3779            ) {
3780                return Ok(());
3781            }
3782        }
3783        let result = async {
3784            self.create_native_moq_carrier_attempt(
3785                connection_id,
3786                remote_node_id,
3787                bootstrap.clone(),
3788                generation,
3789            )
3790            .await?;
3791            self.send_iroh_carrier_control(connection_id, &bootstrap)
3792                .await?;
3793            Ok::<(), anyhow::Error>(())
3794        }
3795        .await;
3796        if let Err(error) = result {
3797            self.fail_native_moq_carrier_attempt(
3798                connection_id,
3799                remote_node_id,
3800                &bootstrap,
3801                "carrier-start-failed",
3802                true,
3803            )
3804            .await;
3805            return Err(error);
3806        }
3807        let client = self.clone();
3808        let connection_id = connection_id.to_string();
3809        let remote_node_id = remote_node_id.to_string();
3810        tokio::spawn(async move {
3811            tokio::time::sleep(std::time::Duration::from_secs(30)).await;
3812            let still_current = client
3813                .native_transport_upgrade_gates
3814                .lock()
3815                .await
3816                .get(&(connection_id.clone(), IrohPathKind::Moq))
3817                .is_some_and(|gate| {
3818                    native_transport_upgrade_gate_matches(gate, &bootstrap.upgrade_id)
3819                });
3820            if still_current {
3821                client
3822                    .fail_native_moq_carrier_attempt(
3823                        &connection_id,
3824                        &remote_node_id,
3825                        &bootstrap,
3826                        "carrier-timeout",
3827                        true,
3828                    )
3829                    .await;
3830            }
3831        });
3832        Ok(())
3833    }
3834
3835    #[cfg(all(not(target_arch = "wasm32"), feature = "transport-moq"))]
3836    async fn handle_native_moq_iroh_carrier_control(
3837        &self,
3838        connection_id: &str,
3839        remote_node_id: Option<&str>,
3840        frame: &serde_json::Value,
3841    ) -> bool {
3842        let Some(bootstrap) =
3843            crate::iroh_carrier_bootstrap::CarrierBootstrapFrame::from_json(frame)
3844        else {
3845            if frame.get("type").and_then(serde_json::Value::as_str)
3846                == Some(crate::iroh_carrier_bootstrap::CARRIER_BOOTSTRAP_FRAME_TYPE)
3847            {
3848                eprintln!("[OpenRTC][MoQ carrier] rejected invalid bootstrap envelope={frame}");
3849            }
3850            return false;
3851        };
3852        if bootstrap.carrier != crate::iroh_carrier_bootstrap::CarrierBootstrapKind::MoqDraft14 {
3853            return false;
3854        }
3855        let Some(remote_node_id) = remote_node_id
3856            .map(str::trim)
3857            .filter(|value| !value.is_empty())
3858        else {
3859            return true;
3860        };
3861        let endpoint_id = match remote_node_id.parse::<iroh::EndpointId>() {
3862            Ok(value) => value,
3863            Err(_) => return true,
3864        };
3865        match bootstrap.action {
3866            crate::iroh_carrier_bootstrap::CarrierBootstrapAction::Request => {
3867                let local_node_id = match self.current_node_id().await {
3868                    Some(value) => value,
3869                    None => return true,
3870                };
3871                if local_node_id.as_str() >= remote_node_id || !self.is_moq_carrier_enabled().await
3872                {
3873                    return true;
3874                }
3875                let duplicate_attempt = self
3876                    .native_moq_carrier_attempts
3877                    .read()
3878                    .await
3879                    .get(connection_id)
3880                    .filter(|attempt| {
3881                        native_carrier_duplicate_request_is_live(
3882                            &bootstrap,
3883                            &attempt.bootstrap,
3884                            matches!(
3885                                attempt.session.state(),
3886                                crate::transport::NativeMoQState::Failed
3887                                    | crate::transport::NativeMoQState::Closed
3888                            ),
3889                        )
3890                    })
3891                    .cloned();
3892                if let Some(attempt) = duplicate_attempt {
3893                    if attempt.session.state() == crate::transport::NativeMoQState::Connected
3894                        && attempt.session.is_peer_data_subscribed().await
3895                    {
3896                        if let Ok(ready) =
3897                            crate::iroh_carrier_bootstrap::CarrierBootstrapFrame::ready_from(
3898                                &bootstrap,
3899                            )
3900                        {
3901                            let _ = self.send_iroh_carrier_control(connection_id, &ready).await;
3902                        }
3903                    }
3904                    eprintln!("[OpenRTC][MoQ carrier] responder reused connection_id={connection_id} reason=duplicate-live-bootstrap");
3905                    return true;
3906                }
3907                if !crate::iroh_connection_policy::custom_carrier_base_allows(
3908                    self.iroh_path_kind(remote_node_id).await,
3909                    IrohPathKind::Moq,
3910                    false,
3911                ) {
3912                    return true;
3913                }
3914                if self.get_connection(endpoint_id).await.is_none() {
3915                    return true;
3916                }
3917                let generation = match self
3918                    .current_native_peer_data_generation(connection_id, None)
3919                    .await
3920                {
3921                    Some(value) => value,
3922                    None => return true,
3923                };
3924                if self
3925                    .reject_native_iroh_carrier_request_if_base_blocked(
3926                        connection_id,
3927                        &bootstrap,
3928                        generation.transport_stable_id,
3929                        "MoQ",
3930                    )
3931                    .await
3932                {
3933                    return true;
3934                }
3935                let node = match self.iroh_node.read().await.as_ref().cloned() {
3936                    Some(value) => value,
3937                    None => return true,
3938                };
3939                let provider = match self
3940                    .native_transport_upgrade_provider(IrohPathKind::Moq)
3941                    .await
3942                {
3943                    Some(value) => value,
3944                    None => return true,
3945                };
3946                let gate_key = (connection_id.to_string(), IrohPathKind::Moq);
3947                {
3948                    let mut gates = self.native_transport_upgrade_gates.lock().await;
3949                    // Gateway/control replay is at-least-once. A matching gate
3950                    // means this exact physical attempt already owns creation;
3951                    // accepting it again would attach the same packet session
3952                    // twice and reject the valid attempt as DuplicateSession.
3953                    if gates.get(&gate_key).is_some_and(|current| {
3954                        current.generation == generation
3955                            && self.iroh_carrier_policy_epoch_is_current(current.policy_epoch)
3956                            && current.peer_policy_epoch
3957                                == self.current_iroh_carrier_peer_policy_epoch(connection_id)
3958                    }) {
3959                        return true;
3960                    }
3961                    if !reserve_native_transport_upgrade_gate(
3962                        &mut gates,
3963                        gate_key.clone(),
3964                        &bootstrap.upgrade_id,
3965                        generation,
3966                        self.current_iroh_carrier_policy_epoch(),
3967                        self.current_iroh_carrier_peer_policy_epoch(connection_id),
3968                        true,
3969                    ) {
3970                        return true;
3971                    }
3972                }
3973                let authorization_expiry = node
3974                    .authorize_inbound_replacement(
3975                        endpoint_id,
3976                        provider.transport_id(),
3977                        std::time::Duration::from_secs(45),
3978                    )
3979                    .await;
3980                let client = self.clone();
3981                let connection_id = connection_id.to_string();
3982                let remote_node_id = remote_node_id.to_string();
3983                tokio::spawn(async move {
3984                    let result = async {
3985                        client
3986                            .create_native_moq_carrier_attempt(
3987                                &connection_id,
3988                                &remote_node_id,
3989                                bootstrap.clone(),
3990                                generation,
3991                            )
3992                            .await?;
3993                        let ready =
3994                            crate::iroh_carrier_bootstrap::CarrierBootstrapFrame::ready_from(
3995                                &bootstrap,
3996                            )?;
3997                        client
3998                            .send_iroh_carrier_control(&connection_id, &ready)
3999                            .await?;
4000                        client
4001                            .wait_native_moq_carrier_ready(&connection_id, &bootstrap.upgrade_id)
4002                            .await?;
4003                        client
4004                            .complete_inbound_native_iroh_carrier_upgrade(
4005                                &connection_id,
4006                                &remote_node_id,
4007                                &bootstrap.upgrade_id,
4008                                generation,
4009                                bootstrap.base.transport_generation,
4010                                bootstrap.base.route_generation,
4011                                IrohPathKind::Moq,
4012                                bootstrap.carrier.transport_id(),
4013                                authorization_expiry,
4014                            )
4015                            .await
4016                    }
4017                    .await;
4018                    node.revoke_inbound_replacement_if_current(
4019                        endpoint_id,
4020                        provider.transport_id(),
4021                        authorization_expiry,
4022                    )
4023                    .await;
4024                    if let Err(error) = result {
4025                        let failure_code = native_iroh_carrier_failure_code(&error);
4026                        let error_detail = format!("{error:#}");
4027                        client.record_native_iroh_carrier_debug_event(
4028                            &connection_id,
4029                            &bootstrap.upgrade_id,
4030                            "completion-failed",
4031                            Some(&error_detail),
4032                        );
4033                        eprintln!(
4034                            "[OpenRTC][MoQ carrier] responder completion failed connection_id={} upgrade_id={} failure_code={} error={error:#}",
4035                            connection_id, bootstrap.upgrade_id, failure_code,
4036                        );
4037                        client
4038                            .fail_native_moq_carrier_attempt(
4039                                &connection_id,
4040                                &remote_node_id,
4041                                &bootstrap,
4042                                failure_code,
4043                                true,
4044                            )
4045                            .await;
4046                        client
4047                            .retry_stale_moq(
4048                                &connection_id,
4049                                &remote_node_id,
4050                                &bootstrap,
4051                                failure_code,
4052                            )
4053                            .await;
4054                    } else {
4055                        remove_native_transport_upgrade_gate(
4056                            &client.native_transport_upgrade_gates,
4057                            &(connection_id.clone(), IrohPathKind::Moq),
4058                            &bootstrap.upgrade_id,
4059                        )
4060                        .await;
4061                        if let Some(attempt) = client
4062                            .native_moq_carrier_attempts
4063                            .read()
4064                            .await
4065                            .get(&connection_id)
4066                            .cloned()
4067                        {
4068                            client.spawn_native_moq_carrier_terminal_watcher(
4069                                connection_id.clone(),
4070                                attempt,
4071                            );
4072                        }
4073                    }
4074                });
4075            }
4076            crate::iroh_carrier_bootstrap::CarrierBootstrapAction::Ready => {
4077                let attempt = self
4078                    .native_moq_carrier_attempts
4079                    .read()
4080                    .await
4081                    .get(connection_id)
4082                    .filter(|attempt| bootstrap.is_response_to(&attempt.bootstrap))
4083                    .cloned();
4084                let Some(attempt) = attempt else {
4085                    return true;
4086                };
4087                let client = self.clone();
4088                let connection_id = connection_id.to_string();
4089                let remote_node_id = remote_node_id.to_string();
4090                tokio::spawn(async move {
4091                    let result = async {
4092                        client
4093                            .wait_native_moq_carrier_ready(&connection_id, &bootstrap.upgrade_id)
4094                            .await?;
4095                        client
4096                            .complete_native_iroh_carrier_upgrade(
4097                                &connection_id,
4098                                &remote_node_id,
4099                                &bootstrap.upgrade_id,
4100                                attempt.generation,
4101                                IrohPathKind::Moq,
4102                            )
4103                            .await
4104                    }
4105                    .await;
4106                    if let Err(error) = result {
4107                        let failure_code = native_iroh_carrier_failure_code(&error);
4108                        let error_detail = format!("{error:#}");
4109                        client.record_native_iroh_carrier_debug_event(
4110                            &connection_id,
4111                            &bootstrap.upgrade_id,
4112                            "completion-failed",
4113                            Some(&error_detail),
4114                        );
4115                        eprintln!(
4116                            "[OpenRTC][MoQ carrier] initiator completion failed connection_id={} upgrade_id={} failure_code={} error={error:#}",
4117                            connection_id, bootstrap.upgrade_id, failure_code,
4118                        );
4119                        client
4120                            .fail_native_moq_carrier_attempt(
4121                                &connection_id,
4122                                &remote_node_id,
4123                                &attempt.bootstrap,
4124                                failure_code,
4125                                true,
4126                            )
4127                            .await;
4128                        client
4129                            .retry_stale_moq(
4130                                &connection_id,
4131                                &remote_node_id,
4132                                &attempt.bootstrap,
4133                                failure_code,
4134                            )
4135                            .await;
4136                    } else {
4137                        remove_native_transport_upgrade_gate(
4138                            &client.native_transport_upgrade_gates,
4139                            &(connection_id.clone(), IrohPathKind::Moq),
4140                            &bootstrap.upgrade_id,
4141                        )
4142                        .await;
4143                        if let Some(current) = client
4144                            .native_moq_carrier_attempts
4145                            .read()
4146                            .await
4147                            .get(&connection_id)
4148                            .cloned()
4149                        {
4150                            client.spawn_native_moq_carrier_terminal_watcher(
4151                                connection_id.clone(),
4152                                current,
4153                            );
4154                        }
4155                    }
4156                });
4157            }
4158            crate::iroh_carrier_bootstrap::CarrierBootstrapAction::Failed => {
4159                let request = self
4160                    .native_moq_carrier_attempts
4161                    .read()
4162                    .await
4163                    .get(connection_id)
4164                    .filter(|attempt| bootstrap.is_response_to(&attempt.bootstrap))
4165                    .map(|attempt| attempt.bootstrap.clone());
4166                if let Some(request) = request {
4167                    let (peer_failure_code, failure_code) =
4168                        native_moq_peer_failure_code(bootstrap.failure_code.as_deref());
4169                    self.record_native_iroh_carrier_debug_event(
4170                        connection_id,
4171                        &bootstrap.upgrade_id,
4172                        "peer-failed",
4173                        Some(peer_failure_code),
4174                    );
4175                    self.fail_native_moq_carrier_attempt(
4176                        connection_id,
4177                        remote_node_id,
4178                        &request,
4179                        failure_code,
4180                        false,
4181                    )
4182                    .await;
4183                    self.retry_stale_moq(connection_id, remote_node_id, &request, failure_code)
4184                        .await;
4185                }
4186            }
4187        }
4188        true
4189    }
4190
4191    #[cfg(not(target_arch = "wasm32"))]
4192    async fn wait_for_native_ble_switch_admission(
4193        &self,
4194        connection_id: &str,
4195        endpoint_id: iroh::EndpointId,
4196        expected_generation: crate::client::NativePeerDataGeneration,
4197    ) -> anyhow::Result<()> {
4198        let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(15);
4199        loop {
4200            let current_transport_stable_id = self
4201                .get_connection(endpoint_id)
4202                .await
4203                .map(|connection| crate::transport_generation::for_connection(&connection));
4204            if current_transport_stable_id != Some(expected_generation.transport_stable_id) {
4205                anyhow::bail!(
4206                    "BLE switch request lost its base transport generation: expected={} current={:?}",
4207                    expected_generation.transport_stable_id,
4208                    current_transport_stable_id,
4209                );
4210            }
4211            if self
4212                .current_native_peer_data_generation(
4213                    connection_id,
4214                    Some(expected_generation.transport_stable_id),
4215                )
4216                .await
4217                != Some(expected_generation)
4218            {
4219                anyhow::bail!(
4220                    "BLE switch request lost its logical generation: expected={:?}",
4221                    expected_generation,
4222                );
4223            }
4224
4225            let block = self.session_admission_block_reason_for_transport(
4226                connection_id,
4227                Some(expected_generation.transport_stable_id),
4228            );
4229            match classify_native_ble_switch_admission(block.as_ref()) {
4230                NativeBleSwitchAdmission::Ready => return Ok(()),
4231                NativeBleSwitchAdmission::Reject => {
4232                    anyhow::bail!(
4233                        "BLE switch request admission rejected: {}",
4234                        block
4235                            .as_ref()
4236                            .map(|(_, reason)| reason.as_str())
4237                            .unwrap_or("unknown"),
4238                    );
4239                }
4240                NativeBleSwitchAdmission::Wait => {}
4241            }
4242
4243            if tokio::time::Instant::now() >= deadline {
4244                anyhow::bail!(
4245                    "timed out waiting for BLE switch admission: {}",
4246                    block
4247                        .as_ref()
4248                        .map(|(_, reason)| reason.as_str())
4249                        .unwrap_or("unknown"),
4250                );
4251            }
4252            tokio::time::sleep(std::time::Duration::from_millis(50)).await;
4253        }
4254    }
4255
4256    #[cfg(not(target_arch = "wasm32"))]
4257    async fn reserve_native_ble_upgrade_attempt(
4258        &self,
4259        connection_id: &str,
4260        generation: crate::client::NativePeerDataGeneration,
4261    ) -> Option<u8> {
4262        let mut attempts = self.native_ble_upgrade_attempts.lock().await;
4263        let (state, attempt) =
4264            reserve_native_ble_attempt_state(attempts.get(connection_id).copied(), generation);
4265        attempts.insert(connection_id.to_string(), state);
4266        attempt
4267    }
4268
4269    #[cfg(not(target_arch = "wasm32"))]
4270    async fn current_native_ble_upgrade_attempt(
4271        &self,
4272        connection_id: &str,
4273        generation: crate::client::NativePeerDataGeneration,
4274    ) -> Option<u8> {
4275        self.native_ble_upgrade_attempts
4276            .lock()
4277            .await
4278            .get(connection_id)
4279            .filter(|state| state.generation == generation)
4280            .map(|state| state.attempts)
4281    }
4282
4283    #[cfg(not(target_arch = "wasm32"))]
4284    async fn clear_native_ble_upgrade_attempts(
4285        &self,
4286        connection_id: &str,
4287        generation: crate::client::NativePeerDataGeneration,
4288    ) {
4289        let mut attempts = self.native_ble_upgrade_attempts.lock().await;
4290        if attempts
4291            .get(connection_id)
4292            .is_some_and(|state| state.generation == generation)
4293        {
4294            attempts.remove(connection_id);
4295        }
4296    }
4297
4298    #[cfg(not(target_arch = "wasm32"))]
4299    pub(crate) async fn wake_native_ble_recovery(&self, reason: &'static str) -> usize {
4300        if !self.is_ble_transport_enabled().await {
4301            return 0;
4302        }
4303
4304        let active_gates = self
4305            .native_transport_upgrade_gates
4306            .lock()
4307            .await
4308            .keys()
4309            .filter(|(_, kind)| *kind == IrohPathKind::Ble)
4310            .map(|(connection_id, _)| connection_id.clone())
4311            .collect::<std::collections::HashSet<_>>();
4312        let reset_count = {
4313            let mut attempts = self.native_ble_upgrade_attempts.lock().await;
4314            let before = attempts.len();
4315            attempts.retain(|connection_id, state| {
4316                native_ble_attempt_state_after_recovery_wake(
4317                    *state,
4318                    active_gates.contains(connection_id),
4319                )
4320                .is_some()
4321            });
4322            before.saturating_sub(attempts.len())
4323        };
4324
4325        let ble_capable_connections = self
4326            .native_peer_transport_capabilities
4327            .read()
4328            .await
4329            .iter()
4330            .filter(|(_, capabilities)| capabilities.contains(&NativePeerTransportCapability::Ble))
4331            .map(|(connection_id, _)| connection_id.clone())
4332            .collect::<std::collections::HashSet<_>>();
4333        let mut eligible_records = 0usize;
4334        for record in self.connection_manager.list_all().await {
4335            if !ble_capable_connections.contains(&record.connection_id)
4336                || !matches!(
4337                    record.state,
4338                    crate::connection_manager::ConnectionState::Connecting
4339                        | crate::connection_manager::ConnectionState::Connected
4340                )
4341            {
4342                continue;
4343            }
4344            let Some(remote_node_id) = record
4345                .endpoint_id
4346                .as_deref()
4347                .or(record.node_id.as_deref())
4348                .map(str::trim)
4349                .filter(|value| !value.is_empty())
4350            else {
4351                continue;
4352            };
4353            eligible_records = eligible_records.saturating_add(1);
4354            if let Err(error) = self
4355                .maybe_start_native_ble_upgrade(&record.connection_id, Some(remote_node_id))
4356                .await
4357            {
4358                eprintln!(
4359                    "[OpenRTC][BLE] recovery wake failed connection_id={} remote_node_id={} reason={} error={:#}",
4360                    record.connection_id, remote_node_id, reason, error,
4361                );
4362            }
4363        }
4364
4365        eprintln!(
4366            "[OpenRTC][BLE] recovery wake reason={} reset_attempt_budgets={} eligible_records={}",
4367            reason, reset_count, eligible_records,
4368        );
4369        eligible_records
4370    }
4371
4372    #[cfg(not(target_arch = "wasm32"))]
4373    fn schedule_native_ble_upgrade_retry(
4374        &self,
4375        connection_id: String,
4376        remote_node_id: String,
4377        expected_generation: crate::client::NativePeerDataGeneration,
4378        failed_attempt: u8,
4379        reason: &'static str,
4380    ) {
4381        let Some(delay) =
4382            native_ble_retry_delay(failed_attempt, uuid::Uuid::new_v4().as_u128() as u64)
4383        else {
4384            eprintln!(
4385                "[OpenRTC][BLE] retry exhausted connection_id={} remote_node_id={} attempts={} reason={}",
4386                connection_id, remote_node_id, failed_attempt, reason,
4387            );
4388            return;
4389        };
4390        let client = self.clone();
4391        tokio::spawn(async move {
4392            tokio::time::sleep(delay).await;
4393            let attempt_is_current = client
4394                .current_native_ble_upgrade_attempt(&connection_id, expected_generation)
4395                .await
4396                == Some(failed_attempt);
4397            let generation_is_current = client
4398                .current_native_peer_data_generation(
4399                    &connection_id,
4400                    Some(expected_generation.transport_stable_id),
4401                )
4402                .await
4403                == Some(expected_generation);
4404            if !attempt_is_current || !generation_is_current {
4405                return;
4406            }
4407            eprintln!(
4408                "[OpenRTC][BLE] retrying upgrade connection_id={} remote_node_id={} next_attempt={} reason={}",
4409                connection_id,
4410                remote_node_id,
4411                failed_attempt.saturating_add(1),
4412                reason,
4413            );
4414            if let Err(error) = client
4415                .maybe_start_native_ble_upgrade(&connection_id, Some(&remote_node_id))
4416                .await
4417            {
4418                eprintln!(
4419                    "[OpenRTC][BLE] retry setup failed connection_id={} remote_node_id={} error={}",
4420                    connection_id, remote_node_id, error,
4421                );
4422            }
4423        });
4424    }
4425
4426    #[cfg(all(not(target_arch = "wasm32"), feature = "iroh-carrier-core"))]
4427    async fn complete_inbound_native_ble_upgrade(
4428        &self,
4429        connection_id: &str,
4430        remote_node_id: &str,
4431        upgrade_id: &str,
4432        expected_generation: crate::client::NativePeerDataGeneration,
4433        proof_generation: crate::client::NativePeerDataGeneration,
4434        replacement_transport_id: u64,
4435        authorization_expires_at: std::time::Instant,
4436    ) -> anyhow::Result<()> {
4437        self.complete_inbound_native_iroh_carrier_upgrade(
4438            connection_id,
4439            remote_node_id,
4440            upgrade_id,
4441            expected_generation,
4442            proof_generation.transport_generation,
4443            proof_generation.route_generation,
4444            IrohPathKind::Ble,
4445            replacement_transport_id,
4446            authorization_expires_at,
4447        )
4448        .await
4449    }
4450
4451    #[cfg(all(not(target_arch = "wasm32"), not(feature = "iroh-carrier-core")))]
4452    async fn complete_inbound_native_ble_upgrade(
4453        &self,
4454        _connection_id: &str,
4455        _remote_node_id: &str,
4456        _upgrade_id: &str,
4457        _expected_generation: crate::client::NativePeerDataGeneration,
4458        _proof_generation: crate::client::NativePeerDataGeneration,
4459        _replacement_transport_id: u64,
4460        _authorization_expires_at: std::time::Instant,
4461    ) -> anyhow::Result<()> {
4462        anyhow::bail!("BLE replacement requires the Iroh custom-carrier core")
4463    }
4464
4465    #[cfg(not(target_arch = "wasm32"))]
4466    async fn accept_native_ble_switch_request(
4467        &self,
4468        connection_id: &str,
4469        remote_node_id: &str,
4470        upgrade_id: &str,
4471        expected_generation: crate::client::NativePeerDataGeneration,
4472        proof_generation: crate::client::NativePeerDataGeneration,
4473    ) -> anyhow::Result<bool> {
4474        let Some(local_node_id) = self.current_node_id().await else {
4475            return Ok(false);
4476        };
4477        let current_path = self.iroh_path_kind(remote_node_id).await;
4478        if !should_accept_native_ble_switch_request(&local_node_id, remote_node_id, current_path) {
4479            return Ok(false);
4480        }
4481        let remote_endpoint_id = remote_node_id.parse::<iroh::EndpointId>()?;
4482        let provider = self
4483            .native_transport_upgrade_provider(IrohPathKind::Ble)
4484            .await
4485            .ok_or_else(|| anyhow::anyhow!("BLE upgrade provider is unavailable"))?;
4486        let replacement_transport_id = provider.transport_id();
4487        let native_node = self
4488            .iroh_node
4489            .read()
4490            .await
4491            .as_ref()
4492            .cloned()
4493            .ok_or_else(|| anyhow::anyhow!("Iroh node is unavailable"))?;
4494
4495        let gate_key = (connection_id.to_string(), IrohPathKind::Ble);
4496        {
4497            let mut gates = self.native_transport_upgrade_gates.lock().await;
4498            if !reserve_native_transport_upgrade_gate(
4499                &mut gates,
4500                gate_key.clone(),
4501                upgrade_id,
4502                expected_generation,
4503                self.current_iroh_carrier_policy_epoch(),
4504                self.current_iroh_carrier_peer_policy_epoch(connection_id),
4505                true,
4506            ) {
4507                return Ok(false);
4508            }
4509        }
4510        let authorization_expires_at = native_node
4511            .authorize_inbound_replacement(
4512                remote_endpoint_id,
4513                replacement_transport_id,
4514                std::time::Duration::from_secs(45),
4515            )
4516            .await;
4517
4518        if let Err(error) = self
4519            .send_native_ble_upgrade_control(
4520                connection_id,
4521                "switch-ready",
4522                upgrade_id,
4523                proof_generation,
4524            )
4525            .await
4526        {
4527            remove_native_transport_upgrade_gate(
4528                &self.native_transport_upgrade_gates,
4529                &gate_key,
4530                upgrade_id,
4531            )
4532            .await;
4533            native_node
4534                .revoke_inbound_replacement_if_current(
4535                    remote_endpoint_id,
4536                    replacement_transport_id,
4537                    authorization_expires_at,
4538                )
4539                .await;
4540            return Err(error);
4541        }
4542
4543        eprintln!(
4544            "[OpenRTC][BLE] switch request accepted connection_id={} remote_node_id={} upgrade_id={}",
4545            connection_id, remote_node_id, upgrade_id,
4546        );
4547        let client = self.clone();
4548        let responder_upgrade_id = upgrade_id.to_string();
4549        let responder_connection_id = connection_id.to_string();
4550        let responder_remote_node_id = remote_node_id.to_string();
4551        tokio::spawn(async move {
4552            let result = client
4553                .complete_inbound_native_ble_upgrade(
4554                    &responder_connection_id,
4555                    &responder_remote_node_id,
4556                    &responder_upgrade_id,
4557                    expected_generation,
4558                    proof_generation,
4559                    replacement_transport_id,
4560                    authorization_expires_at,
4561                )
4562                .await;
4563            native_node
4564                .revoke_inbound_replacement_if_current(
4565                    remote_endpoint_id,
4566                    replacement_transport_id,
4567                    authorization_expires_at,
4568                )
4569                .await;
4570            remove_native_transport_upgrade_gate(
4571                &client.native_transport_upgrade_gates,
4572                &gate_key,
4573                &responder_upgrade_id,
4574            )
4575            .await;
4576            if let Err(error) = result {
4577                eprintln!(
4578                    "[OpenRTC][BLE] inbound replacement completion failed connection_id={} remote_node_id={} upgrade_id={} error={:#}",
4579                    responder_connection_id,
4580                    responder_remote_node_id,
4581                    responder_upgrade_id,
4582                    error,
4583                );
4584            }
4585        });
4586        Ok(true)
4587    }
4588
4589    #[cfg(not(target_arch = "wasm32"))]
4590    pub(super) async fn maybe_start_native_ble_upgrade(
4591        &self,
4592        connection_id: &str,
4593        remote_node_id: Option<&str>,
4594    ) -> anyhow::Result<()> {
4595        let remote_node_id = remote_node_id
4596            .map(str::trim)
4597            .filter(|value| !value.is_empty())
4598            .ok_or_else(|| anyhow::anyhow!("BLE upgrade requires a remote node id"))?;
4599        let local_node_id = self
4600            .current_node_id()
4601            .await
4602            .ok_or_else(|| anyhow::anyhow!("BLE upgrade requires an initialized native node"))?;
4603        let current_path = self.iroh_path_kind(remote_node_id).await;
4604        if !should_initiate_native_ble_upgrade(local_node_id.as_str(), remote_node_id, current_path)
4605        {
4606            eprintln!(
4607                "[OpenRTC][BLE] upgrade decision connection_id={} local_node_id={} remote_node_id={} current_path={:?} role=responder-or-better-path",
4608                connection_id, local_node_id, remote_node_id, current_path,
4609            );
4610            return Ok(());
4611        }
4612        eprintln!(
4613            "[OpenRTC][BLE] upgrade decision connection_id={} local_node_id={} remote_node_id={} current_path={:?} role=initiator",
4614            connection_id, local_node_id, remote_node_id, current_path,
4615        );
4616
4617        let endpoint_id = remote_node_id.parse::<iroh::EndpointId>()?;
4618        let transport_stable_id = self
4619            .get_connection(endpoint_id)
4620            .await
4621            .map(|connection| crate::transport_generation::for_connection(&connection))
4622            .ok_or_else(|| anyhow::anyhow!("BLE upgrade requires a live base transport"))?;
4623        let expected_generation = self
4624            .current_native_peer_data_generation(connection_id, Some(transport_stable_id))
4625            .await
4626            .ok_or_else(|| anyhow::anyhow!("BLE upgrade requires a current peer generation"))?;
4627
4628        let gate_key = (connection_id.to_string(), IrohPathKind::Ble);
4629        let upgrade_id = uuid::Uuid::new_v4().to_string();
4630        {
4631            let mut gates = self.native_transport_upgrade_gates.lock().await;
4632            if !reserve_native_transport_upgrade_gate(
4633                &mut gates,
4634                gate_key.clone(),
4635                &upgrade_id,
4636                expected_generation,
4637                self.current_iroh_carrier_policy_epoch(),
4638                self.current_iroh_carrier_peer_policy_epoch(connection_id),
4639                false,
4640            ) {
4641                return Ok(());
4642            }
4643        }
4644        let Some(attempt) = self
4645            .reserve_native_ble_upgrade_attempt(connection_id, expected_generation)
4646            .await
4647        else {
4648            remove_native_transport_upgrade_gate(
4649                &self.native_transport_upgrade_gates,
4650                &gate_key,
4651                &upgrade_id,
4652            )
4653            .await;
4654            eprintln!(
4655                "[OpenRTC][BLE] upgrade skipped connection_id={} remote_node_id={} reason=attempts-exhausted",
4656                connection_id, remote_node_id,
4657            );
4658            return Ok(());
4659        };
4660
4661        let result = async {
4662            let provider = self
4663                .native_transport_upgrade_provider(IrohPathKind::Ble)
4664                .await
4665                .ok_or_else(|| anyhow::anyhow!("BLE upgrade provider is unavailable"))?;
4666            // Targeted discovery happens while the admitted Iroh control route
4667            // is still live. A failed scan leaves that base route untouched.
4668            let _ = provider.prepare_endpoint_addr(endpoint_id).await?;
4669            self.send_native_ble_upgrade_control(
4670                connection_id,
4671                "switch-request",
4672                &upgrade_id,
4673                expected_generation,
4674            )
4675            .await?;
4676            eprintln!(
4677                "[OpenRTC][BLE] prepared upgrade connection_id={} remote_node_id={} upgrade_id={}",
4678                connection_id, remote_node_id, upgrade_id
4679            );
4680            Ok::<(), anyhow::Error>(())
4681        }
4682        .await;
4683
4684        if result.is_err() {
4685            let removed = remove_native_transport_upgrade_gate(
4686                &self.native_transport_upgrade_gates,
4687                &gate_key,
4688                &upgrade_id,
4689            )
4690            .await;
4691            if removed {
4692                self.schedule_native_ble_upgrade_retry(
4693                    connection_id.to_string(),
4694                    remote_node_id.to_string(),
4695                    expected_generation,
4696                    attempt,
4697                    "setup-failed",
4698                );
4699            }
4700        } else {
4701            let client = self.clone();
4702            let gates = self.native_transport_upgrade_gates.clone();
4703            let connection_id = connection_id.to_string();
4704            let remote_node_id = remote_node_id.to_string();
4705            tokio::spawn(async move {
4706                tokio::time::sleep(NATIVE_BLE_SWITCH_RESPONSE_TIMEOUT).await;
4707                if remove_native_transport_upgrade_gate(&gates, &gate_key, &upgrade_id).await {
4708                    client.schedule_native_ble_upgrade_retry(
4709                        connection_id,
4710                        remote_node_id,
4711                        expected_generation,
4712                        attempt,
4713                        "switch-response-timeout",
4714                    );
4715                }
4716            });
4717        }
4718        result
4719    }
4720
4721    #[cfg(not(target_arch = "wasm32"))]
4722    async fn handle_native_ble_upgrade_control(
4723        &self,
4724        connection_id: &str,
4725        remote_node_id: Option<&str>,
4726        frame: &serde_json::Value,
4727    ) -> bool {
4728        if frame.get("type").and_then(|value| value.as_str()) != Some("transport-upgrade")
4729            || frame.get("transport").and_then(|value| value.as_str()) != Some("ble")
4730        {
4731            return false;
4732        }
4733
4734        let Some(remote_node_id) = remote_node_id
4735            .map(str::trim)
4736            .filter(|value| !value.is_empty())
4737        else {
4738            return true;
4739        };
4740        let action = frame
4741            .get("action")
4742            .and_then(|value| value.as_str())
4743            .unwrap_or_default();
4744        let Some(upgrade_id) = frame
4745            .get("upgradeId")
4746            .and_then(|value| value.as_str())
4747            .map(str::trim)
4748            .filter(|value| !value.is_empty())
4749            .filter(|value| {
4750                (8..=96).contains(&value.len())
4751                    && value
4752                        .bytes()
4753                        .all(|byte| byte.is_ascii_alphanumeric() || byte == b'-')
4754            })
4755        else {
4756            eprintln!("[OpenRTC][BLE] ignored control frame with invalid upgrade id");
4757            return true;
4758        };
4759        let Some(proof_generation) = frame
4760            .get("base")
4761            .cloned()
4762            .and_then(|value| serde_json::from_value(value).ok())
4763            .filter(|generation: &crate::client::NativePeerDataGeneration| {
4764                generation.transport_stable_id != 0 && generation.transport_generation != 0
4765            })
4766        else {
4767            eprintln!(
4768                "[OpenRTC][BLE] ignored control frame without a valid base-generation fence upgrade_id={}",
4769                upgrade_id,
4770            );
4771            return true;
4772        };
4773
4774        if !self.is_ble_transport_enabled().await {
4775            return true;
4776        }
4777
4778        let Some(local_node_id) = self.current_node_id().await else {
4779            return true;
4780        };
4781        let current_path = self.iroh_path_kind(remote_node_id).await;
4782        eprintln!(
4783            "[OpenRTC][BLE] control received connection_id={} remote_node_id={} action={} upgrade_id={} proof_base={:?} current_path={:?}",
4784            connection_id, remote_node_id, action, upgrade_id, proof_generation, current_path,
4785        );
4786
4787        match action {
4788            "switch-request" => {
4789                if !should_accept_native_ble_switch_request(
4790                    &local_node_id,
4791                    remote_node_id,
4792                    current_path,
4793                ) {
4794                    return true;
4795                }
4796                let Ok(endpoint_id) = remote_node_id.parse::<iroh::EndpointId>() else {
4797                    return true;
4798                };
4799                let Some(transport_stable_id) = self
4800                    .get_connection(endpoint_id)
4801                    .await
4802                    .map(|connection| crate::transport_generation::for_connection(&connection))
4803                else {
4804                    return true;
4805                };
4806                let Some(expected_generation) = self
4807                    .current_native_peer_data_generation(connection_id, Some(transport_stable_id))
4808                    .await
4809                else {
4810                    return true;
4811                };
4812                let admission_block = self.session_admission_block_reason_for_transport(
4813                    connection_id,
4814                    Some(transport_stable_id),
4815                );
4816                match classify_native_ble_switch_admission(admission_block.as_ref()) {
4817                    NativeBleSwitchAdmission::Reject => {
4818                        eprintln!(
4819                            "[OpenRTC][BLE] switch request rejected connection_id={} remote_node_id={} upgrade_id={} reason={}",
4820                            connection_id,
4821                            remote_node_id,
4822                            upgrade_id,
4823                            admission_block
4824                                .as_ref()
4825                                .map(|(_, reason)| reason.as_str())
4826                                .unwrap_or("unknown"),
4827                        );
4828                        return true;
4829                    }
4830                    NativeBleSwitchAdmission::Wait => {
4831                        let gate_key = (connection_id.to_string(), IrohPathKind::Ble);
4832                        {
4833                            let mut gates = self.native_transport_upgrade_gates.lock().await;
4834                            if !reserve_native_transport_upgrade_gate(
4835                                &mut gates,
4836                                gate_key.clone(),
4837                                upgrade_id,
4838                                expected_generation,
4839                                self.current_iroh_carrier_policy_epoch(),
4840                                self.current_iroh_carrier_peer_policy_epoch(connection_id),
4841                                false,
4842                            ) {
4843                                return true;
4844                            }
4845                        }
4846                        eprintln!(
4847                            "[OpenRTC][BLE] switch request deferred connection_id={} remote_node_id={} upgrade_id={} transport_stable_id={} reason={}",
4848                            connection_id,
4849                            remote_node_id,
4850                            upgrade_id,
4851                            transport_stable_id,
4852                            admission_block
4853                                .as_ref()
4854                                .map(|(_, reason)| reason.as_str())
4855                                .unwrap_or("unknown"),
4856                        );
4857                        let client = self.clone();
4858                        let connection_id = connection_id.to_string();
4859                        let remote_node_id = remote_node_id.to_string();
4860                        let upgrade_id = upgrade_id.to_string();
4861                        tokio::spawn(async move {
4862                            let result = client
4863                                .wait_for_native_ble_switch_admission(
4864                                    &connection_id,
4865                                    endpoint_id,
4866                                    expected_generation,
4867                                )
4868                                .await;
4869                            if let Err(error) = result {
4870                                remove_native_transport_upgrade_gate(
4871                                    &client.native_transport_upgrade_gates,
4872                                    &(connection_id.clone(), IrohPathKind::Ble),
4873                                    &upgrade_id,
4874                                )
4875                                .await;
4876                                eprintln!(
4877                                    "[OpenRTC][BLE] deferred switch request failed connection_id={} remote_node_id={} upgrade_id={} error={}",
4878                                    connection_id, remote_node_id, upgrade_id, error,
4879                                );
4880                                return;
4881                            }
4882                            if let Err(error) = client
4883                                .accept_native_ble_switch_request(
4884                                    &connection_id,
4885                                    &remote_node_id,
4886                                    &upgrade_id,
4887                                    expected_generation,
4888                                    proof_generation,
4889                                )
4890                                .await
4891                            {
4892                                eprintln!(
4893                                    "[OpenRTC][BLE] deferred switch request failed connection_id={} remote_node_id={} upgrade_id={} error={}",
4894                                    connection_id, remote_node_id, upgrade_id, error,
4895                                );
4896                            }
4897                        });
4898                        return true;
4899                    }
4900                    NativeBleSwitchAdmission::Ready => {}
4901                }
4902                if let Err(error) = self
4903                    .accept_native_ble_switch_request(
4904                        connection_id,
4905                        remote_node_id,
4906                        upgrade_id,
4907                        expected_generation,
4908                        proof_generation,
4909                    )
4910                    .await
4911                {
4912                    eprintln!(
4913                        "[OpenRTC][BLE] switch request failed connection_id={} remote_node_id={} upgrade_id={} error={}",
4914                        connection_id, remote_node_id, upgrade_id, error,
4915                    );
4916                }
4917            }
4918            "switch-ready" => {
4919                let gate_key = (connection_id.to_string(), IrohPathKind::Ble);
4920                let completion_gate_id = format!("{NATIVE_BLE_COMPLETION_GATE_PREFIX}{upgrade_id}");
4921                let expected_generation = {
4922                    let mut gates = self.native_transport_upgrade_gates.lock().await;
4923                    gates.get_mut(&gate_key).and_then(|current| {
4924                        transition_native_ble_upgrade_gate_to_completion(
4925                            current,
4926                            upgrade_id,
4927                            proof_generation,
4928                        )
4929                    })
4930                };
4931                let Some(expected_generation) = expected_generation else {
4932                    eprintln!(
4933                        "[OpenRTC][BLE] switch ready ignored connection_id={} remote_node_id={} upgrade_id={} reason=generation-mismatch",
4934                        connection_id, remote_node_id, upgrade_id,
4935                    );
4936                    return true;
4937                };
4938                eprintln!(
4939                    "[OpenRTC][BLE] switch ready accepted connection_id={} remote_node_id={} upgrade_id={}",
4940                    connection_id, remote_node_id, upgrade_id,
4941                );
4942                let Ok(endpoint_id) = remote_node_id.parse::<iroh::EndpointId>() else {
4943                    return true;
4944                };
4945                let client = self.clone();
4946                let connection_id = connection_id.to_string();
4947                let remote = remote_node_id.to_string();
4948                let upgrade_id = upgrade_id.to_string();
4949                let attempt = self
4950                    .current_native_ble_upgrade_attempt(connection_id.as_str(), expected_generation)
4951                    .await;
4952                tokio::spawn(async move {
4953                    let gate_key = (connection_id.clone(), IrohPathKind::Ble);
4954                    if let Err(error) = client
4955                        .wait_for_native_ble_switch_admission(
4956                            &connection_id,
4957                            endpoint_id,
4958                            expected_generation,
4959                        )
4960                        .await
4961                    {
4962                        let removed = remove_native_transport_upgrade_gate(
4963                            &client.native_transport_upgrade_gates,
4964                            &gate_key,
4965                            &completion_gate_id,
4966                        )
4967                        .await;
4968                        if removed {
4969                            if let Some(attempt) = attempt {
4970                                client.schedule_native_ble_upgrade_retry(
4971                                    connection_id.clone(),
4972                                    remote.clone(),
4973                                    expected_generation,
4974                                    attempt,
4975                                    "admission-wait-failed",
4976                                );
4977                            }
4978                        }
4979                        eprintln!(
4980                            "[OpenRTC][BLE] switch ready admission failed connection_id={} remote_node_id={} upgrade_id={} error={}",
4981                            connection_id, remote, upgrade_id, error,
4982                        );
4983                        return;
4984                    }
4985                    let result = client
4986                        .complete_native_ble_upgrade(
4987                            &connection_id,
4988                            &remote,
4989                            &upgrade_id,
4990                            expected_generation,
4991                        )
4992                        .await;
4993                    let removed = remove_native_transport_upgrade_gate(
4994                        &client.native_transport_upgrade_gates,
4995                        &gate_key,
4996                        &completion_gate_id,
4997                    )
4998                    .await;
4999                    match result {
5000                        Ok(()) => {
5001                            client
5002                                .clear_native_ble_upgrade_attempts(
5003                                    &connection_id,
5004                                    expected_generation,
5005                                )
5006                                .await;
5007                        }
5008                        Err(error) => {
5009                            if removed {
5010                                if let Some(attempt) = attempt {
5011                                    client.schedule_native_ble_upgrade_retry(
5012                                        connection_id.clone(),
5013                                        remote.clone(),
5014                                        expected_generation,
5015                                        attempt,
5016                                        "replacement-failed",
5017                                    );
5018                                }
5019                            }
5020                            eprintln!(
5021                                "[OpenRTC][BLE] upgrade failed connection_id={} remote_node_id={} upgrade_id={} error={}",
5022                                connection_id, remote, upgrade_id, error
5023                            );
5024                        }
5025                    }
5026                });
5027            }
5028            _ => {}
5029        }
5030        true
5031    }
5032
5033    #[cfg(all(not(target_arch = "wasm32"), feature = "iroh-carrier-core"))]
5034    pub(crate) async fn receive_native_iroh_carrier_candidate_proof(
5035        &self,
5036        connection_id: &str,
5037        send: iroh::endpoint::SendStream,
5038        recv: iroh::endpoint::RecvStream,
5039        upgrade_id: &str,
5040        proof_base_transport_generation: u64,
5041        proof_base_route_generation: u64,
5042        kind: IrohPathKind,
5043        transport_id: u64,
5044    ) -> anyhow::Result<(
5045        crate::application_crypto_streams::PeerSendStream,
5046        crate::iroh_carrier_proof::CarrierCandidateProofFrame,
5047        crate::iroh_carrier_proof::CarrierCandidateProofFrame,
5048    )> {
5049        let (send, mut recv) =
5050            self.wrap_peer_streams_for_connection(Some(connection_id), send, recv)?;
5051        let payload = crate::iroh_carrier_proof::read_candidate_proof_stream(&mut recv).await?;
5052        let probe: crate::iroh_carrier_proof::CarrierCandidateProofFrame =
5053            serde_json::from_slice(&payload)?;
5054        let expected_carrier = match kind {
5055            IrohPathKind::Ble => crate::iroh_carrier_bootstrap::CarrierBootstrapKind::Ble,
5056            IrohPathKind::WebRtc => crate::iroh_carrier_bootstrap::CarrierBootstrapKind::WebRtc,
5057            IrohPathKind::Moq => crate::iroh_carrier_bootstrap::CarrierBootstrapKind::MoqDraft14,
5058            _ => anyhow::bail!("unsupported inbound Iroh carrier proof kind {kind:?}"),
5059        };
5060        anyhow::ensure!(
5061            probe.frame_type == crate::iroh_carrier_proof::CARRIER_CANDIDATE_PROOF_TYPE
5062                && probe.role == crate::iroh_carrier_proof::CarrierCandidateProofRole::Probe
5063                && probe.carrier == expected_carrier
5064                && probe.transport_id == transport_id
5065                && probe.upgrade_id == upgrade_id
5066                && probe.base_transport_generation == proof_base_transport_generation
5067                && probe.base_route_generation == proof_base_route_generation,
5068            "carrier candidate proof does not match the authorized native attempt: expected_carrier={:?} actual_carrier={:?} expected_transport_id={} actual_transport_id={} expected_upgrade_id={} actual_upgrade_id={} expected_base_transport_generation={} actual_base_transport_generation={} expected_base_route_generation={} actual_base_route_generation={}",
5069            expected_carrier,
5070            probe.carrier,
5071            transport_id,
5072            probe.transport_id,
5073            upgrade_id,
5074            probe.upgrade_id,
5075            proof_base_transport_generation,
5076            probe.base_transport_generation,
5077            proof_base_route_generation,
5078            probe.base_route_generation,
5079        );
5080        probe.validate()?;
5081        let ack = crate::iroh_carrier_proof::CarrierCandidateProofFrame::ack_from(&probe)?;
5082        Ok((send, probe, ack))
5083    }
5084
5085    #[cfg(all(not(target_arch = "wasm32"), feature = "iroh-carrier-core"))]
5086    pub(crate) async fn send_native_iroh_carrier_candidate_ack(
5087        &self,
5088        mut send: crate::application_crypto_streams::PeerSendStream,
5089        ack: &crate::iroh_carrier_proof::CarrierCandidateProofFrame,
5090    ) -> anyhow::Result<()> {
5091        send.write_all(&serde_json::to_vec(&ack)?).await?;
5092        send.finish_and_wait_for_peer(
5093            crate::iroh_carrier_proof::CARRIER_CANDIDATE_PROOF_DELIVERY_TIMEOUT,
5094        )
5095        .await?;
5096        Ok(())
5097    }
5098
5099    #[cfg(all(not(target_arch = "wasm32"), feature = "iroh-carrier-core"))]
5100    async fn receive_native_iroh_carrier_commit(
5101        &self,
5102        connection_id: &str,
5103        connection: &iroh::endpoint::Connection,
5104        probe: &crate::iroh_carrier_proof::CarrierCandidateProofFrame,
5105    ) -> anyhow::Result<(
5106        crate::application_crypto_streams::PeerSendStream,
5107        crate::iroh_carrier_proof::CarrierCandidateProofFrame,
5108    )> {
5109        let (send, recv) = connection.accept_bi().await?;
5110        let (send, mut recv) =
5111            self.wrap_peer_streams_for_connection(Some(connection_id), send, recv)?;
5112        let payload = crate::iroh_carrier_proof::read_candidate_proof_stream(&mut recv).await?;
5113        let commit: crate::iroh_carrier_proof::CarrierCandidateProofFrame =
5114            serde_json::from_slice(&payload)?;
5115        anyhow::ensure!(
5116            commit.matches_commit_for_probe(probe),
5117            "carrier commit does not match the proven candidate"
5118        );
5119        let committed =
5120            crate::iroh_carrier_proof::CarrierCandidateProofFrame::committed_from(&commit)?;
5121        Ok((send, committed))
5122    }
5123
5124    #[cfg(all(not(target_arch = "wasm32"), feature = "iroh-carrier-core"))]
5125    async fn confirm_outbound_native_iroh_carrier_commit(
5126        &self,
5127        connection_id: &str,
5128        connection: &iroh::endpoint::Connection,
5129        probe: &crate::iroh_carrier_proof::CarrierCandidateProofFrame,
5130    ) -> anyhow::Result<()> {
5131        let commit = crate::iroh_carrier_proof::CarrierCandidateProofFrame::commit_from(probe)?;
5132        let (send, recv) = connection.open_bi().await?;
5133        let (mut send, mut recv) =
5134            self.wrap_peer_streams_for_connection(Some(connection_id), send, recv)?;
5135        send.write_all(&serde_json::to_vec(&commit)?).await?;
5136        send.finish()?;
5137        let payload = crate::iroh_carrier_proof::read_candidate_proof_stream(&mut recv).await?;
5138        let committed: crate::iroh_carrier_proof::CarrierCandidateProofFrame =
5139            serde_json::from_slice(&payload)?;
5140        anyhow::ensure!(
5141            committed.matches_committed_for_commit(&commit),
5142            "carrier commit acknowledgement does not match the current candidate"
5143        );
5144        Ok(())
5145    }
5146
5147    #[cfg(all(not(target_arch = "wasm32"), feature = "iroh-carrier-core"))]
5148    async fn prove_outbound_native_iroh_carrier_candidate(
5149        &self,
5150        connection_id: &str,
5151        connection: &iroh::endpoint::Connection,
5152        upgrade_id: &str,
5153        expected_generation: crate::client::NativePeerDataGeneration,
5154        kind: IrohPathKind,
5155        transport_id: u64,
5156    ) -> anyhow::Result<crate::iroh_carrier_proof::CarrierCandidateProofFrame> {
5157        anyhow::ensure!(
5158            self.application_crypto_key_for_connection(Some(connection_id))
5159                .is_some(),
5160            "carrier candidate proof requires an installed application-crypto key"
5161        );
5162        let carrier = match kind {
5163            IrohPathKind::Ble => crate::iroh_carrier_bootstrap::CarrierBootstrapKind::Ble,
5164            IrohPathKind::WebRtc => crate::iroh_carrier_bootstrap::CarrierBootstrapKind::WebRtc,
5165            IrohPathKind::Moq => crate::iroh_carrier_bootstrap::CarrierBootstrapKind::MoqDraft14,
5166            _ => anyhow::bail!("unsupported outbound Iroh carrier proof kind {kind:?}"),
5167        };
5168        let probe = crate::iroh_carrier_proof::CarrierCandidateProofFrame::probe(
5169            carrier,
5170            transport_id,
5171            upgrade_id,
5172            expected_generation.transport_generation,
5173            expected_generation.route_generation,
5174        )?;
5175        let (send, recv) = connection.open_bi().await?;
5176        let (mut send, mut recv) =
5177            self.wrap_peer_streams_for_connection(Some(connection_id), send, recv)?;
5178        send.write_all(&serde_json::to_vec(&probe)?).await?;
5179        send.finish()?;
5180        let payload = crate::iroh_carrier_proof::read_candidate_proof_stream(&mut recv).await?;
5181        let ack: crate::iroh_carrier_proof::CarrierCandidateProofFrame =
5182            serde_json::from_slice(&payload)?;
5183        anyhow::ensure!(
5184            ack.matches_probe(&probe),
5185            "carrier candidate proof acknowledgement did not match the current probe"
5186        );
5187        Ok(probe)
5188    }
5189
5190    #[cfg(all(not(target_arch = "wasm32"), feature = "iroh-carrier-core"))]
5191    async fn complete_native_iroh_carrier_upgrade(
5192        &self,
5193        connection_id: &str,
5194        remote_node_id: &str,
5195        upgrade_id: &str,
5196        expected_generation: crate::client::NativePeerDataGeneration,
5197        kind: IrohPathKind,
5198    ) -> anyhow::Result<()> {
5199        anyhow::ensure!(
5200            matches!(
5201                kind,
5202                IrohPathKind::Ble | IrohPathKind::WebRtc | IrohPathKind::Moq
5203            ),
5204            "unsupported Iroh packet carrier: {kind:?}"
5205        );
5206        let endpoint_id = remote_node_id.parse::<iroh::EndpointId>()?;
5207        self.record_native_iroh_carrier_debug_event(
5208            connection_id,
5209            upgrade_id,
5210            "candidate-outbound-provider-start",
5211            Some(kind.transport_label()),
5212        );
5213        let provider = self
5214            .native_transport_upgrade_provider(kind)
5215            .await
5216            .ok_or_else(|| anyhow::anyhow!("{kind:?} carrier provider is unavailable"))?;
5217        self.record_native_iroh_carrier_debug_event(
5218            connection_id,
5219            upgrade_id,
5220            "candidate-outbound-provider-ready",
5221            Some(kind.transport_label()),
5222        );
5223        let endpoint_addr = provider.prepare_endpoint_addr(endpoint_id).await?;
5224        self.record_native_iroh_carrier_debug_event(
5225            connection_id,
5226            upgrade_id,
5227            "candidate-outbound-address-ready",
5228            Some(kind.transport_label()),
5229        );
5230        let gate_key = (connection_id.to_string(), kind);
5231        let policy_epoch = self.current_iroh_carrier_policy_epoch();
5232        let peer_policy_epoch = self.current_iroh_carrier_peer_policy_epoch(connection_id);
5233        anyhow::ensure!(
5234            self.native_transport_upgrade_gates
5235                .lock()
5236                .await
5237                .get(&gate_key)
5238                .is_some_and(|gate| {
5239                    native_transport_upgrade_gate_matches(gate, upgrade_id)
5240                        && gate.policy_epoch == policy_epoch
5241                        && gate.peer_policy_epoch == peer_policy_epoch
5242                }),
5243            "{kind:?} replacement belongs to a retired upgrade"
5244        );
5245        let node = self
5246            .iroh_node
5247            .read()
5248            .await
5249            .as_ref()
5250            .cloned()
5251            .ok_or_else(|| anyhow::anyhow!("Iroh node is unavailable"))?;
5252        self.record_native_iroh_carrier_debug_event(
5253            connection_id,
5254            upgrade_id,
5255            "candidate-outbound-dial-start",
5256            Some(kind.transport_label()),
5257        );
5258        let candidate = node
5259            .dial_replacement_candidate(endpoint_id, endpoint_addr, provider.transport_id())
5260            .await?;
5261        self.record_native_iroh_carrier_debug_event(
5262            connection_id,
5263            upgrade_id,
5264            "candidate-outbound-dial-ready",
5265            Some(kind.transport_label()),
5266        );
5267        let authorization_fence = self.capture_native_iroh_carrier_authorization_fence(
5268            connection_id,
5269            expected_generation,
5270            kind,
5271            policy_epoch,
5272        )?;
5273        self.record_native_iroh_carrier_debug_event(
5274            connection_id,
5275            upgrade_id,
5276            "candidate-outbound-proof-start",
5277            Some(kind.transport_label()),
5278        );
5279        let probe = self
5280            .prove_outbound_native_iroh_carrier_candidate(
5281                connection_id,
5282                candidate.connection(),
5283                upgrade_id,
5284                expected_generation,
5285                kind,
5286                provider.transport_id(),
5287            )
5288            .await?;
5289        self.record_native_iroh_carrier_debug_event(
5290            connection_id,
5291            upgrade_id,
5292            "candidate-outbound-proof-ready",
5293            Some(kind.transport_label()),
5294        );
5295        // Ask the responder to commit first. If its incumbent changed while
5296        // the carrier was being prepared, it rejects the commit while this
5297        // side still owns its healthy base route instead of leaving one peer
5298        // stranded on the custom carrier.
5299        self.confirm_outbound_native_iroh_carrier_commit(
5300            connection_id,
5301            candidate.connection(),
5302            &probe,
5303        )
5304        .await?;
5305        self.record_native_iroh_carrier_debug_event(
5306            connection_id,
5307            upgrade_id,
5308            "candidate-outbound-commit-confirmed",
5309            Some(kind.transport_label()),
5310        );
5311        let committed_replacement = self
5312            .commit_proven_native_iroh_carrier_candidate(
5313                connection_id,
5314                remote_node_id,
5315                upgrade_id,
5316                expected_generation,
5317                kind,
5318                authorization_fence,
5319                node,
5320                candidate,
5321            )
5322            .await?;
5323        self.record_native_iroh_carrier_debug_event(
5324            connection_id,
5325            upgrade_id,
5326            "candidate-outbound-installed",
5327            Some(kind.transport_label()),
5328        );
5329        let replacement_transport_stable_id = committed_replacement
5330            .logical_result()
5331            .transport_stable_id
5332            .ok_or_else(|| anyhow::anyhow!("{kind:?} replacement has no stable ID"))?;
5333        committed_replacement
5334            .finish(crate::lifecycle_reason::REASON_NATIVE_CUSTOM_TRANSPORT_UPGRADE.as_bytes());
5335        self.settle_committed_native_iroh_carrier_application_route(
5336            connection_id,
5337            upgrade_id,
5338            kind,
5339            replacement_transport_stable_id,
5340        )
5341        .await;
5342        Ok(())
5343    }
5344
5345    #[cfg(all(not(target_arch = "wasm32"), feature = "iroh-carrier-core"))]
5346    async fn complete_inbound_native_iroh_carrier_upgrade(
5347        &self,
5348        connection_id: &str,
5349        remote_node_id: &str,
5350        upgrade_id: &str,
5351        expected_generation: crate::client::NativePeerDataGeneration,
5352        proof_base_transport_generation: u64,
5353        proof_base_route_generation: u64,
5354        kind: IrohPathKind,
5355        transport_id: u64,
5356        authorization_expires_at: std::time::Instant,
5357    ) -> anyhow::Result<()> {
5358        let endpoint_id = remote_node_id.parse::<iroh::EndpointId>()?;
5359        let policy_epoch = self.current_iroh_carrier_policy_epoch();
5360        let peer_policy_epoch = self.current_iroh_carrier_peer_policy_epoch(connection_id);
5361        anyhow::ensure!(
5362            self.native_transport_upgrade_gates
5363                .lock()
5364                .await
5365                .get(&(connection_id.to_string(), kind))
5366                .is_some_and(|gate| {
5367                    native_transport_upgrade_gate_matches(gate, upgrade_id)
5368                        && gate.policy_epoch == policy_epoch
5369                        && gate.peer_policy_epoch == peer_policy_epoch
5370                }),
5371            "{kind:?} inbound replacement belongs to a retired upgrade"
5372        );
5373        let node = self
5374            .iroh_node
5375            .read()
5376            .await
5377            .as_ref()
5378            .cloned()
5379            .ok_or_else(|| anyhow::anyhow!("Iroh node is unavailable"))?;
5380        self.record_native_iroh_carrier_debug_event(
5381            connection_id,
5382            upgrade_id,
5383            "candidate-inbound-wait-start",
5384            Some(kind.transport_label()),
5385        );
5386        let candidate = node
5387            .wait_for_inbound_replacement_candidate(
5388                endpoint_id,
5389                transport_id,
5390                authorization_expires_at,
5391                std::time::Duration::from_secs(40),
5392            )
5393            .await?;
5394        self.record_native_iroh_carrier_debug_event(
5395            connection_id,
5396            upgrade_id,
5397            "candidate-inbound-arrived",
5398            Some(kind.transport_label()),
5399        );
5400        let authorization_fence = self.capture_native_iroh_carrier_authorization_fence(
5401            connection_id,
5402            expected_generation,
5403            kind,
5404            policy_epoch,
5405        )?;
5406        let (send, recv) = candidate.connection().accept_bi().await?;
5407        self.record_native_iroh_carrier_debug_event(
5408            connection_id,
5409            upgrade_id,
5410            "candidate-inbound-proof-start",
5411            Some(kind.transport_label()),
5412        );
5413        let (send, probe, ack) = self
5414            .receive_native_iroh_carrier_candidate_proof(
5415                connection_id,
5416                send,
5417                recv,
5418                upgrade_id,
5419                proof_base_transport_generation,
5420                proof_base_route_generation,
5421                kind,
5422                transport_id,
5423            )
5424            .await?;
5425        self.record_native_iroh_carrier_debug_event(
5426            connection_id,
5427            upgrade_id,
5428            "candidate-inbound-proof-ready",
5429            Some(kind.transport_label()),
5430        );
5431        anyhow::ensure!(
5432            self.native_iroh_carrier_pre_ack_authority_is_current(
5433                connection_id,
5434                kind,
5435                upgrade_id,
5436                &authorization_fence,
5437            )
5438            .await,
5439            "{kind:?} inbound carrier authority changed before candidate acknowledgement"
5440        );
5441        self.send_native_iroh_carrier_candidate_ack(send, &ack)
5442            .await?;
5443        self.record_native_iroh_carrier_debug_event(
5444            connection_id,
5445            upgrade_id,
5446            "candidate-inbound-proof-acknowledged",
5447            Some(kind.transport_label()),
5448        );
5449        let (commit_send, committed) = self
5450            .receive_native_iroh_carrier_commit(connection_id, candidate.connection(), &probe)
5451            .await?;
5452        self.record_native_iroh_carrier_debug_event(
5453            connection_id,
5454            upgrade_id,
5455            "candidate-inbound-commit-received",
5456            Some(kind.transport_label()),
5457        );
5458        let committed_replacement = self
5459            .commit_proven_native_iroh_carrier_candidate(
5460                connection_id,
5461                remote_node_id,
5462                upgrade_id,
5463                expected_generation,
5464                kind,
5465                authorization_fence,
5466                node,
5467                candidate,
5468            )
5469            .await?;
5470        self.record_native_iroh_carrier_debug_event(
5471            connection_id,
5472            upgrade_id,
5473            "candidate-inbound-installed",
5474            Some(kind.transport_label()),
5475        );
5476        self.send_native_iroh_carrier_candidate_ack(commit_send, &committed)
5477            .await?;
5478        let replacement_transport_stable_id = committed_replacement
5479            .logical_result()
5480            .transport_stable_id
5481            .ok_or_else(|| anyhow::anyhow!("{kind:?} replacement has no stable ID"))?;
5482        committed_replacement
5483            .finish(crate::lifecycle_reason::REASON_NATIVE_CUSTOM_TRANSPORT_UPGRADE.as_bytes());
5484        self.settle_committed_native_iroh_carrier_application_route(
5485            connection_id,
5486            upgrade_id,
5487            kind,
5488            replacement_transport_stable_id,
5489        )
5490        .await;
5491        Ok(())
5492    }
5493
5494    /// Re-run the one application-key owner only after both peers have
5495    /// committed the physical carrier and the displaced incumbent has been
5496    /// retired. The carrier challenge proves possession of the existing key;
5497    /// it does not replace the stable-ID-bound capability/update/response/ACK
5498    /// transcript required for product traffic.
5499    #[cfg(all(not(target_arch = "wasm32"), feature = "iroh-carrier-core"))]
5500    async fn settle_committed_native_iroh_carrier_application_route(
5501        &self,
5502        connection_id: &str,
5503        upgrade_id: &str,
5504        kind: IrohPathKind,
5505        replacement_transport_stable_id: u64,
5506    ) {
5507        self.record_native_iroh_carrier_debug_event(
5508            connection_id,
5509            upgrade_id,
5510            "application-crypto-confirmation-start",
5511            Some(kind.transport_label()),
5512        );
5513        let result = async {
5514            self.ensure_connection_application_crypto(connection_id, 20_000)
5515                .await?;
5516            anyhow::ensure!(
5517                self.connection_application_crypto_is_confirmed(
5518                    connection_id,
5519                    Some(replacement_transport_stable_id),
5520                ),
5521                "{kind:?} replacement key transcript did not confirm the committed generation"
5522            );
5523            anyhow::ensure!(
5524                self.native_application_stream_admitted_for_transport(
5525                    connection_id,
5526                    replacement_transport_stable_id,
5527                ),
5528                "{kind:?} replacement key transcript did not settle admission: {}",
5529                self.native_application_stream_pending_diagnostic(
5530                    connection_id,
5531                    replacement_transport_stable_id,
5532                ),
5533            );
5534            anyhow::ensure!(
5535                self.confirm_managed_connection_readiness_from_transport_proof(
5536                    connection_id,
5537                    replacement_transport_stable_id,
5538                )
5539                .await,
5540                "{kind:?} replacement key transcript belongs to a retired generation"
5541            );
5542            Ok::<(), anyhow::Error>(())
5543        }
5544        .await;
5545
5546        match result {
5547            Ok(()) => {
5548                self.record_native_iroh_carrier_debug_event(
5549                    connection_id,
5550                    upgrade_id,
5551                    "application-crypto-confirmed",
5552                    Some(kind.transport_label()),
5553                );
5554                eprintln!(
5555                    "[OpenRTC][Iroh carrier] generation settled connection_id={} carrier={} transport_stable_id={}",
5556                    connection_id,
5557                    kind.transport_label(),
5558                    replacement_transport_stable_id,
5559                );
5560            }
5561            Err(error) => {
5562                let error_detail = format!("{error:#}");
5563                self.record_native_iroh_carrier_debug_event(
5564                    connection_id,
5565                    upgrade_id,
5566                    "application-crypto-confirmation-failed",
5567                    Some(&error_detail),
5568                );
5569                eprintln!(
5570                    "[OpenRTC][Iroh carrier] committed generation remains application-pending connection_id={} carrier={} transport_stable_id={} error={error:#}",
5571                    connection_id,
5572                    kind.transport_label(),
5573                    replacement_transport_stable_id,
5574                );
5575            }
5576        }
5577    }
5578
5579    #[cfg(all(not(target_arch = "wasm32"), feature = "iroh-carrier-core"))]
5580    pub(super) fn capture_native_iroh_carrier_authorization_fence(
5581        &self,
5582        connection_id: &str,
5583        expected_generation: crate::client::NativePeerDataGeneration,
5584        kind: IrohPathKind,
5585        policy_epoch: u64,
5586    ) -> anyhow::Result<NativeIrohCarrierAuthorizationFence> {
5587        anyhow::ensure!(
5588            self.native_application_stream_admitted_for_transport(
5589                connection_id,
5590                expected_generation.transport_stable_id,
5591            ),
5592            "{kind:?} replacement base is not fully admitted: {}",
5593            self.native_application_stream_pending_diagnostic(
5594                connection_id,
5595                expected_generation.transport_stable_id,
5596            ),
5597        );
5598        Ok(NativeIrohCarrierAuthorizationFence {
5599            inbound_admission_was_current: self.inbound_session_token_admitted_for_transport(
5600                connection_id,
5601                expected_generation.transport_stable_id,
5602            ),
5603            remote_admission_proof: self
5604                .remote_session_admission_proofs
5605                .read()
5606                .ok()
5607                .and_then(|proofs| proofs.get(connection_id).cloned())
5608                .filter(|proof| {
5609                    proof.transport_stable_id == expected_generation.transport_stable_id
5610                }),
5611            admission_fingerprint: self
5612                .session_token_registry
5613                .admission_fingerprint(connection_id),
5614            application_security_epoch_fingerprint: self
5615                .session_token_registry
5616                .application_security_epoch_fingerprint(connection_id),
5617            policy_epoch,
5618        })
5619    }
5620
5621    #[cfg(all(not(target_arch = "wasm32"), feature = "iroh-carrier-core"))]
5622    pub(super) fn native_iroh_carrier_authorization_epoch_is_current(
5623        &self,
5624        connection_id: &str,
5625        authorization_fence: &NativeIrohCarrierAuthorizationFence,
5626    ) -> bool {
5627        matches!(
5628            self.session_admission(connection_id),
5629            crate::session_token::SessionAdmission::Accepted { .. }
5630        ) && self
5631            .session_token_registry
5632            .admission_fingerprint(connection_id)
5633            == authorization_fence.admission_fingerprint
5634            && self
5635                .session_token_registry
5636                .application_security_epoch_fingerprint(connection_id)
5637                == authorization_fence.application_security_epoch_fingerprint
5638            && self.iroh_carrier_policy_epoch_is_current(authorization_fence.policy_epoch)
5639    }
5640
5641    #[cfg(all(not(target_arch = "wasm32"), feature = "iroh-carrier-core"))]
5642    pub(super) async fn native_iroh_carrier_pre_ack_authority_is_current(
5643        &self,
5644        connection_id: &str,
5645        kind: IrohPathKind,
5646        upgrade_id: &str,
5647        authorization_fence: &NativeIrohCarrierAuthorizationFence,
5648    ) -> bool {
5649        // Route observations and a locally projected successor may advance the
5650        // connection record while the protected candidate proves itself. The
5651        // upgrade gate and captured authorization epoch remain the control
5652        // authority here; atomic commit below still rejects a competing
5653        // physical successor against the exact incumbent stable ID.
5654        self.native_iroh_carrier_upgrade_is_current(
5655            connection_id,
5656            kind,
5657            upgrade_id,
5658            authorization_fence.policy_epoch,
5659        )
5660        .await
5661            && self.native_iroh_carrier_authorization_epoch_is_current(
5662                connection_id,
5663                authorization_fence,
5664            )
5665    }
5666
5667    #[cfg(all(not(target_arch = "wasm32"), feature = "iroh-carrier-core"))]
5668    async fn native_iroh_carrier_upgrade_is_current(
5669        &self,
5670        connection_id: &str,
5671        kind: IrohPathKind,
5672        upgrade_id: &str,
5673        policy_epoch: u64,
5674    ) -> bool {
5675        let gates = self.native_transport_upgrade_gates.lock().await;
5676        let peer_policy_epoch = self.current_iroh_carrier_peer_policy_epoch(connection_id);
5677        gates
5678            .get(&(connection_id.to_string(), kind))
5679            .is_some_and(|gate| {
5680                native_transport_upgrade_gate_matches(gate, upgrade_id)
5681                    && gate.policy_epoch == policy_epoch
5682                    && gate.peer_policy_epoch == peer_policy_epoch
5683                    && self.iroh_carrier_policy_epoch_is_current(policy_epoch)
5684            })
5685    }
5686
5687    #[cfg(all(not(target_arch = "wasm32"), feature = "iroh-carrier-core"))]
5688    async fn commit_proven_native_iroh_carrier_candidate(
5689        &self,
5690        connection_id: &str,
5691        remote_node_id: &str,
5692        upgrade_id: &str,
5693        expected_generation: crate::client::NativePeerDataGeneration,
5694        kind: IrohPathKind,
5695        authorization_fence: NativeIrohCarrierAuthorizationFence,
5696        node: crate::native_node::IrohNativeNode,
5697        candidate: crate::native_node::PendingReplacementConnection,
5698    ) -> anyhow::Result<
5699        crate::native_node::CommittedReplacement<crate::connection_manager::ConnectionRecord>,
5700    > {
5701        anyhow::ensure!(
5702            self.native_iroh_carrier_upgrade_is_current(
5703                connection_id,
5704                kind,
5705                upgrade_id,
5706                authorization_fence.policy_epoch,
5707            )
5708            .await,
5709            "{kind:?} replacement belongs to a retired policy or upgrade"
5710        );
5711        let authorization_fence_for_commit = authorization_fence.clone();
5712        let NativeIrohCarrierAuthorizationFence {
5713            inbound_admission_was_current,
5714            remote_admission_proof,
5715            ..
5716        } = authorization_fence;
5717        let client = self.clone();
5718        let connection_id_for_commit = connection_id.to_string();
5719        let upgrade_id_for_commit = upgrade_id.to_string();
5720        let policy_epoch_for_commit = authorization_fence_for_commit.policy_epoch;
5721        let transport_source = Some(format!("{}-upgrade", kind.transport_label()));
5722        let committed_replacement = node
5723            .commit_replacement_candidate(
5724                candidate,
5725                expected_generation.transport_stable_id,
5726                move |replacement_transport_stable_id| async move {
5727                    let mut gates = client.native_transport_upgrade_gates.lock().await;
5728                    let peer_policy_epoch =
5729                        client.current_iroh_carrier_peer_policy_epoch(&connection_id_for_commit);
5730                    anyhow::ensure!(
5731                        gates
5732                            .get(&(connection_id_for_commit.clone(), kind))
5733                            .is_some_and(|gate| {
5734                                native_transport_upgrade_gate_matches(gate, &upgrade_id_for_commit)
5735                                    && gate.policy_epoch == policy_epoch_for_commit
5736                                    && gate.peer_policy_epoch == peer_policy_epoch
5737                                    && client.iroh_carrier_policy_epoch_is_current(
5738                                        policy_epoch_for_commit,
5739                                    )
5740                            }),
5741                        "{kind:?} replacement upgrade changed before atomic commit"
5742                    );
5743                    anyhow::ensure!(
5744                        client.native_iroh_carrier_authorization_epoch_is_current(
5745                            &connection_id_for_commit,
5746                            &authorization_fence_for_commit,
5747                        ),
5748                        "{kind:?} replacement authorization epoch changed before atomic commit"
5749                    );
5750                    let installed = client
5751                        .begin_crypto_bound_transport_replacement_commit(
5752                            &connection_id_for_commit,
5753                            expected_generation.transport_stable_id,
5754                            expected_generation.transport_generation,
5755                            expected_generation.route_generation,
5756                            replacement_transport_stable_id,
5757                            transport_source,
5758                        )
5759                        .await;
5760                    if installed.is_some() && kind == IrohPathKind::WebRtc {
5761                        // Commit and failure cleanup share one linearization point.
5762                        // Later delivery/settlement errors cannot retire this winner.
5763                        gates.remove(&(connection_id_for_commit.clone(), kind));
5764                    }
5765                    drop(gates);
5766                    installed
5767                        .ok_or_else(|| anyhow::anyhow!("{kind:?} replacement generation is stale"))
5768                },
5769            )
5770            .await?;
5771        // The selected mechanism's terminal owner begins at physical commit,
5772        // even if a later acknowledgement or application settlement is delayed.
5773        #[cfg(feature = "transport-webrtc")]
5774        if kind == IrohPathKind::WebRtc {
5775            if let Some(attempt) = self
5776                .native_webrtc_carrier_attempts
5777                .read()
5778                .await
5779                .get(connection_id)
5780                .filter(|attempt| attempt.upgrade_id == upgrade_id)
5781                .cloned()
5782            {
5783                self.spawn_native_webrtc_carrier_terminal_watcher(
5784                    connection_id.to_string(),
5785                    remote_node_id.parse()?,
5786                    attempt,
5787                );
5788            }
5789        }
5790        #[cfg(not(feature = "transport-webrtc"))]
5791        let _ = remote_node_id;
5792        let committed = committed_replacement.logical_result();
5793        let replacement_transport_stable_id = committed
5794            .transport_stable_id
5795            .ok_or_else(|| anyhow::anyhow!("{kind:?} replacement has no stable ID"))?;
5796        self.retire_stale_native_main_route(connection_id, replacement_transport_stable_id)
5797            .await;
5798        anyhow::ensure!(
5799            committed.transport_stable_id == Some(replacement_transport_stable_id)
5800                && committed.transport_generation
5801                    == expected_generation.transport_generation.saturating_add(1),
5802            "{kind:?} replacement failed atomic generation commit"
5803        );
5804        if inbound_admission_was_current {
5805            self.bind_inbound_session_token_transport(
5806                connection_id,
5807                replacement_transport_stable_id,
5808            );
5809        }
5810        if let Some(mut proof) = remote_admission_proof {
5811            proof.transport_stable_id = replacement_transport_stable_id;
5812            if let Ok(mut proofs) = self.remote_session_admission_proofs.write() {
5813                proofs.insert(connection_id.to_string(), proof);
5814                self.notify_connection_application_route_update();
5815            }
5816        }
5817        eprintln!(
5818            "[OpenRTC][Iroh carrier] admission proof rebound connection_id={} carrier={} transport_stable_id={} route=[{}]",
5819            connection_id,
5820            kind.transport_label(),
5821            replacement_transport_stable_id,
5822            self.native_application_stream_pending_diagnostic(
5823                connection_id,
5824                replacement_transport_stable_id,
5825            ),
5826        );
5827        anyhow::ensure!(
5828            self.report_transport_status_for_current_generation(
5829                connection_id,
5830                kind.transport_label(),
5831                None,
5832            )
5833            .await
5834            .is_some(),
5835            "{kind:?} replacement could not publish its proven route"
5836        );
5837        // Candidate proof is transport authorization only. The atomic commit
5838        // above deliberately leaves application-crypto confirmation false;
5839        // the caller retires the incumbent and then re-drives the existing
5840        // stable-ID-bound key transcript.
5841        // Exact carrier selection is already proven by the pending candidate:
5842        // the native node admits/dials only `kind`'s transport ID, and the
5843        // bilateral encrypted probe binds that ID, carrier kind, upgrade ID,
5844        // challenge, and base generations before this commit. Do not veto the
5845        // committed candidate by querying `iroh_path_kind` here. That lookup
5846        // resolves through the shared current-connection map, which may still
5847        // expose the incumbent during the two-sided swap; treating that
5848        // point-in-time projection as authority closes the healthy carrier and
5849        // creates an endless custom-carrier/relay replacement loop.
5850        Ok(committed_replacement)
5851    }
5852
5853    #[cfg(all(not(target_arch = "wasm32"), feature = "iroh-carrier-core"))]
5854    async fn complete_native_ble_upgrade(
5855        &self,
5856        connection_id: &str,
5857        remote_node_id: &str,
5858        upgrade_id: &str,
5859        expected_generation: crate::client::NativePeerDataGeneration,
5860    ) -> anyhow::Result<()> {
5861        self.complete_native_iroh_carrier_upgrade(
5862            connection_id,
5863            remote_node_id,
5864            upgrade_id,
5865            expected_generation,
5866            IrohPathKind::Ble,
5867        )
5868        .await
5869    }
5870
5871    #[cfg(all(not(target_arch = "wasm32"), not(feature = "iroh-carrier-core")))]
5872    async fn complete_native_ble_upgrade(
5873        &self,
5874        _connection_id: &str,
5875        _remote_node_id: &str,
5876        _upgrade_id: &str,
5877        _expected_generation: crate::client::NativePeerDataGeneration,
5878    ) -> anyhow::Result<()> {
5879        anyhow::bail!("BLE replacement requires the Iroh custom-carrier core")
5880    }
5881
5882    #[cfg(not(target_arch = "wasm32"))]
5883    pub(crate) async fn migrate_remote_admission_proof_for_authorized_ble_upgrade(
5884        &self,
5885        connection_id: &str,
5886        endpoint_id: iroh::EndpointId,
5887        upgrade_id: &str,
5888    ) -> bool {
5889        let gate_key = (connection_id.to_string(), IrohPathKind::Ble);
5890        let authorized = self
5891            .native_transport_upgrade_gates
5892            .lock()
5893            .await
5894            .get(&gate_key)
5895            .is_some_and(|expected| native_transport_upgrade_gate_matches(expected, upgrade_id));
5896        if !authorized || self.iroh_path_kind(&endpoint_id.to_string()).await != IrohPathKind::Ble {
5897            return false;
5898        }
5899        if !matches!(
5900            self.session_admission(connection_id),
5901            crate::session_token::SessionAdmission::Accepted {
5902                mechanism: crate::session_token::AdmissionMechanism::SessionToken,
5903                ..
5904            }
5905        ) {
5906            return false;
5907        }
5908        let Some(stable_id) = self
5909            .get_connection(endpoint_id)
5910            .await
5911            .map(|connection| crate::transport_generation::for_connection(&connection))
5912        else {
5913            return false;
5914        };
5915        let migrated = self
5916            .remote_session_admission_proofs
5917            .write()
5918            .ok()
5919            .and_then(|mut proofs| {
5920                let proof = proofs.get_mut(connection_id)?;
5921                let previous_stable_id = proof.transport_stable_id;
5922                proof.transport_stable_id = stable_id;
5923                Some(previous_stable_id)
5924            });
5925        if let Some(previous_stable_id) = migrated {
5926            self.notify_connection_application_route_update();
5927            eprintln!(
5928                "[OpenRTC][BLE][session-admission] migrated remote proof connection_id={} endpoint_id={} upgrade_id={} previous_stable_id={} stable_id={}",
5929                connection_id, endpoint_id, upgrade_id, previous_stable_id, stable_id
5930            );
5931            true
5932        } else {
5933            false
5934        }
5935    }
5936
5937    #[cfg(not(target_arch = "wasm32"))]
5938    pub(crate) async fn migrate_remote_admission_proof_for_pending_ble_upgrade(
5939        &self,
5940        connection_id: &str,
5941        endpoint_id: iroh::EndpointId,
5942    ) -> bool {
5943        let upgrade_id = self
5944            .native_transport_upgrade_gates
5945            .lock()
5946            .await
5947            .get(&(connection_id.to_string(), IrohPathKind::Ble))
5948            .map(|gate| gate.upgrade_id.clone());
5949        let Some(upgrade_id) = upgrade_id else {
5950            return false;
5951        };
5952        self.migrate_remote_admission_proof_for_authorized_ble_upgrade(
5953            connection_id,
5954            endpoint_id,
5955            &upgrade_id,
5956        )
5957        .await
5958    }
5959
5960    pub(crate) async fn maybe_handle_typescript_json_frame(
5961        &self,
5962        connection_id: &str,
5963        remote_node_id: Option<&str>,
5964        frame: &serde_json::Value,
5965    ) {
5966        #[cfg(all(not(target_arch = "wasm32"), feature = "transport-webrtc"))]
5967        if self
5968            .handle_native_webrtc_iroh_carrier_control(connection_id, remote_node_id, frame)
5969            .await
5970        {
5971            return;
5972        }
5973        #[cfg(all(not(target_arch = "wasm32"), feature = "transport-moq"))]
5974        if self
5975            .handle_native_moq_iroh_carrier_control(connection_id, remote_node_id, frame)
5976            .await
5977        {
5978            return;
5979        }
5980        #[cfg(not(target_arch = "wasm32"))]
5981        if self
5982            .handle_native_ble_upgrade_control(connection_id, remote_node_id, frame)
5983            .await
5984        {
5985            return;
5986        }
5987    }
5988
5989    /// Send a bounded protected message on the admitted logical Iroh connection.
5990    ///
5991    /// WebRTC and MoQ are packet carriers beneath this logical connection. They
5992    /// are selected by Iroh and never appear as independent application send
5993    /// fallbacks here.
5994    pub async fn send_peer(&self, id: &str, data: &[u8]) -> anyhow::Result<()> {
5995        if data.is_empty() {
5996            return Ok(());
5997        }
5998        if data.len() > crate::native_protocol::MAX_PEER_MESSAGE_BYTES {
5999            anyhow::bail!(
6000                "peer message exceeds {}-byte limit; use a named stream for bulk data",
6001                crate::native_protocol::MAX_PEER_MESSAGE_BYTES,
6002            );
6003        }
6004        let protected = self.protect_outbound_peer_payload(id, data).await?;
6005        self.send_peer_over_iroh(id, &protected).await
6006    }
6007
6008    async fn protect_outbound_peer_payload(
6009        &self,
6010        id: &str,
6011        data: &[u8],
6012    ) -> anyhow::Result<Vec<u8>> {
6013        let mut connection_ids = Vec::new();
6014        if let Some(record) = self.connection_manager.get_by_connection_id(id).await {
6015            connection_ids.push(record.connection_id);
6016        } else if let Some(record) = self.connection_manager.best_connection_for_peer(id).await {
6017            connection_ids.push(record.connection_id);
6018        }
6019
6020        for connection_id in &connection_ids {
6021            if self
6022                .application_crypto_key_for_connection(Some(connection_id))
6023                .is_some()
6024            {
6025                return self.protect_outbound_application_payload(connection_id, data);
6026            }
6027        }
6028        if self.connection_ids_requiring_application_crypto(&connection_ids) {
6029            anyhow::bail!("application crypto required for peer send but no key is installed");
6030        }
6031        Ok(data.to_vec())
6032    }
6033}
6034
6035/// Native-only iroh send path.
6036#[cfg(not(target_arch = "wasm32"))]
6037fn frame_iroh_peer_application_payload(data: &[u8]) -> anyhow::Result<Vec<u8>> {
6038    let frame_len = 1usize
6039        .checked_add(data.len())
6040        .and_then(|len| u32::try_from(len).ok())
6041        .ok_or_else(|| anyhow::anyhow!("peer application frame is too large"))?;
6042    let mut frame = Vec::with_capacity(4 + frame_len as usize);
6043    frame.extend_from_slice(&frame_len.to_be_bytes());
6044    frame.push(0x00);
6045    frame.extend_from_slice(data);
6046    Ok(frame)
6047}
6048
6049/// Frame an SDK-owned TypeScript JSON control message for native-main.
6050///
6051/// The `0x00` discriminator is part of the native-main wire contract. Keeping
6052/// capability, WebRTC signaling, and future carrier-control messages on
6053/// one encoder prevents a valid JSON envelope from being mistaken for opaque
6054/// application data by the receiving Rust runtime.
6055#[cfg(not(target_arch = "wasm32"))]
6056fn frame_typescript_native_main_json(message: &serde_json::Value) -> anyhow::Result<Vec<u8>> {
6057    let serialized = serde_json::to_vec(message)?;
6058    let frame_len = serialized
6059        .len()
6060        .checked_add(1)
6061        .and_then(|len| u32::try_from(len).ok())
6062        .ok_or_else(|| anyhow::anyhow!("TypeScript native-main frame is too large"))?;
6063    let mut frame = Vec::with_capacity(4 + frame_len as usize);
6064    frame.extend_from_slice(&frame_len.to_be_bytes());
6065    frame.push(0x00);
6066    frame.extend_from_slice(&serialized);
6067    Ok(frame)
6068}
6069
6070#[cfg(not(target_arch = "wasm32"))]
6071impl Client {
6072    /// Send a bounded peer message over a short-lived application stream.
6073    ///
6074    /// Frame format after stream decryption: `[frame_len u32be][type=0x00][payload]`.
6075    pub async fn send_peer_over_iroh(&self, id: &str, data: &[u8]) -> anyhow::Result<()> {
6076        let record = if let Some(r) = self.connection_manager.get_by_connection_id(id).await {
6077            r
6078        } else if let Some(r) = self.connection_manager.best_connection_for_peer(id).await {
6079            r
6080        } else {
6081            anyhow::bail!("send_peer_over_iroh: no connection record for id={}", id);
6082        };
6083
6084        // Product payloads belong on an application stream, never on the SDK
6085        // native-main control stream. This distinction matters after a physical
6086        // Iroh replacement: the new admitted stream is wrapped by the host's
6087        // application-crypto boundary, while native-main remains reserved for
6088        // admission and carrier signaling.
6089        let frame = frame_iroh_peer_application_payload(data)?;
6090        self.send_peer_application_frame(&record.connection_id, &frame, Some(5_000))
6091            .await?;
6092
6093        let transport_label = self.iroh_path_kind(id).await.transport_label();
6094        if let Some(transport_stable_id) = record.transport_stable_id {
6095            let _ = self
6096                .report_transport_status_for_generation(
6097                    &record.connection_id,
6098                    transport_label,
6099                    None,
6100                    transport_stable_id,
6101                    record.transport_generation,
6102                    record.route_generation,
6103                )
6104                .await;
6105        }
6106
6107        Ok(())
6108    }
6109
6110    async fn maybe_negotiate_typescript_application_crypto(
6111        &self,
6112        connection_id: &str,
6113        remote_node_id: Option<&str>,
6114        transport_stable_id: Option<u64>,
6115        handshake: &crate::native_protocol::TypeScriptHandshake,
6116    ) -> Option<[u8; crate::key_agreement::PUBLIC_KEY_BYTES]> {
6117        let remote_public = handshake.application_key_agreement_public_key?;
6118        let remote_node_id = remote_node_id
6119            .map(str::trim)
6120            .filter(|value| !value.is_empty())?;
6121        let outcome = self
6122            .accept_application_key_handshake(
6123                connection_id,
6124                remote_node_id,
6125                transport_stable_id,
6126                handshake.action.as_deref(),
6127                remote_public,
6128            )
6129            .await
6130            .ok()?;
6131        outcome.key_changed.then_some(outcome.local_public_key)
6132    }
6133
6134    pub(crate) async fn send_typescript_capability_update(
6135        &self,
6136        connection_id: &str,
6137        reason: &str,
6138        application_key_agreement_public_key: Option<[u8; crate::key_agreement::PUBLIC_KEY_BYTES]>,
6139    ) -> anyhow::Result<()> {
6140        self.send_typescript_handshake_update(
6141            connection_id,
6142            reason,
6143            application_key_agreement_public_key,
6144            "capability-update",
6145        )
6146        .await
6147    }
6148
6149    /// Complete reciprocal application-key agreement for one exact logical
6150    /// connection generation.
6151    ///
6152    /// This is the single native/WASM recovery owner. Adapters may await the
6153    /// result and project readiness, but must not generate keys or retry
6154    /// handshake frames independently.
6155    pub async fn ensure_connection_application_crypto(
6156        &self,
6157        connection_id: &str,
6158        timeout_ms: u64,
6159    ) -> anyhow::Result<()> {
6160        let connection_id = connection_id.trim();
6161        anyhow::ensure!(!connection_id.is_empty(), "connection id is required");
6162        let timeout_ms = timeout_ms.max(1);
6163        let initial = self
6164            .connection_manager
6165            .get_by_connection_id(connection_id)
6166            .await
6167            .ok_or_else(|| anyhow::anyhow!("connection {connection_id} is not registered"))?;
6168        let initial_endpoint = initial
6169            .endpoint_id
6170            .as_deref()
6171            .or(initial.node_id.as_deref())
6172            .map(ToOwned::to_owned)
6173            .ok_or_else(|| anyhow::anyhow!("connection {connection_id} has no endpoint"))?;
6174        let initial_transport_stable_id = initial.transport_stable_id;
6175        let initial_transport_generation = initial.transport_generation;
6176        let initial_route_generation = initial.route_generation;
6177        self.set_connection_application_crypto_required(connection_id);
6178
6179        let started = std::time::Instant::now();
6180        let retry_delays_ms = [0_u64, 250, 750];
6181        for (attempt, delay_ms) in retry_delays_ms.into_iter().enumerate() {
6182            if delay_ms > 0 {
6183                let remaining = timeout_ms.saturating_sub(started.elapsed().as_millis() as u64);
6184                if remaining == 0 {
6185                    break;
6186                }
6187                tokio::time::sleep(std::time::Duration::from_millis(delay_ms.min(remaining))).await;
6188            }
6189            self.assert_application_crypto_generation(
6190                connection_id,
6191                &initial_endpoint,
6192                initial_transport_stable_id,
6193                initial_transport_generation,
6194                initial_route_generation,
6195            )
6196            .await?;
6197            if self
6198                .application_crypto_key_for_connection(Some(connection_id))
6199                .is_some()
6200                && self.connection_application_crypto_is_confirmed(
6201                    connection_id,
6202                    initial_transport_stable_id,
6203                )
6204            {
6205                return Ok(());
6206            }
6207            let public_key = self
6208                .get_or_create_connection_key_agreement(connection_id)
6209                .map_err(|error| {
6210                    anyhow::anyhow!("application key agreement initialization failed: {error:?}")
6211                })?
6212                .public_key_bytes();
6213            self.send_typescript_capability_update(
6214                connection_id,
6215                if attempt == 0 {
6216                    "application-crypto-required"
6217                } else {
6218                    "application-crypto-retry"
6219                },
6220                Some(public_key),
6221            )
6222            .await?;
6223
6224            let attempt_deadline_ms = ((attempt + 1) as u64 * timeout_ms / 3).max(1);
6225            loop {
6226                if self
6227                    .application_crypto_key_for_connection(Some(connection_id))
6228                    .is_some()
6229                    && self.connection_application_crypto_is_confirmed(
6230                        connection_id,
6231                        initial_transport_stable_id,
6232                    )
6233                {
6234                    return Ok(());
6235                }
6236                if started.elapsed().as_millis() as u64 >= attempt_deadline_ms
6237                    || started.elapsed().as_millis() as u64 >= timeout_ms
6238                {
6239                    break;
6240                }
6241                self.assert_application_crypto_generation(
6242                    connection_id,
6243                    &initial_endpoint,
6244                    initial_transport_stable_id,
6245                    initial_transport_generation,
6246                    initial_route_generation,
6247                )
6248                .await?;
6249                tokio::time::sleep(std::time::Duration::from_millis(25)).await;
6250            }
6251        }
6252
6253        anyhow::bail!(
6254            "connection {connection_id} reciprocal application key agreement timed out after {timeout_ms}ms"
6255        )
6256    }
6257
6258    async fn send_typescript_handshake_update(
6259        &self,
6260        connection_id: &str,
6261        reason: &str,
6262        application_key_agreement_public_key: Option<[u8; crate::key_agreement::PUBLIC_KEY_BYTES]>,
6263        action: &str,
6264    ) -> anyhow::Result<()> {
6265        use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
6266
6267        let local_webrtc_enabled = self.local_typescript_webrtc_capability().await;
6268        let local_moq_enabled = self.local_typescript_moq_capability().await;
6269        let local_ble_enabled = self.is_ble_transport_enabled().await;
6270        let dedicated_carrier_control = local_webrtc_enabled || local_moq_enabled;
6271        let native_admission_route_ready = {
6272            #[cfg(not(target_arch = "wasm32"))]
6273            {
6274                self.current_native_peer_data_generation(connection_id, None)
6275                    .await
6276                    .is_some_and(|generation| {
6277                        self.native_admission_route_is_ready_for_transport(
6278                            connection_id,
6279                            Some(generation.transport_stable_id),
6280                        )
6281                    })
6282            }
6283            #[cfg(target_arch = "wasm32")]
6284            {
6285                true
6286            }
6287        };
6288        let dedicated_carrier_control_ready = dedicated_carrier_control_is_ready(
6289            dedicated_carrier_control,
6290            self.connection_application_crypto_is_confirmed(
6291                connection_id,
6292                self.connection_manager
6293                    .get_by_connection_id(connection_id)
6294                    .await
6295                    .and_then(|record| record.transport_stable_id),
6296            ),
6297            native_admission_route_ready,
6298        );
6299        // Native must seed mandatory application key agreement. Waiting for the
6300        // peer's public key here deadlocks with TS peers, which also wait for a
6301        // remote advertisement before sending a reciprocal capability update.
6302        let application_key_agreement_public_key =
6303            application_key_agreement_public_key.or_else(|| {
6304                self.get_or_create_connection_key_agreement(connection_id)
6305                    .ok()
6306                    .map(|agreement| agreement.public_key_bytes())
6307            });
6308        let mut capability_frame = serde_json::json!({
6309            "type": "handshake",
6310            "action": action,
6311            "capabilities": {
6312                "carrierSchema": 2,
6313                "webrtc": local_webrtc_enabled,
6314                "moq": local_moq_enabled,
6315                "dedicatedCarrierControl": dedicated_carrier_control,
6316                "dedicatedCarrierControlReady": dedicated_carrier_control_ready,
6317                "ble": local_ble_enabled,
6318            }
6319        });
6320        if let Some(public_key) = application_key_agreement_public_key {
6321            capability_frame["capabilities"]["applicationKeyAgreement"] =
6322                serde_json::Value::Bool(true);
6323            capability_frame["applicationKeyAgreement"] = serde_json::json!({
6324                "algorithm": crate::key_agreement::KEY_ALGORITHM,
6325                "publicKey": URL_SAFE_NO_PAD.encode(public_key),
6326            });
6327        }
6328        eprintln!(
6329            "[OpenRTC][capability] sending update connection_id={} reason={} action={} webrtc={} moq={} ble={} application_key_agreement={}",
6330            connection_id,
6331            reason,
6332            action,
6333            local_webrtc_enabled,
6334            local_moq_enabled,
6335            local_ble_enabled,
6336            application_key_agreement_public_key.is_some(),
6337        );
6338        #[cfg(not(target_arch = "wasm32"))]
6339        let persistent_native_control = self.native_admission_stream_is_persistent(connection_id);
6340        #[cfg(target_arch = "wasm32")]
6341        let persistent_native_control = false;
6342        if application_key_reply_uses_fresh_signal(action, persistent_native_control) {
6343            self.send_typescript_handshake_over_fresh_signal(connection_id, &capability_frame)
6344                .await
6345        } else {
6346            self.send_typescript_handshake_over_native_main(connection_id, &capability_frame)
6347                .await
6348        }
6349    }
6350
6351    async fn send_typescript_handshake_over_fresh_signal(
6352        &self,
6353        connection_id: &str,
6354        message: &serde_json::Value,
6355    ) -> anyhow::Result<()> {
6356        let record = self
6357            .connection_manager
6358            .get_by_connection_id(connection_id)
6359            .await
6360            .ok_or_else(|| anyhow::anyhow!("connection {connection_id} is not registered"))?;
6361        let endpoint_id = record
6362            .endpoint_id
6363            .as_deref()
6364            .or(record.node_id.as_deref())
6365            .ok_or_else(|| anyhow::anyhow!("connection {connection_id} has no endpoint"))?
6366            .parse::<iroh::EndpointId>()?;
6367        let frame = frame_typescript_native_main_json(message)?;
6368        let (mut send, _recv) = self.open_bi_internal(endpoint_id).await?;
6369        let label = b"signal";
6370        send.write_all(&[0x00]).await?;
6371        send.write_all(&(label.len() as u32).to_be_bytes()).await?;
6372        send.write_all(label).await?;
6373        send.write_all(&frame).await?;
6374        crate::application_crypto_streams::PeerSendStream::plain(send)
6375            .finish_and_wait_for_peer(std::time::Duration::from_secs(2))
6376            .await
6377            .map_err(anyhow::Error::msg)
6378    }
6379
6380    async fn send_typescript_handshake_over_native_main(
6381        &self,
6382        id: &str,
6383        message: &serde_json::Value,
6384    ) -> anyhow::Result<()> {
6385        let record = if let Some(r) = self.connection_manager.get_by_connection_id(id).await {
6386            r
6387        } else if let Some(r) = self.connection_manager.best_connection_for_peer(id).await {
6388            r
6389        } else {
6390            anyhow::bail!(
6391                "send_typescript_handshake_over_native_main: no connection record for id={}",
6392                id
6393            );
6394        };
6395
6396        let endpoint_str = record
6397            .endpoint_id
6398            .as_deref()
6399            .or(record.node_id.as_deref())
6400            .ok_or_else(|| {
6401                anyhow::anyhow!(
6402                    "send_typescript_handshake_over_native_main: missing endpoint for connection_id={}",
6403                    record.connection_id
6404                )
6405            })?;
6406
6407        let endpoint_id = endpoint_str.parse::<iroh::EndpointId>().map_err(|e| {
6408            anyhow::anyhow!(
6409                "send_typescript_handshake_over_native_main: invalid endpoint_id={} error={}",
6410                endpoint_str,
6411                e
6412            )
6413        })?;
6414
6415        let frame = frame_typescript_native_main_json(message)?;
6416
6417        self.send_native_main_frame(
6418            id,
6419            endpoint_id,
6420            message
6421                .get("type")
6422                .and_then(|value| value.as_str())
6423                .unwrap_or("control"),
6424            &frame,
6425        )
6426        .await
6427        .map_err(anyhow::Error::msg)
6428    }
6429}
6430
6431/// WASM stub — `send_peer_over_iroh` is not meaningful on WASM since the TypeScript
6432/// layer owns transport selection. Defined here so `send_peer` compiles on all targets.
6433#[cfg(target_arch = "wasm32")]
6434impl Client {
6435    pub async fn send_peer_over_iroh(&self, id: &str, data: &[u8]) -> anyhow::Result<()> {
6436        let _ = (id, data);
6437        Err(anyhow::anyhow!(
6438            "send_peer_over_iroh: not available on WASM — use TypeScript Connection.sendTyped()"
6439        ))
6440    }
6441}
6442
6443#[cfg(all(not(target_arch = "wasm32"), feature = "transport-webrtc"))]
6444fn parse_webrtc_signal_message_for_transport(
6445    frame: &serde_json::Value,
6446    expected_transport: &str,
6447) -> Option<crate::transport::WebRTCSignalMessage> {
6448    if frame.get("type").and_then(|value| value.as_str()) != Some("#pluto-signal") {
6449        return None;
6450    }
6451
6452    let content = frame.get("content")?;
6453    let transport = content
6454        .get("transport")
6455        .and_then(|value| value.as_str())
6456        .unwrap_or("webrtc");
6457    if transport != expected_transport {
6458        return None;
6459    }
6460
6461    let signal_type = content
6462        .get("type")
6463        .and_then(|value| value.as_str())
6464        .map(ToOwned::to_owned)
6465        .unwrap_or_else(|| "candidate".to_string());
6466
6467    Some(crate::transport::WebRTCSignalMessage {
6468        transport: transport.to_string(),
6469        signal_type,
6470        negotiation_id: content
6471            .get("negotiationId")
6472            .and_then(|value| value.as_str())
6473            .map(str::trim)
6474            .filter(|value| !value.is_empty())
6475            .map(ToOwned::to_owned),
6476        sdp: content.get("sdp").cloned(),
6477        candidate: content.get("candidate").cloned(),
6478    })
6479}
6480
6481#[cfg(all(not(target_arch = "wasm32"), feature = "transport-webrtc"))]
6482fn iroh_webrtc_signal_matches_upgrade(
6483    signal: &crate::transport::WebRTCSignalMessage,
6484    upgrade_id: &str,
6485) -> bool {
6486    signal.negotiation_id.as_deref() == Some(upgrade_id)
6487}
6488
6489fn should_send_typescript_capability_update(
6490    local_webrtc_enabled: bool,
6491    local_moq_enabled: bool,
6492    local_ble_enabled: bool,
6493    has_application_key_agreement_public_key: bool,
6494    incoming_action: Option<&str>,
6495) -> bool {
6496    match incoming_action {
6497        // Key-bearing replies already carry the complete local capability set.
6498        // Never answer one with another publication: doing so amplifies a
6499        // stable key transcript into an unbounded response/update loop.
6500        Some("capability-update" | "response" | "ack") => return false,
6501        _ => {}
6502    }
6503
6504    has_application_key_agreement_public_key
6505        || local_webrtc_enabled
6506        || local_moq_enabled
6507        || local_ble_enabled
6508}
6509
6510fn application_key_reply_uses_fresh_signal(action: &str, persistent_native_control: bool) -> bool {
6511    matches!(action, "response" | "ack") && !persistent_native_control
6512}
6513
6514#[cfg(not(target_arch = "wasm32"))]
6515fn effective_typescript_transport_capability(
6516    uses_canonical_carrier_schema: bool,
6517    advertised: bool,
6518    known: bool,
6519) -> bool {
6520    if uses_canonical_carrier_schema {
6521        advertised
6522    } else {
6523        known
6524    }
6525}
6526
6527fn typescript_application_key_reply_action(
6528    incoming_action: Option<&str>,
6529    has_application_key: bool,
6530    _application_key_changed: bool,
6531    application_crypto_was_confirmed: bool,
6532) -> Option<&'static str> {
6533    if !has_application_key {
6534        return None;
6535    }
6536    match incoming_action {
6537        // The ACK is the first frame that confirms application crypto when
6538        // this endpoint sent the response. Publish the now-actionable carrier
6539        // readiness once so the peer can move carrier control off the
6540        // bootstrap main stream. A repeated ACK for an already-confirmed
6541        // security epoch remains a no-op.
6542        Some("ack") if !application_crypto_was_confirmed => Some("capability-update"),
6543        Some("ack") => None,
6544        Some("response") => Some("ack"),
6545        _ => Some("response"),
6546    }
6547}
6548
6549#[cfg(not(target_arch = "wasm32"))]
6550fn should_initiate_native_ble_upgrade(
6551    local_node_id: &str,
6552    remote_node_id: &str,
6553    current_path: IrohPathKind,
6554) -> bool {
6555    local_node_id > remote_node_id
6556        && !matches!(
6557            current_path,
6558            IrohPathKind::DirectQuic | IrohPathKind::DirectLan | IrohPathKind::Ble
6559        )
6560}
6561
6562#[cfg(not(target_arch = "wasm32"))]
6563fn should_accept_native_ble_switch_request(
6564    local_node_id: &str,
6565    remote_node_id: &str,
6566    current_path: IrohPathKind,
6567) -> bool {
6568    local_node_id < remote_node_id
6569        && matches!(current_path, IrohPathKind::Relay | IrohPathKind::Unknown)
6570}
6571
6572#[cfg(not(target_arch = "wasm32"))]
6573fn native_transport_upgrade_gate_matches(
6574    current: &crate::client::NativeTransportUpgradeGate,
6575    upgrade_id: &str,
6576) -> bool {
6577    current.upgrade_id == upgrade_id
6578        || current
6579            .upgrade_id
6580            .strip_prefix(NATIVE_BLE_COMPLETION_GATE_PREFIX)
6581            .is_some_and(|completing_upgrade_id| completing_upgrade_id == upgrade_id)
6582}
6583
6584#[cfg(not(target_arch = "wasm32"))]
6585fn transition_native_ble_upgrade_gate_to_completion(
6586    current: &mut crate::client::NativeTransportUpgradeGate,
6587    upgrade_id: &str,
6588    proof_generation: crate::client::NativePeerDataGeneration,
6589) -> Option<crate::client::NativePeerDataGeneration> {
6590    if current.upgrade_id != upgrade_id || current.generation != proof_generation {
6591        return None;
6592    }
6593
6594    current.upgrade_id = format!("{NATIVE_BLE_COMPLETION_GATE_PREFIX}{upgrade_id}");
6595    Some(current.generation)
6596}
6597
6598/// An at-least-once control replay must reuse the exact live responder
6599/// attempt even after that attempt has installed a custom generation. Looking
6600/// only at the now-current base generation would misclassify this replay as a
6601/// new request and can retire the winning carrier before Ready is delivered.
6602#[cfg(all(
6603    not(target_arch = "wasm32"),
6604    any(feature = "transport-webrtc", feature = "transport-moq")
6605))]
6606fn native_carrier_duplicate_request_is_live(
6607    request: &crate::iroh_carrier_bootstrap::CarrierBootstrapFrame,
6608    attempt: &crate::iroh_carrier_bootstrap::CarrierBootstrapFrame,
6609    terminal: bool,
6610) -> bool {
6611    !terminal && request == attempt
6612}
6613
6614/// A repeated bootstrap is safe to reuse only while it still names the
6615/// current physical base generation and a non-terminal WebRTC attempt.
6616#[cfg(all(not(target_arch = "wasm32"), feature = "transport-webrtc"))]
6617fn native_webrtc_duplicate_bootstrap_is_reserved_or_live(
6618    gate: &crate::client::NativeTransportUpgradeGate,
6619    upgrade_id: &str,
6620    generation: crate::client::NativePeerDataGeneration,
6621    attempt: Option<&crate::client::NativeWebRtcCarrierAttempt>,
6622) -> bool {
6623    if !native_transport_upgrade_gate_matches(gate, upgrade_id) || gate.generation != generation {
6624        return false;
6625    }
6626
6627    // The gate is inserted before packet-session activation and attempt
6628    // publication. A replay in that window must reuse the reservation; starting
6629    // a second responder would attach the same packet session twice and can
6630    // retire the winning carrier with DuplicateSession. Once an attempt is
6631    // published, only that exact non-terminal attempt keeps the reservation
6632    // live. Failure and terminal paths remove the gate before a later replay is
6633    // allowed to create a replacement.
6634    attempt.is_none_or(|attempt| {
6635        native_webrtc_duplicate_bootstrap_matches_live_attempt(
6636            gate,
6637            upgrade_id,
6638            generation,
6639            &attempt.upgrade_id,
6640            attempt.generation,
6641            attempt.channel.state(),
6642        )
6643    })
6644}
6645
6646#[cfg(all(not(target_arch = "wasm32"), feature = "transport-webrtc"))]
6647fn native_webrtc_duplicate_bootstrap_matches_live_attempt(
6648    gate: &crate::client::NativeTransportUpgradeGate,
6649    upgrade_id: &str,
6650    generation: crate::client::NativePeerDataGeneration,
6651    attempt_upgrade_id: &str,
6652    attempt_generation: crate::client::NativePeerDataGeneration,
6653    attempt_state: crate::transport::NativeWebRTCState,
6654) -> bool {
6655    native_transport_upgrade_gate_matches(gate, upgrade_id)
6656        && gate.generation == generation
6657        && attempt_upgrade_id == upgrade_id
6658        && attempt_generation == generation
6659        && !matches!(
6660            attempt_state,
6661            crate::transport::NativeWebRTCState::Failed
6662                | crate::transport::NativeWebRTCState::Closed
6663        )
6664}
6665
6666#[cfg(not(target_arch = "wasm32"))]
6667fn reserve_native_transport_upgrade_gate(
6668    gates: &mut std::collections::HashMap<
6669        (String, IrohPathKind),
6670        crate::client::NativeTransportUpgradeGate,
6671    >,
6672    key: (String, IrohPathKind),
6673    upgrade_id: &str,
6674    generation: crate::client::NativePeerDataGeneration,
6675    policy_epoch: u64,
6676    peer_policy_epoch: u64,
6677    replace_existing: bool,
6678) -> bool {
6679    // WebRTC and MoQ are alternative physical carriers for one logical Iroh
6680    // generation. A capability replay can temporarily rank the fallback while
6681    // the preferred carrier is already proving; allowing both kind-specific
6682    // gates would let the faster candidate replace the same incumbent and
6683    // invalidate the preferred proof. Policy changes use a new epoch and may
6684    // deliberately prepare a different carrier over the selected incumbent.
6685    if gates.iter().any(|((connection_id, kind), current)| {
6686        connection_id == &key.0
6687            && *kind != key.1
6688            && current.generation == generation
6689            && current.policy_epoch == policy_epoch
6690            && current.peer_policy_epoch == peer_policy_epoch
6691    }) {
6692        return false;
6693    }
6694    if let Some(current) = gates.get(&key) {
6695        if current.policy_epoch == policy_epoch && current.peer_policy_epoch == peer_policy_epoch {
6696            if !replace_existing && current.generation == generation {
6697                return false;
6698            }
6699            if current.upgrade_id == upgrade_id {
6700                return replace_existing;
6701            }
6702        }
6703    }
6704    gates.insert(
6705        key,
6706        crate::client::NativeTransportUpgradeGate {
6707            upgrade_id: upgrade_id.to_string(),
6708            generation,
6709            policy_epoch,
6710            peer_policy_epoch,
6711        },
6712    );
6713    true
6714}
6715
6716#[cfg(not(target_arch = "wasm32"))]
6717async fn remove_native_transport_upgrade_gate(
6718    gates: &std::sync::Arc<
6719        tokio::sync::Mutex<
6720            std::collections::HashMap<
6721                (String, IrohPathKind),
6722                crate::client::NativeTransportUpgradeGate,
6723            >,
6724        >,
6725    >,
6726    key: &(String, IrohPathKind),
6727    upgrade_id: &str,
6728) -> bool {
6729    let mut gates = gates.lock().await;
6730    if gates
6731        .get(key)
6732        .is_some_and(|current| current.upgrade_id == upgrade_id)
6733    {
6734        gates.remove(key);
6735        true
6736    } else {
6737        false
6738    }
6739}
6740
6741#[cfg(all(not(target_arch = "wasm32"), feature = "transport-webrtc"))]
6742async fn remove_native_transport_upgrade_gate_for_generation(
6743    gates: &std::sync::Arc<
6744        tokio::sync::Mutex<
6745            std::collections::HashMap<
6746                (String, IrohPathKind),
6747                crate::client::NativeTransportUpgradeGate,
6748            >,
6749        >,
6750    >,
6751    key: &(String, IrohPathKind),
6752    upgrade_id: &str,
6753    generation: crate::client::NativePeerDataGeneration,
6754) -> bool {
6755    let mut gates = gates.lock().await;
6756    if gates
6757        .get(key)
6758        .is_some_and(|current| current.upgrade_id == upgrade_id && current.generation == generation)
6759    {
6760        gates.remove(key);
6761        true
6762    } else {
6763        false
6764    }
6765}
6766
6767#[cfg(test)]
6768mod tests {
6769    #[cfg(not(target_arch = "wasm32"))]
6770    use super::effective_typescript_transport_capability;
6771    #[cfg(all(not(target_arch = "wasm32"), feature = "iroh-carrier-core"))]
6772    use crate::Client;
6773
6774    #[cfg(all(
6775        not(target_arch = "wasm32"),
6776        any(feature = "transport-webrtc", feature = "transport-moq")
6777    ))]
6778    #[tokio::test]
6779    async fn carrier_policy_epoch_changes_only_with_effective_config() {
6780        let client = Client::new_with_app_tag(
6781            crate::test_constants::TEST_PROJECT_ID.to_string(),
6782            "test-app".to_string(),
6783            Box::new(|| None),
6784        );
6785        let initial = client.current_iroh_carrier_policy_epoch();
6786        let mut config = client.transport_config().await;
6787
6788        client
6789            .update_transport_config(config.clone())
6790            .await
6791            .expect("idempotent carrier policy update");
6792        assert_eq!(client.current_iroh_carrier_policy_epoch(), initial);
6793
6794        config.optimize_for = crate::route_policy::RouteOptimization::LowestLatency;
6795        client
6796            .update_transport_config(config)
6797            .await
6798            .expect("changed carrier policy update");
6799        assert_eq!(
6800            client.current_iroh_carrier_policy_epoch(),
6801            initial.saturating_add(1),
6802        );
6803    }
6804
6805    #[cfg(all(not(target_arch = "wasm32"), feature = "iroh-carrier-core"))]
6806    #[tokio::test]
6807    async fn capability_withdrawal_is_serialized_with_the_native_proof_fence() {
6808        let client = Client::new_with_app_tag(
6809            crate::test_constants::TEST_PROJECT_ID.to_string(),
6810            "test-app".to_string(),
6811            Box::new(|| None),
6812        );
6813        let generation = crate::client::NativePeerDataGeneration {
6814            transport_stable_id: 7,
6815            transport_generation: 11,
6816            route_generation: 13,
6817        };
6818        let policy_epoch = client.current_iroh_carrier_policy_epoch();
6819        let peer_epoch = client.current_iroh_carrier_peer_policy_epoch("peer-a");
6820        {
6821            let mut capabilities = client.native_peer_transport_capabilities.write().await;
6822            capabilities.insert(
6823                "peer-a".to_string(),
6824                [crate::client::NativePeerTransportCapability::Moq]
6825                    .into_iter()
6826                    .collect(),
6827            );
6828            capabilities.insert(
6829                "peer-b".to_string(),
6830                [crate::client::NativePeerTransportCapability::Moq]
6831                    .into_iter()
6832                    .collect(),
6833            );
6834        }
6835        {
6836            let mut gates = client.native_transport_upgrade_gates.lock().await;
6837            for (connection_id, upgrade_id) in [("peer-a", "attempt-a"), ("peer-b", "attempt-b")] {
6838                gates.insert(
6839                    (connection_id.to_string(), crate::client::IrohPathKind::Moq),
6840                    crate::client::NativeTransportUpgradeGate {
6841                        upgrade_id: upgrade_id.to_string(),
6842                        generation,
6843                        policy_epoch,
6844                        peer_policy_epoch: peer_epoch,
6845                    },
6846                );
6847            }
6848        }
6849
6850        let held_gate = client.native_transport_upgrade_gates.lock().await;
6851        let updating = client.clone();
6852        let update = tokio::spawn(async move {
6853            updating
6854                .maybe_handle_typescript_handshake_capabilities(
6855                    "peer-a",
6856                    None,
6857                    None,
6858                    &crate::native_protocol::TypeScriptHandshake {
6859                        action: Some("capability-update".to_string()),
6860                        session_token: None,
6861                        session_token_payload: None,
6862                        claimed_device_id: None,
6863                        capabilities: Some(crate::native_protocol::HandshakeFeatures {
6864                            carrier_schema: Some(2),
6865                            webrtc: Some(false),
6866                            moq: Some(false),
6867                            dedicated_carrier_control: Some(false),
6868                            dedicated_carrier_control_ready: Some(false),
6869                            ble: Some(false),
6870                            application_key_agreement: Some(false),
6871                        }),
6872                        application_key_agreement_public_key: None,
6873                    },
6874                )
6875                .await;
6876        });
6877        tokio::task::yield_now().await;
6878        assert!(
6879            client
6880                .native_peer_transport_capabilities
6881                .read()
6882                .await
6883                .get("peer-a")
6884                .is_some_and(|capabilities| capabilities
6885                    .contains(&crate::client::NativePeerTransportCapability::Moq)),
6886            "capability mutation must wait behind the proof/commit fence",
6887        );
6888        drop(held_gate);
6889        update.await.expect("capability update task");
6890
6891        assert!(
6892            !client
6893                .native_iroh_carrier_upgrade_is_current(
6894                    "peer-a",
6895                    crate::client::IrohPathKind::Moq,
6896                    "attempt-a",
6897                    policy_epoch,
6898                )
6899                .await
6900        );
6901        assert!(
6902            client
6903                .native_iroh_carrier_upgrade_is_current(
6904                    "peer-b",
6905                    crate::client::IrohPathKind::Moq,
6906                    "attempt-b",
6907                    policy_epoch,
6908                )
6909                .await
6910        );
6911    }
6912
6913    #[cfg(not(target_arch = "wasm32"))]
6914    use super::{
6915        application_key_reply_uses_fresh_signal, classify_native_ble_switch_admission,
6916        frame_iroh_peer_application_payload, native_ble_attempt_state_after_recovery_wake,
6917        native_ble_retry_delay, native_control_stream_rank, native_transport_upgrade_gate_matches,
6918        remove_native_transport_upgrade_gate, reserve_native_ble_attempt_state,
6919        reserve_native_transport_upgrade_gate, should_accept_native_ble_switch_request,
6920        should_drain_replaced_native_control_frame, should_initiate_native_ble_upgrade,
6921        should_replace_native_control_stream, should_send_typescript_capability_update,
6922        transition_native_ble_upgrade_gate_to_completion, typescript_application_key_reply_action,
6923        NativeBleSwitchAdmission, NativeControlStreamOwner,
6924        MAX_REPLACED_NATIVE_CONTROL_DRAIN_FRAMES, NATIVE_BLE_COMPLETION_GATE_PREFIX,
6925        NATIVE_BLE_MAX_UPGRADE_ATTEMPTS,
6926    };
6927
6928    #[cfg(all(not(target_arch = "wasm32"), feature = "transport-moq"))]
6929    use super::{moq_retry_attempt, native_moq_peer_failure_code};
6930
6931    #[cfg(all(
6932        not(target_arch = "wasm32"),
6933        any(feature = "transport-webrtc", feature = "transport-moq")
6934    ))]
6935    use super::native_carrier_duplicate_request_is_live;
6936
6937    #[cfg(all(not(target_arch = "wasm32"), feature = "transport-webrtc"))]
6938    use super::{
6939        dedicated_carrier_control_is_ready, native_webrtc_duplicate_bootstrap_is_reserved_or_live,
6940        native_webrtc_duplicate_bootstrap_matches_live_attempt,
6941        remove_native_transport_upgrade_gate_for_generation, webrtc_retry_attempt,
6942    };
6943
6944    #[cfg(all(not(target_arch = "wasm32"), feature = "transport-webrtc"))]
6945    #[test]
6946    fn native_dedicated_control_readiness_requires_current_admission_route() {
6947        assert!(!dedicated_carrier_control_is_ready(true, true, false));
6948        assert!(!dedicated_carrier_control_is_ready(true, false, true));
6949        assert!(dedicated_carrier_control_is_ready(true, true, true));
6950        assert!(!dedicated_carrier_control_is_ready(false, true, true));
6951    }
6952
6953    #[cfg(all(
6954        not(target_arch = "wasm32"),
6955        any(feature = "transport-webrtc", feature = "transport-moq")
6956    ))]
6957    #[test]
6958    fn exact_live_native_carrier_replay_survives_base_generation_replacement() {
6959        let request = crate::iroh_carrier_bootstrap::CarrierBootstrapFrame::request_with_ids(
6960            crate::iroh_carrier_bootstrap::CarrierBootstrapKind::MoqDraft14,
6961            crate::iroh_carrier_bootstrap::CarrierGenerationFence {
6962                transport_stable_id: 3,
6963                transport_generation: 2,
6964                route_generation: 0,
6965            },
6966            1,
6967            [7; 16],
6968            [8; 16],
6969        );
6970        let mut competing = request.clone();
6971        competing.upgrade_id = "09".repeat(16);
6972
6973        assert!(native_carrier_duplicate_request_is_live(
6974            &request, &request, false,
6975        ));
6976        assert!(!native_carrier_duplicate_request_is_live(
6977            &request, &request, true,
6978        ));
6979        assert!(!native_carrier_duplicate_request_is_live(
6980            &competing, &request, false,
6981        ));
6982    }
6983
6984    #[cfg(all(not(target_arch = "wasm32"), feature = "transport-webrtc"))]
6985    #[tokio::test]
6986    async fn repeated_typescript_application_key_offer_is_not_reciprocated_twice() {
6987        let client = Client::new_with_app_tag(
6988            crate::test_constants::TEST_PROJECT_ID.to_string(),
6989            "test-app".to_string(),
6990            Box::new(|| None),
6991        );
6992        *client.node_id.write().await = Some("local-node".to_string());
6993        let remote_agreement =
6994            crate::key_agreement::KeyAgreement::generate().expect("remote agreement");
6995        let handshake = crate::native_protocol::TypeScriptHandshake {
6996            action: Some("capability-update".to_string()),
6997            session_token: None,
6998            session_token_payload: None,
6999            claimed_device_id: None,
7000            capabilities: Some(crate::native_protocol::HandshakeFeatures {
7001                carrier_schema: Some(2),
7002                webrtc: Some(true),
7003                moq: Some(false),
7004                dedicated_carrier_control: Some(true),
7005                dedicated_carrier_control_ready: Some(false),
7006                ble: Some(false),
7007                application_key_agreement: Some(true),
7008            }),
7009            application_key_agreement_public_key: Some(remote_agreement.public_key_bytes()),
7010        };
7011
7012        assert!(client
7013            .maybe_negotiate_typescript_application_crypto(
7014                "connection-1",
7015                Some("remote-node"),
7016                None,
7017                &handshake,
7018            )
7019            .await
7020            .is_some());
7021        assert!(client
7022            .maybe_negotiate_typescript_application_crypto(
7023                "connection-1",
7024                Some("remote-node"),
7025                None,
7026                &handshake,
7027            )
7028            .await
7029            .is_none());
7030        assert!(client.connection_requires_application_crypto_confirmation("connection-1"));
7031        assert!(!client.connection_application_crypto_is_confirmed("connection-1", None));
7032        assert_eq!(
7033            client.session_admission_block_reason("connection-1"),
7034            Some((false, "application-crypto-confirmation-pending".to_string())),
7035        );
7036
7037        // Confirmation frames are generation-bound. This unit intentionally
7038        // has no live Iroh/manager generation, so the response must fail
7039        // closed instead of manufacturing stable id 10. Live-generation ACK
7040        // and replacement fencing are covered by the native integration tests.
7041        let mut response = handshake;
7042        response.action = Some("response".to_string());
7043        assert!(client
7044            .maybe_negotiate_typescript_application_crypto(
7045                "connection-1",
7046                Some("remote-node"),
7047                None,
7048                &response,
7049            )
7050            .await
7051            .is_none());
7052        assert!(!client.connection_application_crypto_is_confirmed("connection-1", None));
7053        assert_eq!(
7054            client.session_admission_block_reason("connection-1"),
7055            Some((false, "application-crypto-confirmation-pending".to_string())),
7056        );
7057    }
7058
7059    #[cfg(all(not(target_arch = "wasm32"), feature = "transport-webrtc"))]
7060    #[tokio::test]
7061    async fn native_webrtc_terminal_cleanup_retires_only_its_exact_upgrade_gate() {
7062        let generation = crate::client::NativePeerDataGeneration {
7063            transport_stable_id: 10,
7064            transport_generation: 2,
7065            route_generation: 1,
7066        };
7067        let key = (
7068            "connection-1".to_string(),
7069            crate::client::IrohPathKind::WebRtc,
7070        );
7071        let gates =
7072            std::sync::Arc::new(tokio::sync::Mutex::new(std::collections::HashMap::from([
7073                (
7074                    key.clone(),
7075                    crate::client::NativeTransportUpgradeGate {
7076                        upgrade_id: "terminal-attempt".to_string(),
7077                        generation,
7078                        policy_epoch: 0,
7079                        peer_policy_epoch: 0,
7080                    },
7081                ),
7082            ])));
7083
7084        assert!(remove_native_transport_upgrade_gate(&gates, &key, "terminal-attempt").await);
7085        assert!(gates.lock().await.is_empty());
7086
7087        gates.lock().await.insert(
7088            key.clone(),
7089            crate::client::NativeTransportUpgradeGate {
7090                upgrade_id: "replacement-attempt".to_string(),
7091                generation,
7092                policy_epoch: 0,
7093                peer_policy_epoch: 0,
7094            },
7095        );
7096        assert!(
7097            !remove_native_transport_upgrade_gate(&gates, &key, "terminal-attempt").await,
7098            "a delayed terminal watcher must not retire a replacement attempt's gate",
7099        );
7100        assert_eq!(
7101            gates
7102                .lock()
7103                .await
7104                .get(&key)
7105                .map(|gate| gate.upgrade_id.as_str()),
7106            Some("replacement-attempt"),
7107        );
7108
7109        let next_generation = crate::client::NativePeerDataGeneration {
7110            transport_stable_id: 11,
7111            transport_generation: 3,
7112            route_generation: 2,
7113        };
7114        assert!(
7115            !remove_native_transport_upgrade_gate_for_generation(
7116                &gates,
7117                &key,
7118                "replacement-attempt",
7119                next_generation,
7120            )
7121            .await
7122        );
7123        assert!(
7124            remove_native_transport_upgrade_gate_for_generation(
7125                &gates,
7126                &key,
7127                "replacement-attempt",
7128                generation,
7129            )
7130            .await
7131        );
7132        assert!(gates.lock().await.is_empty());
7133    }
7134
7135    #[cfg(all(not(target_arch = "wasm32"), feature = "transport-webrtc"))]
7136    #[test]
7137    fn native_webrtc_duplicate_bootstrap_requires_live_same_base_generation() {
7138        let base = crate::client::NativePeerDataGeneration {
7139            transport_stable_id: 10,
7140            transport_generation: 2,
7141            route_generation: 1,
7142        };
7143        let replacement = crate::client::NativePeerDataGeneration {
7144            transport_stable_id: 11,
7145            transport_generation: 3,
7146            route_generation: 0,
7147        };
7148        let gate = crate::client::NativeTransportUpgradeGate {
7149            upgrade_id: "upgrade-1".to_string(),
7150            generation: base,
7151            policy_epoch: 0,
7152            peer_policy_epoch: 0,
7153        };
7154
7155        assert!(native_webrtc_duplicate_bootstrap_is_reserved_or_live(
7156            &gate,
7157            "upgrade-1",
7158            base,
7159            None,
7160        ));
7161
7162        assert!(native_webrtc_duplicate_bootstrap_matches_live_attempt(
7163            &gate,
7164            "upgrade-1",
7165            base,
7166            "upgrade-1",
7167            base,
7168            crate::transport::NativeWebRTCState::Connecting,
7169        ));
7170        assert!(!native_webrtc_duplicate_bootstrap_matches_live_attempt(
7171            &gate,
7172            "upgrade-1",
7173            replacement,
7174            "upgrade-1",
7175            base,
7176            crate::transport::NativeWebRTCState::Connected,
7177        ));
7178        assert!(!native_webrtc_duplicate_bootstrap_matches_live_attempt(
7179            &gate,
7180            "upgrade-1",
7181            base,
7182            "upgrade-1",
7183            base,
7184            crate::transport::NativeWebRTCState::Closed,
7185        ));
7186        assert!(!native_webrtc_duplicate_bootstrap_matches_live_attempt(
7187            &gate,
7188            "upgrade-1",
7189            base,
7190            "other-upgrade",
7191            base,
7192            crate::transport::NativeWebRTCState::Connected,
7193        ));
7194    }
7195
7196    #[cfg(all(not(target_arch = "wasm32"), feature = "transport-webrtc"))]
7197    #[test]
7198    fn webrtc_carrier_retries_one_transient_candidate_failure() {
7199        for failure_code in [
7200            "carrier-base-generation-stale",
7201            "carrier-start-failed",
7202            "carrier-timeout",
7203            "carrier-proof-failed",
7204        ] {
7205            assert_eq!(webrtc_retry_attempt(failure_code, 1), Some(2));
7206            assert_eq!(webrtc_retry_attempt(failure_code, 2), None);
7207        }
7208        assert_eq!(webrtc_retry_attempt("peer-rejected-carrier", 1), None);
7209        assert_eq!(
7210            webrtc_retry_attempt("carrier-authorization-rejected", 1),
7211            None,
7212        );
7213    }
7214
7215    #[test]
7216    fn sends_typescript_capability_update_for_application_key_without_webrtc() {
7217        assert!(should_send_typescript_capability_update(
7218            false,
7219            false,
7220            false,
7221            true,
7222            Some("hello")
7223        ));
7224    }
7225
7226    #[test]
7227    fn application_key_reply_protocol_is_advertise_response_ack() {
7228        assert_eq!(
7229            typescript_application_key_reply_action(Some("capability-update"), true, true, false,),
7230            Some("response"),
7231        );
7232        assert_eq!(
7233            typescript_application_key_reply_action(Some("hello"), true, true, false),
7234            Some("response"),
7235        );
7236        assert_eq!(
7237            typescript_application_key_reply_action(Some("response"), true, false, false),
7238            Some("ack"),
7239        );
7240        assert_eq!(
7241            typescript_application_key_reply_action(Some("ack"), true, true, false),
7242            Some("capability-update"),
7243        );
7244        assert_eq!(
7245            typescript_application_key_reply_action(Some("ack"), true, false, true),
7246            None,
7247        );
7248        assert_eq!(
7249            typescript_application_key_reply_action(Some("capability-update"), false, false, false,),
7250            None,
7251        );
7252        assert_eq!(
7253            typescript_application_key_reply_action(Some("capability-update"), true, false, true,),
7254            Some("response"),
7255        );
7256        assert_eq!(
7257            typescript_application_key_reply_action(Some("response"), true, false, true),
7258            Some("ack"),
7259        );
7260    }
7261
7262    #[test]
7263    fn application_key_reply_preserves_persistent_native_control_route() {
7264        assert!(!application_key_reply_uses_fresh_signal("response", true));
7265        assert!(!application_key_reply_uses_fresh_signal("ack", true));
7266        assert!(application_key_reply_uses_fresh_signal("response", false));
7267        assert!(application_key_reply_uses_fresh_signal("ack", false));
7268        assert!(!application_key_reply_uses_fresh_signal(
7269            "capability-update",
7270            false,
7271        ));
7272    }
7273
7274    #[cfg(not(target_arch = "wasm32"))]
7275    #[test]
7276    fn key_only_transcript_preserves_last_canonical_transport_capability() {
7277        assert!(effective_typescript_transport_capability(
7278            false, false, true
7279        ));
7280        assert!(!effective_typescript_transport_capability(
7281            false, true, false
7282        ));
7283        assert!(!effective_typescript_transport_capability(
7284            true, false, true
7285        ));
7286        assert!(effective_typescript_transport_capability(true, true, false));
7287    }
7288
7289    #[test]
7290    fn advertises_local_transport_before_remote_capability_is_known() {
7291        assert!(should_send_typescript_capability_update(
7292            true,
7293            false,
7294            false,
7295            false,
7296            Some("hello")
7297        ));
7298        assert!(!should_send_typescript_capability_update(
7299            false,
7300            true,
7301            false,
7302            false,
7303            Some("response")
7304        ));
7305        assert!(should_send_typescript_capability_update(
7306            false, false, true, false, None
7307        ));
7308        assert!(!should_send_typescript_capability_update(
7309            false,
7310            false,
7311            false,
7312            false,
7313            Some("hello")
7314        ));
7315        assert!(!should_send_typescript_capability_update(
7316            false,
7317            false,
7318            true,
7319            false,
7320            Some("capability-update")
7321        ));
7322        assert!(!should_send_typescript_capability_update(
7323            true,
7324            false,
7325            false,
7326            true,
7327            Some("capability-update")
7328        ));
7329        assert!(!should_send_typescript_capability_update(
7330            true,
7331            true,
7332            true,
7333            true,
7334            Some("ack")
7335        ));
7336    }
7337
7338    #[cfg(not(target_arch = "wasm32"))]
7339    #[test]
7340    fn native_ble_upgrade_has_one_initiator_and_preserves_better_paths() {
7341        assert!(should_initiate_native_ble_upgrade(
7342            "node-z",
7343            "node-a",
7344            crate::client::IrohPathKind::Relay,
7345        ));
7346        assert!(should_send_typescript_capability_update(
7347            true,
7348            true,
7349            true,
7350            false,
7351            Some("hello")
7352        ));
7353        assert!(!should_initiate_native_ble_upgrade(
7354            "node-a",
7355            "node-z",
7356            crate::client::IrohPathKind::Relay,
7357        ));
7358        for path in [
7359            crate::client::IrohPathKind::DirectQuic,
7360            crate::client::IrohPathKind::DirectLan,
7361            crate::client::IrohPathKind::Ble,
7362        ] {
7363            assert!(!should_initiate_native_ble_upgrade(
7364                "node-z", "node-a", path,
7365            ));
7366        }
7367        assert!(should_initiate_native_ble_upgrade(
7368            "node-z",
7369            "node-a",
7370            crate::client::IrohPathKind::Unknown,
7371        ));
7372        assert!(should_accept_native_ble_switch_request(
7373            "node-a",
7374            "node-z",
7375            crate::client::IrohPathKind::Relay,
7376        ));
7377        assert!(!should_accept_native_ble_switch_request(
7378            "node-z",
7379            "node-a",
7380            crate::client::IrohPathKind::Relay,
7381        ));
7382        assert!(!should_accept_native_ble_switch_request(
7383            "node-a",
7384            "node-z",
7385            crate::client::IrohPathKind::Ble,
7386        ));
7387    }
7388
7389    #[cfg(all(not(target_arch = "wasm32"), feature = "transport-moq"))]
7390    #[test]
7391    fn moq_carrier_retries_one_stale_base_generation() {
7392        assert_eq!(
7393            moq_retry_attempt("carrier-base-generation-stale", 1),
7394            Some(2),
7395        );
7396        assert_eq!(moq_retry_attempt("carrier-base-generation-stale", 2), None,);
7397        assert_eq!(moq_retry_attempt("carrier-proof-failed", 1), None,);
7398        assert!(crate::iroh_connection_policy::custom_carrier_base_allows(
7399            crate::client::IrohPathKind::Moq,
7400            crate::client::IrohPathKind::Moq,
7401            true,
7402        ));
7403        assert!(!crate::iroh_connection_policy::custom_carrier_base_allows(
7404            crate::client::IrohPathKind::Moq,
7405            crate::client::IrohPathKind::Moq,
7406            false,
7407        ));
7408    }
7409
7410    #[cfg(all(not(target_arch = "wasm32"), feature = "transport-moq"))]
7411    #[test]
7412    fn moq_peer_failure_preserves_proof_code_without_broad_retry() {
7413        let (diagnostic, lifecycle) = native_moq_peer_failure_code(Some("carrier-proof-failed"));
7414        assert_eq!(diagnostic, "carrier-proof-failed");
7415        assert_eq!(lifecycle, "carrier-proof-failed");
7416        assert_eq!(moq_retry_attempt(lifecycle, 1), None);
7417
7418        let (diagnostic, lifecycle) =
7419            native_moq_peer_failure_code(Some("carrier-adapter-specific"));
7420        assert_eq!(diagnostic, "carrier-adapter-specific");
7421        assert_eq!(lifecycle, "peer-rejected-carrier");
7422    }
7423
7424    #[cfg(not(target_arch = "wasm32"))]
7425    #[test]
7426    fn native_ble_switch_waits_for_pending_admission_without_accepting_rejection() {
7427        let pending = (false, "native-main-route-pending".to_string());
7428        let rejected = (true, "session-token-rejected".to_string());
7429
7430        assert_eq!(
7431            classify_native_ble_switch_admission(None),
7432            NativeBleSwitchAdmission::Ready,
7433        );
7434        assert_eq!(
7435            classify_native_ble_switch_admission(Some(&pending)),
7436            NativeBleSwitchAdmission::Wait,
7437        );
7438        assert_eq!(
7439            classify_native_ble_switch_admission(Some(&rejected)),
7440            NativeBleSwitchAdmission::Reject,
7441        );
7442    }
7443
7444    #[cfg(not(target_arch = "wasm32"))]
7445    #[test]
7446    fn native_ble_retry_is_bounded_jittered_and_reset_by_typed_recovery_wakes() {
7447        let generation_one = crate::client::NativePeerDataGeneration {
7448            transport_stable_id: 10,
7449            transport_generation: 2,
7450            route_generation: 1,
7451        };
7452        let generation_two = crate::client::NativePeerDataGeneration {
7453            transport_stable_id: 11,
7454            transport_generation: 3,
7455            route_generation: 0,
7456        };
7457
7458        let (state, attempt) = reserve_native_ble_attempt_state(None, generation_one);
7459        assert_eq!(attempt, Some(1));
7460        assert_eq!(native_ble_retry_delay(1, 0).unwrap().as_millis(), 800);
7461        assert_eq!(native_ble_retry_delay(1, 40).unwrap().as_millis(), 1_200);
7462        let (state, attempt) = reserve_native_ble_attempt_state(Some(state), generation_one);
7463        assert_eq!(attempt, Some(2));
7464        assert_eq!(native_ble_retry_delay(2, 0).unwrap().as_millis(), 1_600);
7465        assert_eq!(native_ble_retry_delay(2, 40).unwrap().as_millis(), 2_400);
7466        let (state, attempt) = reserve_native_ble_attempt_state(Some(state), generation_one);
7467        assert_eq!(attempt, Some(3));
7468        assert_eq!(native_ble_retry_delay(3, 20), None);
7469        let (exhausted, attempt) = reserve_native_ble_attempt_state(Some(state), generation_one);
7470        assert_eq!(attempt, None);
7471        assert_eq!(exhausted.attempts, NATIVE_BLE_MAX_UPGRADE_ATTEMPTS);
7472
7473        assert_eq!(
7474            native_ble_attempt_state_after_recovery_wake(exhausted, true),
7475            Some(exhausted),
7476        );
7477        let reset_after_wake = native_ble_attempt_state_after_recovery_wake(exhausted, false);
7478        let (reset, attempt) = reserve_native_ble_attempt_state(reset_after_wake, generation_one);
7479        assert_eq!(attempt, Some(1));
7480        assert_eq!(reset.generation, generation_one);
7481
7482        let (reset, attempt) = reserve_native_ble_attempt_state(Some(exhausted), generation_two);
7483        assert_eq!(attempt, Some(1));
7484        assert_eq!(reset.generation, generation_two);
7485    }
7486
7487    #[cfg(not(target_arch = "wasm32"))]
7488    #[test]
7489    fn native_carriers_cannot_race_the_same_logical_generation() {
7490        let generation = crate::client::NativePeerDataGeneration {
7491            transport_stable_id: 10,
7492            transport_generation: 2,
7493            route_generation: 1,
7494        };
7495        let connection_id = "connection-1".to_string();
7496        let mut gates = std::collections::HashMap::new();
7497
7498        assert!(reserve_native_transport_upgrade_gate(
7499            &mut gates,
7500            (connection_id.clone(), crate::client::IrohPathKind::Moq),
7501            "moq-attempt",
7502            generation,
7503            7,
7504            0,
7505            false,
7506        ));
7507        assert!(!reserve_native_transport_upgrade_gate(
7508            &mut gates,
7509            (connection_id.clone(), crate::client::IrohPathKind::WebRtc),
7510            "webrtc-attempt",
7511            generation,
7512            7,
7513            0,
7514            false,
7515        ));
7516        assert_eq!(gates.len(), 1);
7517
7518        assert!(reserve_native_transport_upgrade_gate(
7519            &mut gates,
7520            (
7521                "connection-2".to_string(),
7522                crate::client::IrohPathKind::WebRtc,
7523            ),
7524            "unrelated-peer-policy-change",
7525            generation,
7526            7,
7527            1,
7528            false,
7529        ));
7530        assert!(!reserve_native_transport_upgrade_gate(
7531            &mut gates,
7532            (connection_id.clone(), crate::client::IrohPathKind::WebRtc),
7533            "still-fenced-for-peer-one",
7534            generation,
7535            7,
7536            0,
7537            false,
7538        ));
7539
7540        assert!(reserve_native_transport_upgrade_gate(
7541            &mut gates,
7542            (connection_id.clone(), crate::client::IrohPathKind::WebRtc),
7543            "peer-one-capability-changed",
7544            generation,
7545            7,
7546            1,
7547            false,
7548        ));
7549
7550        assert!(reserve_native_transport_upgrade_gate(
7551            &mut gates,
7552            (connection_id, crate::client::IrohPathKind::WebRtc),
7553            "policy-switched-webrtc-attempt",
7554            generation,
7555            8,
7556            0,
7557            false,
7558        ));
7559        assert_eq!(gates.len(), 3);
7560    }
7561
7562    #[cfg(not(target_arch = "wasm32"))]
7563    #[tokio::test]
7564    async fn native_ble_upgrade_gate_cleanup_is_generation_fenced() {
7565        let generation_one = crate::client::NativePeerDataGeneration {
7566            transport_stable_id: 10,
7567            transport_generation: 2,
7568            route_generation: 1,
7569        };
7570        let generation_two = crate::client::NativePeerDataGeneration {
7571            transport_stable_id: 11,
7572            transport_generation: 3,
7573            route_generation: 0,
7574        };
7575        let gates =
7576            std::sync::Arc::new(tokio::sync::Mutex::new(std::collections::HashMap::from([
7577                (
7578                    ("connection-1".to_string(), crate::client::IrohPathKind::Ble),
7579                    crate::client::NativeTransportUpgradeGate {
7580                        upgrade_id: "upgrade-new".to_string(),
7581                        generation: generation_one,
7582                        policy_epoch: 0,
7583                        peer_policy_epoch: 0,
7584                    },
7585                ),
7586            ])));
7587        let key = ("connection-1".to_string(), crate::client::IrohPathKind::Ble);
7588
7589        assert!(!remove_native_transport_upgrade_gate(&gates, &key, "upgrade-old").await);
7590        assert_eq!(
7591            gates
7592                .lock()
7593                .await
7594                .get(&key)
7595                .map(|gate| gate.upgrade_id.as_str()),
7596            Some("upgrade-new")
7597        );
7598
7599        assert!(!reserve_native_transport_upgrade_gate(
7600            &mut *gates.lock().await,
7601            key.clone(),
7602            "same-generation-retry",
7603            generation_one,
7604            0,
7605            0,
7606            false,
7607        ));
7608        assert!(reserve_native_transport_upgrade_gate(
7609            &mut *gates.lock().await,
7610            key.clone(),
7611            "same-generation-remote-retry",
7612            generation_one,
7613            0,
7614            0,
7615            true,
7616        ));
7617        assert!(reserve_native_transport_upgrade_gate(
7618            &mut *gates.lock().await,
7619            key.clone(),
7620            "replacement-generation",
7621            generation_two,
7622            0,
7623            0,
7624            false,
7625        ));
7626        assert_eq!(
7627            gates
7628                .lock()
7629                .await
7630                .get(&key)
7631                .map(|gate| (gate.upgrade_id.as_str(), gate.generation)),
7632            Some(("replacement-generation", generation_two)),
7633        );
7634
7635        assert!(reserve_native_transport_upgrade_gate(
7636            &mut *gates.lock().await,
7637            key.clone(),
7638            "policy-epoch-replacement",
7639            generation_two,
7640            1,
7641            0,
7642            false,
7643        ));
7644        assert_eq!(
7645            gates
7646                .lock()
7647                .await
7648                .get(&key)
7649                .map(|gate| (gate.upgrade_id.as_str(), gate.policy_epoch)),
7650            Some(("policy-epoch-replacement", 1)),
7651        );
7652
7653        assert!(!remove_native_transport_upgrade_gate(&gates, &key, "upgrade-new").await);
7654        assert!(
7655            !remove_native_transport_upgrade_gate(&gates, &key, "replacement-generation").await
7656        );
7657        assert!(
7658            remove_native_transport_upgrade_gate(&gates, &key, "policy-epoch-replacement").await
7659        );
7660        assert!(!gates.lock().await.contains_key(&key));
7661
7662        let completion_gate = format!("{NATIVE_BLE_COMPLETION_GATE_PREFIX}upgrade-completing");
7663        gates.lock().await.insert(
7664            key.clone(),
7665            crate::client::NativeTransportUpgradeGate {
7666                upgrade_id: completion_gate.clone(),
7667                generation: generation_two,
7668                policy_epoch: 0,
7669                peer_policy_epoch: 0,
7670            },
7671        );
7672        let completion = gates.lock().await.get(&key).cloned().unwrap();
7673        assert!(native_transport_upgrade_gate_matches(
7674            &completion,
7675            "upgrade-completing",
7676        ));
7677        assert!(
7678            !remove_native_transport_upgrade_gate(&gates, &key, "upgrade-completing").await,
7679            "the response timer must not retire an upgrade after switch-ready",
7680        );
7681        assert!(
7682            remove_native_transport_upgrade_gate(&gates, &key, completion_gate.as_str()).await,
7683            "the completion owner must retire its phase-specific gate",
7684        );
7685    }
7686
7687    #[cfg(not(target_arch = "wasm32"))]
7688    #[test]
7689    fn native_ble_switch_ready_requires_the_exact_echoed_generation() {
7690        let generation = crate::client::NativePeerDataGeneration {
7691            transport_stable_id: 10,
7692            transport_generation: 2,
7693            route_generation: 1,
7694        };
7695        let mut gate = crate::client::NativeTransportUpgradeGate {
7696            upgrade_id: "upgrade-exact".to_string(),
7697            generation,
7698            policy_epoch: 7,
7699            peer_policy_epoch: 0,
7700        };
7701        let mismatched_generation = crate::client::NativePeerDataGeneration {
7702            route_generation: generation.route_generation + 1,
7703            ..generation
7704        };
7705
7706        assert_eq!(
7707            transition_native_ble_upgrade_gate_to_completion(
7708                &mut gate,
7709                "upgrade-exact",
7710                mismatched_generation,
7711            ),
7712            None,
7713        );
7714        assert_eq!(gate.upgrade_id, "upgrade-exact");
7715        assert_eq!(gate.generation, generation);
7716        assert_eq!(gate.policy_epoch, 7);
7717
7718        assert_eq!(
7719            transition_native_ble_upgrade_gate_to_completion(
7720                &mut gate,
7721                "upgrade-exact",
7722                generation,
7723            ),
7724            Some(generation),
7725        );
7726        assert_eq!(
7727            gate.upgrade_id,
7728            format!("{NATIVE_BLE_COMPLETION_GATE_PREFIX}upgrade-exact"),
7729        );
7730        assert_eq!(gate.generation, generation);
7731        assert_eq!(gate.policy_epoch, 7);
7732    }
7733
7734    #[cfg(not(target_arch = "wasm32"))]
7735    #[test]
7736    fn native_control_stream_rank_converges_on_same_quic_stream() {
7737        let client_stream =
7738            iroh::endpoint::StreamId::new(iroh::endpoint::Side::Client, iroh::endpoint::Dir::Bi, 0);
7739        let server_stream =
7740            iroh::endpoint::StreamId::new(iroh::endpoint::Side::Server, iroh::endpoint::Dir::Bi, 0);
7741
7742        assert!(
7743            native_control_stream_rank(client_stream) < native_control_stream_rank(server_stream)
7744        );
7745    }
7746
7747    #[cfg(not(target_arch = "wasm32"))]
7748    #[test]
7749    fn native_control_stream_replacement_is_generation_fenced() {
7750        let generation_one = NativeControlStreamOwner {
7751            transport_stable_id: 10,
7752            stream_rank: 4,
7753        };
7754        let same_generation_better_stream = NativeControlStreamOwner {
7755            transport_stable_id: 10,
7756            stream_rank: 2,
7757        };
7758        let same_generation_worse_stream = NativeControlStreamOwner {
7759            transport_stable_id: 10,
7760            stream_rank: 6,
7761        };
7762        let generation_two = NativeControlStreamOwner {
7763            transport_stable_id: 11,
7764            stream_rank: 8,
7765        };
7766
7767        assert!(should_replace_native_control_stream(
7768            generation_one,
7769            same_generation_better_stream,
7770        ));
7771        assert!(!should_replace_native_control_stream(
7772            generation_one,
7773            same_generation_worse_stream,
7774        ));
7775        assert!(should_replace_native_control_stream(
7776            generation_one,
7777            generation_two,
7778        ));
7779    }
7780
7781    #[cfg(not(target_arch = "wasm32"))]
7782    #[test]
7783    fn replaced_native_control_stream_drain_is_strictly_bounded() {
7784        assert!(should_drain_replaced_native_control_frame(0));
7785        assert!(should_drain_replaced_native_control_frame(
7786            MAX_REPLACED_NATIVE_CONTROL_DRAIN_FRAMES - 1
7787        ));
7788        assert!(!should_drain_replaced_native_control_frame(
7789            MAX_REPLACED_NATIVE_CONTROL_DRAIN_FRAMES
7790        ));
7791    }
7792
7793    #[cfg(not(target_arch = "wasm32"))]
7794    #[test]
7795    fn iroh_peer_payload_uses_application_stream_framing_not_native_main() {
7796        let frame = frame_iroh_peer_application_payload(b"hello").unwrap();
7797
7798        assert_eq!(u32::from_be_bytes(frame[..4].try_into().unwrap()), 6);
7799        assert_eq!(frame[4], 0x00);
7800        assert_eq!(&frame[5..], b"hello");
7801        assert_ne!(&frame[5..], b"main");
7802    }
7803}