#![cfg(target_arch = "wasm32")]
use super::*;
use crate::{
iroh_carrier_bootstrap::CarrierBootstrapKind,
iroh_carrier_proof::{
CarrierCandidateProofFrame, CarrierCandidateProofRole, CARRIER_CANDIDATE_PROOF_TYPE,
},
};
#[derive(Debug, Clone)]
pub(crate) struct WasmIrohCarrierAuthorizationFence {
remote_admission_proof: Option<RemoteSessionAdmissionProof>,
admission_fingerprint: Option<String>,
application_security_epoch_fingerprint: Option<String>,
policy_epoch: u64,
}
fn carrier_proof_kind(kind: IrohPathKind) -> anyhow::Result<CarrierBootstrapKind> {
match kind {
IrohPathKind::WebRtc => Ok(CarrierBootstrapKind::WebRtc),
IrohPathKind::Moq => Ok(CarrierBootstrapKind::MoqDraft14),
other => anyhow::bail!("unsupported browser carrier proof kind {other:?}"),
}
}
impl Client {
pub(crate) async fn bump_wasm_iroh_carrier_peer_policy_epoch(
&self,
connection_id: &str,
) -> u64 {
let _gates = self.wasm_transport_upgrade_gates.lock().await;
self.bump_iroh_carrier_peer_policy_epoch(connection_id)
}
#[cfg(feature = "transport-webrtc")]
pub(crate) async fn set_webrtc_carrier(&self, enabled: bool, privacy_mode: bool) {
let _gates = self.wasm_transport_upgrade_gates.lock().await;
let mut config = self.transport_config.write().await;
let next = enabled.then(|| WebRTCConfig {
privacy_mode,
..WebRTCConfig::default()
});
if config.webrtc != next {
self.bump_iroh_carrier_policy_epoch();
config.webrtc = next;
}
}
pub(crate) async fn configure_wasm_route_policy(
&self,
relay_only: bool,
optimize_for: crate::route_policy::RouteOptimization,
route_priority: Vec<crate::route_policy::KnownRoute>,
) {
let _gates = self.wasm_transport_upgrade_gates.lock().await;
let mut config = self.transport_config.write().await;
let next_priority = if route_priority.is_empty() {
crate::route_policy::DEFAULT_ROUTE_PRIORITY.to_vec()
} else {
route_priority
};
if config.privacy_mode != relay_only
|| config.iroh_relay_only != relay_only
|| config.optimize_for != optimize_for
|| config.route_priority != next_priority
{
self.bump_iroh_carrier_policy_epoch();
config.privacy_mode = relay_only;
config.iroh_relay_only = relay_only;
config.optimize_for = optimize_for;
config.route_priority = next_priority;
}
}
pub(crate) async fn ranked_wasm_iroh_carriers(
&self,
remote_supports_webrtc: bool,
remote_supports_moq: bool,
) -> Vec<crate::route_policy::KnownRoute> {
let config = self.transport_config.read().await;
config.ranked_iroh_carriers(
remote_supports_webrtc && self.is_webrtc_carrier_enabled_locked(&config),
remote_supports_moq && self.is_moq_carrier_enabled_locked(&config),
)
}
pub(crate) async fn reconcile_wasm_iroh_carrier_policy(
&self,
connection_id: &str,
remote_supports_webrtc: bool,
remote_supports_moq: bool,
) -> anyhow::Result<bool> {
let ranked = self
.ranked_wasm_iroh_carriers(remote_supports_webrtc, remote_supports_moq)
.await;
let desired = ranked.first().copied();
let Some(record) = self
.connection_manager
.get_by_connection_id(connection_id)
.await
else {
return Ok(false);
};
let current = match record.active_transport.as_str() {
crate::transport_label::WEBRTC => IrohPathKind::WebRtc,
crate::transport_label::MOQ => IrohPathKind::Moq,
_ => return Ok(false),
};
if desired.is_some_and(|route| route.as_str() == current.transport_label()) {
return Ok(false);
}
if desired.is_some() {
return Ok(false);
}
let remote_endpoint_id = record
.endpoint_id
.or(record.node_id)
.ok_or_else(|| {
anyhow::anyhow!("carrier policy reconciliation requires remote endpoint id")
})?
.parse::<iroh::EndpointId>()?;
Ok(self
.close_current_iroh_carrier_generation_with_reason(
connection_id,
remote_endpoint_id,
current,
record.transport_generation,
crate::lifecycle_reason::REASON_IROH_CARRIER_POLICY_CHANGED,
)
.await)
}
pub(crate) async fn wasm_iroh_carrier_remote_endpoint_id(
&self,
connection_id: &str,
) -> Option<String> {
self.connection_manager
.get_by_connection_id(connection_id)
.await
.and_then(|record| record.endpoint_id.or(record.node_id))
}
fn is_webrtc_carrier_enabled_locked(&self, config: &TransportConfig) -> bool {
config.webrtc.as_ref().is_some()
}
fn is_moq_carrier_enabled_locked(&self, config: &TransportConfig) -> bool {
config
.moq
.as_ref()
.is_some_and(|moq| !moq.relay_url.trim().is_empty())
}
pub(crate) async fn is_webrtc_carrier_configured(&self) -> bool {
self.transport_config.read().await.webrtc.is_some()
}
pub(crate) async fn is_moq_carrier_configured(&self) -> bool {
self.transport_config
.read()
.await
.moq
.as_ref()
.is_some_and(|config| !config.relay_url.trim().is_empty())
}
pub async fn is_webrtc_carrier_enabled(&self) -> bool {
self.transport_config.read().await.webrtc.as_ref().is_some()
&& self
.iroh_packet_carriers
.read()
.await
.contains_key(&crate::iroh_carrier_kind::IrohCarrierKind::WebRtc)
}
#[cfg(feature = "transport-moq")]
pub(crate) async fn set_moq_carrier(&self, enabled: bool, relay_url: Option<String>) {
let _gates = self.wasm_transport_upgrade_gates.lock().await;
let mut config = self.transport_config.write().await;
let next = enabled.then(|| MoQConfig {
relay_url: relay_url.unwrap_or_default(),
access_token: None,
});
if config.moq != next {
self.bump_iroh_carrier_policy_epoch();
config.moq = next;
}
}
pub async fn is_moq_carrier_enabled(&self) -> bool {
self.transport_config
.read()
.await
.moq
.as_ref()
.is_some_and(|config| !config.relay_url.trim().is_empty())
&& self
.iroh_packet_carriers
.read()
.await
.contains_key(&crate::iroh_carrier_kind::IrohCarrierKind::Moq)
}
pub(crate) async fn reserve_wasm_carrier_upgrade(
&self,
connection_id: &str,
kind: IrohPathKind,
upgrade_id: &str,
generation: WasmPeerDataGeneration,
) -> WasmCarrierUpgradeReservation {
let key = (connection_id.to_string(), kind);
let mut gates = self.wasm_transport_upgrade_gates.lock().await;
let policy_epoch = self.current_iroh_carrier_policy_epoch();
let peer_policy_epoch = self.current_iroh_carrier_peer_policy_epoch(connection_id);
let current = gates
.get(&key)
.filter(|current| current.peer_policy_epoch == peer_policy_epoch)
.map(|current| {
(
current.upgrade_id.as_str(),
(
current.generation.transport_stable_id,
current.generation.transport_generation,
current.generation.route_generation,
),
current.policy_epoch,
)
});
let competing_kind_current =
gates
.iter()
.any(|((current_connection_id, current_kind), current)| {
current_connection_id == connection_id
&& *current_kind != kind
&& current.generation == generation
&& current.policy_epoch == policy_epoch
&& current.peer_policy_epoch == peer_policy_epoch
});
match crate::client::classify_wasm_carrier_upgrade_reservation(
current,
competing_kind_current,
upgrade_id,
(
generation.transport_stable_id,
generation.transport_generation,
generation.route_generation,
),
policy_epoch,
) {
WasmCarrierUpgradeReservation::Reserved => {
gates.insert(
key,
crate::client::WasmTransportUpgradeGate {
upgrade_id: upgrade_id.to_string(),
generation,
policy_epoch,
peer_policy_epoch,
},
);
WasmCarrierUpgradeReservation::Reserved
}
other => other,
}
}
pub(crate) async fn wasm_carrier_upgrade_is_current(
&self,
connection_id: &str,
kind: IrohPathKind,
upgrade_id: &str,
generation: WasmPeerDataGeneration,
) -> bool {
matches!(
self.wasm_carrier_upgrade_fence(connection_id, kind, upgrade_id, generation)
.await,
WasmCarrierUpgradeFence::Current(_)
)
}
pub(crate) async fn wasm_carrier_upgrade_fence(
&self,
connection_id: &str,
kind: IrohPathKind,
upgrade_id: &str,
generation: WasmPeerDataGeneration,
) -> WasmCarrierUpgradeFence {
let gates = self.wasm_transport_upgrade_gates.lock().await;
let current_policy_epoch = self.current_iroh_carrier_policy_epoch();
let current_peer_policy_epoch = self.current_iroh_carrier_peer_policy_epoch(connection_id);
let gate = gates.get(&(connection_id.to_string(), kind)).cloned();
if gate
.as_ref()
.is_some_and(|gate| gate.peer_policy_epoch != current_peer_policy_epoch)
{
return WasmCarrierUpgradeFence::PolicyChanged;
}
let current = gate.as_ref().map(|current| {
(
current.upgrade_id.as_str(),
(
current.generation.transport_stable_id,
current.generation.transport_generation,
current.generation.route_generation,
),
current.policy_epoch,
)
});
crate::client::classify_wasm_carrier_upgrade_fence(
current,
upgrade_id,
(
generation.transport_stable_id,
generation.transport_generation,
generation.route_generation,
),
current_policy_epoch,
)
}
pub(crate) async fn require_current_wasm_carrier_upgrade_fence(
&self,
connection_id: &str,
kind: IrohPathKind,
upgrade_id: &str,
generation: WasmPeerDataGeneration,
) -> anyhow::Result<u64> {
match self
.wasm_carrier_upgrade_fence(connection_id, kind, upgrade_id, generation)
.await
{
WasmCarrierUpgradeFence::Current(policy_epoch) => Ok(policy_epoch),
WasmCarrierUpgradeFence::Retired => {
anyhow::bail!("browser {kind:?} carrier attempt retired before proof")
}
WasmCarrierUpgradeFence::Superseded => {
anyhow::bail!("browser {kind:?} carrier attempt superseded before proof")
}
WasmCarrierUpgradeFence::PolicyChanged => {
anyhow::bail!("browser {kind:?} carrier policy changed before proof")
}
WasmCarrierUpgradeFence::BaseGenerationChanged => {
anyhow::bail!("browser {kind:?} carrier base generation changed before proof")
}
}
}
pub(crate) async fn retire_wasm_carrier_upgrade(
&self,
connection_id: &str,
kind: IrohPathKind,
upgrade_id: &str,
generation: WasmPeerDataGeneration,
) -> bool {
let key = (connection_id.to_string(), kind);
let mut gates = self.wasm_transport_upgrade_gates.lock().await;
if gates.get(&key).is_some_and(|current| {
current.upgrade_id == upgrade_id && current.generation == generation
}) {
gates.remove(&key);
true
} else {
false
}
}
pub(crate) fn wasm_candidate_proof_probe(
&self,
connection_id: &str,
upgrade_id: &str,
base_transport_generation: u64,
base_route_generation: u64,
kind: IrohPathKind,
transport_id: u64,
) -> anyhow::Result<CarrierCandidateProofFrame> {
anyhow::ensure!(
self.application_crypto_key_for_connection(Some(connection_id))
.is_some(),
"carrier candidate proof requires an installed application-crypto key"
);
CarrierCandidateProofFrame::probe(
carrier_proof_kind(kind)?,
transport_id,
upgrade_id,
base_transport_generation,
base_route_generation,
)
.map_err(Into::into)
}
async fn prove_outbound_wasm_carrier_candidate(
&self,
connection_id: &str,
candidate: &crate::wasm_node::PendingReplacementConnection,
probe: &CarrierCandidateProofFrame,
) -> anyhow::Result<CarrierCandidateProofFrame> {
let (send, recv) = candidate.open_bi().await?;
let (mut send, mut recv) =
self.wrap_peer_streams_for_connection(Some(connection_id), send, recv)?;
send.write_all(&serde_json::to_vec(probe)?).await?;
send.finish()?;
let payload = crate::iroh_carrier_proof::read_candidate_proof_stream(&mut recv).await?;
let ack: CarrierCandidateProofFrame = serde_json::from_slice(&payload)?;
anyhow::ensure!(
ack.matches_probe(probe),
"carrier candidate proof acknowledgement did not match the current probe"
);
Ok(probe.clone())
}
pub(crate) async fn receive_inbound_wasm_carrier_candidate_proof(
&self,
connection_id: &str,
candidate: &crate::wasm_node::PendingReplacementConnection,
expected_probe: &CarrierCandidateProofFrame,
) -> anyhow::Result<(
crate::application_crypto_streams::PeerSendStream,
CarrierCandidateProofFrame,
CarrierCandidateProofFrame,
)> {
let (send, recv) = candidate.accept_proof_bi().await?;
let (send, mut recv) =
self.wrap_peer_streams_for_connection(Some(connection_id), send, recv)?;
let payload = crate::iroh_carrier_proof::read_candidate_proof_stream(&mut recv).await?;
let probe: CarrierCandidateProofFrame = serde_json::from_slice(&payload)?;
anyhow::ensure!(
probe.frame_type == CARRIER_CANDIDATE_PROOF_TYPE
&& probe.role == CarrierCandidateProofRole::Probe
&& probe.carrier == expected_probe.carrier
&& probe.transport_id == expected_probe.transport_id
&& probe.upgrade_id == expected_probe.upgrade_id
&& probe.base_transport_generation == expected_probe.base_transport_generation
&& probe.base_route_generation == expected_probe.base_route_generation,
"carrier candidate proof does not match the authorized browser attempt"
);
probe.validate()?;
let ack = CarrierCandidateProofFrame::ack_from(&probe)?;
Ok((send, probe, ack))
}
pub(crate) async fn send_inbound_wasm_carrier_candidate_ack(
&self,
mut send: crate::application_crypto_streams::PeerSendStream,
ack: &CarrierCandidateProofFrame,
) -> anyhow::Result<()> {
send.write_all(&serde_json::to_vec(&ack)?).await?;
send.finish_and_wait_for_peer(
crate::iroh_carrier_proof::CARRIER_CANDIDATE_PROOF_DELIVERY_TIMEOUT,
)
.await?;
Ok(())
}
pub(crate) async fn receive_inbound_wasm_carrier_commit(
&self,
connection_id: &str,
candidate: &crate::wasm_node::PendingReplacementConnection,
probe: &CarrierCandidateProofFrame,
) -> anyhow::Result<(
crate::application_crypto_streams::PeerSendStream,
CarrierCandidateProofFrame,
)> {
let (send, recv) = candidate.accept_proof_bi().await?;
let (send, mut recv) =
self.wrap_peer_streams_for_connection(Some(connection_id), send, recv)?;
let payload = crate::iroh_carrier_proof::read_candidate_proof_stream(&mut recv).await?;
let commit: CarrierCandidateProofFrame = serde_json::from_slice(&payload)?;
anyhow::ensure!(
commit.matches_commit_for_probe(probe),
"carrier commit does not match the proven browser candidate"
);
let committed = CarrierCandidateProofFrame::committed_from(&commit)?;
Ok((send, committed))
}
async fn confirm_outbound_wasm_carrier_commit(
&self,
connection_id: &str,
candidate: &crate::wasm_node::PendingReplacementConnection,
probe: &CarrierCandidateProofFrame,
) -> anyhow::Result<()> {
let commit = CarrierCandidateProofFrame::commit_from(probe)?;
let (send, recv) = candidate.open_bi().await?;
let (mut send, mut recv) =
self.wrap_peer_streams_for_connection(Some(connection_id), send, recv)?;
send.write_all(&serde_json::to_vec(&commit)?).await?;
send.finish()?;
let payload = crate::iroh_carrier_proof::read_candidate_proof_stream(&mut recv).await?;
let committed: CarrierCandidateProofFrame = serde_json::from_slice(&payload)?;
anyhow::ensure!(
committed.matches_committed_for_commit(&commit),
"carrier commit acknowledgement does not match the browser candidate"
);
Ok(())
}
pub(crate) fn capture_wasm_iroh_carrier_authorization_fence(
&self,
connection_id: &str,
expected_generation: WasmPeerDataGeneration,
kind: IrohPathKind,
policy_epoch: u64,
) -> anyhow::Result<WasmIrohCarrierAuthorizationFence> {
anyhow::ensure!(
matches!(
self.session_admission(connection_id),
crate::session_token::SessionAdmission::Accepted { .. }
) && self
.application_crypto_key_for_connection(Some(connection_id))
.is_some(),
"browser {kind:?} replacement base is not authorized for application traffic"
);
Ok(WasmIrohCarrierAuthorizationFence {
remote_admission_proof: self
.remote_session_admission_proofs
.read()
.ok()
.and_then(|proofs| proofs.get(connection_id).cloned())
.filter(|proof| {
proof.transport_stable_id == expected_generation.transport_stable_id
}),
admission_fingerprint: self
.session_token_registry
.admission_fingerprint(connection_id),
application_security_epoch_fingerprint: self
.session_token_registry
.application_security_epoch_fingerprint(connection_id),
policy_epoch,
})
}
fn wasm_iroh_carrier_authorization_epoch_is_current(
&self,
connection_id: &str,
authorization_fence: &WasmIrohCarrierAuthorizationFence,
) -> bool {
matches!(
self.session_admission(connection_id),
crate::session_token::SessionAdmission::Accepted { .. }
) && self
.session_token_registry
.admission_fingerprint(connection_id)
== authorization_fence.admission_fingerprint
&& self
.session_token_registry
.application_security_epoch_fingerprint(connection_id)
== authorization_fence.application_security_epoch_fingerprint
&& self.iroh_carrier_policy_epoch_is_current(authorization_fence.policy_epoch)
}
pub(crate) async fn commit_proven_wasm_carrier_candidate(
&self,
connection_id: &str,
upgrade_id: &str,
expected_generation: WasmPeerDataGeneration,
kind: IrohPathKind,
authorization_fence: WasmIrohCarrierAuthorizationFence,
candidate: crate::wasm_node::PendingReplacementConnection,
) -> anyhow::Result<
crate::wasm_node::CommittedReplacement<crate::connection_manager::ConnectionRecord>,
> {
if !self
.wasm_carrier_upgrade_is_current(connection_id, kind, upgrade_id, expected_generation)
.await
{
candidate.close(b"replacement-upgrade-retired");
anyhow::bail!("browser carrier replacement belongs to a retired upgrade");
}
let authorization_fence_for_commit = authorization_fence.clone();
let remote_admission_proof = authorization_fence.remote_admission_proof;
let replacement_transport_stable_id = candidate.transport_stable_id();
let node = self
.iroh_node
.read()
.await
.as_ref()
.cloned()
.ok_or_else(|| anyhow::anyhow!("Iroh node is unavailable"))?;
let client = self.clone();
let connection_id_for_commit = connection_id.to_string();
let upgrade_id_for_commit = upgrade_id.to_string();
let transport_source = Some(
format!("iroh-carrier-{kind:?}-upgrade")
.to_ascii_lowercase()
.replace(['(', ')'], ""),
);
let committed_replacement = node
.commit_replacement_candidate(
candidate,
expected_generation.transport_stable_id,
move |replacement_transport_stable_id| async move {
let gates = client.wasm_transport_upgrade_gates.lock().await;
let current_policy_epoch = client.current_iroh_carrier_policy_epoch();
let current_peer_policy_epoch = client
.current_iroh_carrier_peer_policy_epoch(&connection_id_for_commit);
let gate = gates.get(&(connection_id_for_commit.clone(), kind));
let current = gate.map(|current| {
(
current.upgrade_id.as_str(),
(
current.generation.transport_stable_id,
current.generation.transport_generation,
current.generation.route_generation,
),
current.policy_epoch,
)
});
anyhow::ensure!(
gate.is_some_and(|gate| {
gate.peer_policy_epoch == current_peer_policy_epoch
&& matches!(
crate::client::classify_wasm_carrier_upgrade_fence(
current,
&upgrade_id_for_commit,
(
expected_generation.transport_stable_id,
expected_generation.transport_generation,
expected_generation.route_generation,
),
current_policy_epoch,
),
WasmCarrierUpgradeFence::Current(_)
)
}),
"browser {kind:?} replacement upgrade changed before atomic commit"
);
anyhow::ensure!(
client.wasm_iroh_carrier_authorization_epoch_is_current(
&connection_id_for_commit,
&authorization_fence_for_commit,
),
"browser {kind:?} replacement authorization epoch changed before atomic commit"
);
let installed = client
.begin_crypto_bound_transport_replacement_commit(
&connection_id_for_commit,
expected_generation.transport_stable_id,
expected_generation.transport_generation,
expected_generation.route_generation,
replacement_transport_stable_id,
transport_source,
)
.await;
drop(gates);
installed.ok_or_else(|| {
anyhow::anyhow!("{kind:?} replacement is stale before atomic commit")
})
},
)
.await?;
let committed = committed_replacement.logical_result();
anyhow::ensure!(
committed.transport_stable_id == Some(replacement_transport_stable_id)
&& committed.transport_generation
== expected_generation.transport_generation.saturating_add(1),
"browser {kind:?} replacement became stale during atomic commit"
);
if let Some(mut proof) = remote_admission_proof {
proof.transport_stable_id = replacement_transport_stable_id;
if let Ok(mut proofs) = self.remote_session_admission_proofs.write() {
proofs.insert(connection_id.to_string(), proof);
}
}
Ok(committed_replacement)
}
pub(crate) async fn publish_committed_wasm_carrier_route(
&self,
connection_id: &str,
kind: IrohPathKind,
replacement_transport_stable_id: u64,
) -> anyhow::Result<()> {
let snapshot = self
.report_transport_status_for_current_generation(
connection_id,
kind.transport_label(),
None,
)
.await
.ok_or_else(|| {
anyhow::anyhow!(
"browser {kind:?} replacement could not publish its committed route"
)
})?;
anyhow::ensure!(
snapshot.active_transport_stable_id == Some(replacement_transport_stable_id),
"browser {kind:?} replacement route belongs to a retired generation"
);
self.emit_current_wasm_connection_state(connection_id).await;
Ok(())
}
pub(crate) async fn complete_outbound_wasm_carrier_upgrade(
&self,
connection_id: &str,
remote_node_id: &str,
upgrade_id: &str,
expected_generation: WasmPeerDataGeneration,
kind: IrohPathKind,
) -> anyhow::Result<()> {
anyhow::ensure!(
matches!(kind, IrohPathKind::WebRtc | IrohPathKind::Moq),
"unsupported browser Iroh carrier kind: {kind:?}"
);
web_sys::console::debug_1(&wasm_bindgen::JsValue::from_str(&format!(
"[OpenRTC][Iroh carrier][candidate] browser outbound start connection_id={connection_id} upgrade_id={upgrade_id} carrier={}",
kind.transport_label(),
)));
let policy_epoch = self
.require_current_wasm_carrier_upgrade_fence(
connection_id,
kind,
upgrade_id,
expected_generation,
)
.await?;
let endpoint_id = remote_node_id.parse::<iroh::EndpointId>()?;
let transport_id = match kind {
IrohPathKind::WebRtc => crate::iroh_carrier_kind::EXPERIMENTAL_WEBRTC_TRANSPORT_ID,
IrohPathKind::Moq => crate::iroh_carrier_kind::EXPERIMENTAL_MOQ_TRANSPORT_ID,
_ => unreachable!(),
};
let carrier_kind = match kind {
IrohPathKind::WebRtc => crate::iroh_carrier_kind::IrohCarrierKind::WebRtc,
IrohPathKind::Moq => crate::iroh_carrier_kind::IrohCarrierKind::Moq,
_ => unreachable!(),
};
let endpoint_addr = self
.active_iroh_packet_carrier_addr(carrier_kind, endpoint_id)
.await?;
web_sys::console::debug_1(&wasm_bindgen::JsValue::from_str(&format!(
"[OpenRTC][Iroh carrier][candidate] browser outbound address ready connection_id={connection_id} upgrade_id={upgrade_id} carrier={}",
kind.transport_label(),
)));
let node = self
.iroh_node
.read()
.await
.as_ref()
.cloned()
.ok_or_else(|| anyhow::anyhow!("Iroh node is unavailable"))?;
web_sys::console::debug_1(&wasm_bindgen::JsValue::from_str(&format!(
"[OpenRTC][Iroh carrier][candidate] browser outbound dial start connection_id={connection_id} upgrade_id={upgrade_id} carrier={}",
kind.transport_label(),
)));
let candidate = node
.dial_replacement_candidate(endpoint_id, endpoint_addr, transport_id)
.await?;
web_sys::console::debug_1(&wasm_bindgen::JsValue::from_str(&format!(
"[OpenRTC][Iroh carrier][candidate] browser outbound dial ready connection_id={connection_id} upgrade_id={upgrade_id} carrier={}",
kind.transport_label(),
)));
let current_transport_stable_id = self
.get_connection(endpoint_id)
.await
.map(|connection| crate::transport_generation::for_connection(&connection));
if current_transport_stable_id != Some(expected_generation.transport_stable_id) {
candidate.close(b"replacement-base-generation-stale");
anyhow::bail!(
"browser carrier incumbent generation changed before proof: expected={} current={current_transport_stable_id:?}",
expected_generation.transport_stable_id,
);
}
if let Err(error) = self
.require_current_wasm_carrier_upgrade_fence(
connection_id,
kind,
upgrade_id,
expected_generation,
)
.await
{
candidate.close(b"replacement-upgrade-retired");
return Err(error);
}
let authorization_fence = self.capture_wasm_iroh_carrier_authorization_fence(
connection_id,
expected_generation,
kind,
policy_epoch,
)?;
let proof = self.wasm_candidate_proof_probe(
connection_id,
upgrade_id,
expected_generation.transport_generation,
expected_generation.route_generation,
kind,
transport_id,
)?;
let proof = self
.prove_outbound_wasm_carrier_candidate(connection_id, &candidate, &proof)
.await?;
self.confirm_outbound_wasm_carrier_commit(connection_id, &candidate, &proof)
.await?;
let committed_replacement = self
.commit_proven_wasm_carrier_candidate(
connection_id,
upgrade_id,
expected_generation,
kind,
authorization_fence,
candidate,
)
.await?;
let replacement_transport_stable_id = committed_replacement
.logical_result()
.transport_stable_id
.ok_or_else(|| anyhow::anyhow!("browser {kind:?} replacement has no stable ID"))?;
committed_replacement.finish(b"wasm-custom-transport-upgrade");
self.publish_committed_wasm_carrier_route(
connection_id,
kind,
replacement_transport_stable_id,
)
.await?;
Ok(())
}
}