1#![allow(deprecated)]
2
3pub mod application_crypto;
4pub mod application_crypto_streams;
5pub mod broadcast;
6pub mod client;
7pub mod coordination;
8pub mod datagrams;
9pub mod explicit_transfer_crypto;
10pub(crate) mod generated;
11pub mod heartbeat;
12#[cfg(feature = "iroh-carrier-core")]
13pub mod iroh_carrier;
14#[cfg(feature = "iroh-carrier-core")]
15pub mod iroh_carrier_bootstrap;
16pub mod iroh_carrier_kind;
19#[cfg(feature = "iroh-carrier-core")]
20pub mod iroh_carrier_proof;
21pub(crate) mod iroh_connection_policy;
22pub mod key_agreement;
23pub mod lifecycle_reason;
24#[cfg(feature = "managed-group-encryption")]
25#[allow(
26 dead_code,
27 reason = "gated managed-room controller; consumed only by managed gateway adapters"
28)]
29pub(crate) mod managed_group_controller;
30#[cfg(feature = "managed-group-encryption")]
31#[allow(
32 dead_code,
33 reason = "gated managed-room crypto owner; enabled only after the tracked persistence and router milestones"
34)]
35pub(crate) mod managed_group_crypto;
36pub mod media;
37pub mod native_protocol;
38pub mod offline;
39#[cfg(feature = "iroh-carrier-core")]
40pub mod packet_carrier_transport;
41pub mod presence;
42pub(crate) mod presence_policy;
43pub mod protocol_config;
44pub mod route_policy;
45pub mod runtime_policy;
46pub mod service_errors;
47pub mod session_token;
48pub mod signaling;
49pub mod sparse_fanout;
50pub mod stream_metadata;
51pub(crate) mod transport_generation;
52pub(crate) mod transport_label;
53
54#[cfg(all(not(target_arch = "wasm32"), feature = "transport-lan"))]
55pub mod local_discovery;
56
57#[cfg(test)]
61pub mod test_constants {
62 pub const TEST_PROJECT_ID: &str = "test-project";
63 pub const TEST_API_KEY: &str = "pk_test_0000000000000000000000000000000000000000";
64}
65
66pub const LIVE_PROJECT_ID: &str = "pluto-rtc-prod";
68
69pub fn validate_api_key(api_key: &str) -> anyhow::Result<&str> {
73 let trimmed = api_key.trim();
74 let valid_prefix = trimmed.starts_with("pk_live_") || trimmed.starts_with("pk_test_");
75 let suffix = trimmed.get(8..).unwrap_or_default();
76 if !valid_prefix || suffix.len() != 40 || !suffix.bytes().all(|byte| byte.is_ascii_hexdigit()) {
77 anyhow::bail!("OpenRTC 2.0 requires a public pk_live_ or pk_test_ API key");
78 }
79 Ok(trimmed)
80}
81
82pub fn app_tag_from_api_key(api_key: &str) -> String {
83 let trimmed = api_key.trim();
84 if trimmed.is_empty() {
85 return "app_anonymous".to_string();
86 }
87
88 let suffix_len = trimmed.len().min(16);
89 format!("app_{}", &trimmed[trimmed.len() - suffix_len..])
90}
91
92pub fn space_app_tag(api_key: &str, space_key: &str) -> String {
93 let input = format!("{}:{}", api_key.trim(), space_key.trim());
94 let digest = <sha2::Sha256 as sha2::Digest>::digest(input.as_bytes());
95 format!("space::{}", hex::encode(digest))
96}
97
98#[cfg(test)]
99mod constructor_contract_tests {
100 use super::*;
101
102 #[test]
103 fn rust_constructor_is_provider_neutral_and_side_effect_free() {
104 let api_key = test_constants::TEST_API_KEY;
105 let client = client::Client::new(api_key.to_string()).expect("valid public API key");
106
107 assert_eq!(client.app_tag(), app_tag_from_api_key(api_key));
108 assert!(client::Client::new("firebase-project-id".to_string()).is_err());
109 }
110}
111
112#[cfg(not(target_arch = "wasm32"))]
113pub fn ensure_rustls() {
114 if rustls::crypto::CryptoProvider::get_default().is_none() {
115 let _ = rustls::crypto::ring::default_provider().install_default();
116 }
117}
118
119#[cfg(not(target_arch = "wasm32"))]
120pub mod adapters;
121
122#[cfg(not(target_arch = "wasm32"))]
123pub(crate) mod native_coordination_gateway;
124
125#[cfg(not(target_arch = "wasm32"))]
126pub mod native;
127
128#[cfg(not(target_arch = "wasm32"))]
129mod native_key_store;
130
131pub use client::Client;
132pub mod ticket_mesh;
133#[cfg(not(target_arch = "wasm32"))]
134pub use native::ControlPlane;
135#[cfg(not(target_arch = "wasm32"))]
136pub use ticket_mesh::TicketMesh;
137pub use ticket_mesh::{TicketMeshError, TicketMeshErrorCode, TicketMeshOptions};
138
139pub mod connection_manager;
140
141pub mod protocol_registry;
142
143#[cfg(not(target_arch = "wasm32"))]
144pub mod runtime_manager;
145
146#[cfg(not(target_arch = "wasm32"))]
147pub mod transport;
148
149#[cfg(not(target_arch = "wasm32"))]
150pub use client::EndpointHandle;
151
152#[cfg(all(
153 not(target_arch = "wasm32"),
154 not(any(target_os = "ios", target_os = "android"))
155))]
156pub mod sso;
157
158#[cfg(not(target_arch = "wasm32"))]
159pub mod native_node;
160
161#[cfg(not(target_arch = "wasm32"))]
162pub mod native_device;
163
164#[cfg(all(
165 test,
166 not(target_arch = "wasm32"),
167 feature = "iroh-protocols-wasm",
168 any(feature = "transport-webrtc", feature = "transport-moq")
169))]
170mod native_carrier_protocol_test;
171#[cfg(all(not(target_arch = "wasm32"), feature = "transport-moq"))]
172pub mod native_moq_carrier;
173#[cfg(all(not(target_arch = "wasm32"), feature = "transport-webrtc"))]
174pub mod native_webrtc_carrier;
175
176#[cfg(all(target_arch = "wasm32", feature = "iroh-protocols-wasm"))]
177mod wasm_docs_persistence;
178#[cfg(all(target_arch = "wasm32", feature = "iroh-protocols-wasm"))]
179mod wasm_indexeddb_blob_store;
180#[cfg(all(target_arch = "wasm32", feature = "transport-moq"))]
181pub mod wasm_moq_carrier;
182#[cfg(target_arch = "wasm32")]
183pub mod wasm_node;
184#[cfg(all(target_arch = "wasm32", feature = "transport-webrtc"))]
185pub mod wasm_webrtc_carrier;
186
187#[cfg(target_arch = "wasm32")]
188#[macro_export]
189macro_rules! console_log {
190 ($($t:tt)*) => (web_sys::console::log_1(&wasm_bindgen::JsValue::from_str(&format_args!($($t)*).to_string())))
191}
192
193#[cfg(target_arch = "wasm32")]
195pub mod wasm_api {
196 use crate::client::Client;
197 use crate::session_token::split_ticket;
198 use crate::wasm_node::{peer_uni_stream_from_send, BiStream, PeerUniStream};
199 use iroh_tickets::endpoint::EndpointTicket;
200 use std::cell::RefCell;
201 use std::collections::{HashMap, HashSet};
202 use std::rc::Rc;
203 use std::str::FromStr;
204 use std::sync::{Arc, Mutex};
205 use wasm_bindgen::prelude::*;
206 #[cfg(any(feature = "transport-webrtc", feature = "transport-moq"))]
207 use wasm_bindgen_futures::spawn_local;
208 use wasm_streams::readable::sys::ReadableStream as JsReadableStream;
209
210 async fn send_native_signal_control_frame(
211 inner: &Client,
212 endpoint_id: String,
213 frame: Vec<u8>,
214 ) -> Result<(), JsValue> {
215 let endpoint_id: iroh::EndpointId = endpoint_id
216 .parse()
217 .map_err(|error| JsValue::from_str(&format!("{error}")))?;
218 inner
219 .ensure_connection_manager_record_before_peer_stream(&endpoint_id)
220 .await
221 .map_err(|error| JsValue::from_str(&error.to_string()))?;
222 let (mut send, _recv) = inner
223 .open_bi_internal(endpoint_id)
224 .await
225 .map_err(|error| JsValue::from_str(&error.to_string()))?;
226 send.write_all(&[0x00])
227 .await
228 .map_err(|error| JsValue::from_str(&error.to_string()))?;
229 send.write_all(&(b"signal".len() as u32).to_be_bytes())
230 .await
231 .map_err(|error| JsValue::from_str(&error.to_string()))?;
232 send.write_all(b"signal")
233 .await
234 .map_err(|error| JsValue::from_str(&error.to_string()))?;
235 send.write_all(&frame)
236 .await
237 .map_err(|error| JsValue::from_str(&error.to_string()))?;
238 crate::application_crypto_streams::PeerSendStream::plain(send)
239 .finish_and_wait_for_peer(std::time::Duration::from_secs(2))
240 .await
241 .map_err(|error| JsValue::from_str(&error.to_string()))
242 }
243
244 #[cfg(feature = "transport-webrtc")]
245 #[derive(Debug, Clone)]
246 struct WasmWebRtcCarrierAttempt {
247 connection_id: String,
248 remote_endpoint_id: iroh::EndpointId,
249 bootstrap: crate::iroh_carrier_bootstrap::CarrierBootstrapFrame,
250 generation: crate::client::WasmPeerDataGeneration,
251 role: &'static str,
252 prepared: bool,
253 retry_sent: bool,
254 offer_started: bool,
255 remote_ready: bool,
256 completion_started: bool,
257 retry_count: u8,
258 inbound_authorization_expires_at_ms: Option<f64>,
259 }
260
261 #[cfg(feature = "transport-moq")]
262 #[derive(Debug, Clone)]
263 struct WasmMoqCarrierAttempt {
264 connection_id: String,
265 remote_endpoint_id: iroh::EndpointId,
266 bootstrap: crate::iroh_carrier_bootstrap::CarrierBootstrapFrame,
267 generation: crate::client::WasmPeerDataGeneration,
268 role: &'static str,
269 prepared: bool,
270 retry_sent: bool,
271 retry_count: u8,
272 inbound_authorization_expires_at_ms: Option<f64>,
273 }
274
275 #[cfg(any(feature = "transport-webrtc", feature = "transport-moq"))]
276 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
277 struct WasmRemoteCarrierCapabilities {
278 webrtc: bool,
279 moq: bool,
280 }
281
282 #[wasm_bindgen]
283 pub struct WasmPeerDatagramPolicy {
284 inner: RefCell<crate::datagrams::PeerDatagramPolicy>,
285 }
286
287 #[wasm_bindgen]
288 impl WasmPeerDatagramPolicy {
289 #[wasm_bindgen(js_name = setIncomingMaxAge)]
290 pub fn set_incoming_max_age(&self, value: Option<u32>, now_ms: f64) {
291 self.inner
292 .borrow_mut()
293 .set_incoming_max_age_ms(value.map(u64::from), now_ms.max(0.0) as u64);
294 }
295
296 #[wasm_bindgen(js_name = setOutgoingMaxAge)]
297 pub fn set_outgoing_max_age(&self, value: Option<u32>) {
298 self.inner
299 .borrow_mut()
300 .set_outgoing_max_age_ms(value.map(u64::from));
301 }
302
303 #[wasm_bindgen(js_name = setIncomingMaxBufferedDatagrams)]
304 pub fn set_incoming_max_buffered_datagrams(&self, value: u32) -> Result<(), JsValue> {
305 self.inner
306 .borrow_mut()
307 .set_incoming_max_buffered(value as usize)
308 .map_err(|error| JsValue::from_str(&error.to_string()))
309 }
310
311 #[wasm_bindgen(js_name = setOutgoingMaxBufferedDatagrams)]
312 pub fn set_outgoing_max_buffered_datagrams(&self, value: u32) -> Result<(), JsValue> {
313 self.inner
314 .borrow_mut()
315 .set_outgoing_max_buffered(value as usize)
316 .map_err(|error| JsValue::from_str(&error.to_string()))
317 }
318
319 #[wasm_bindgen(js_name = pushIncoming)]
320 pub fn push_incoming(&self, payload: Vec<u8>, now_ms: f64) -> Result<(), JsValue> {
321 self.inner
322 .borrow_mut()
323 .push_incoming(payload, now_ms.max(0.0) as u64)
324 .map_err(|error| JsValue::from_str(&error.to_string()))
325 }
326
327 #[wasm_bindgen(js_name = popIncoming)]
328 pub fn pop_incoming(&self, now_ms: f64) -> Option<Vec<u8>> {
329 self.inner.borrow_mut().pop_incoming(now_ms.max(0.0) as u64)
330 }
331
332 #[wasm_bindgen(js_name = pushOutgoing)]
333 pub fn push_outgoing(&self, payload: Vec<u8>, now_ms: f64) -> Result<bool, JsValue> {
334 self.inner
335 .borrow_mut()
336 .push_outgoing(payload, now_ms.max(0.0) as u64)
337 .map_err(|error| JsValue::from_str(&error.to_string()))
338 }
339
340 #[wasm_bindgen(js_name = popOutgoing)]
341 pub fn pop_outgoing(&self, now_ms: f64) -> Result<JsValue, JsValue> {
342 let Some(outgoing) = self.inner.borrow_mut().pop_outgoing(now_ms.max(0.0) as u64)
343 else {
344 return Ok(JsValue::NULL);
345 };
346 let result = js_sys::Object::new();
347 js_sys::Reflect::set(
348 &result,
349 &JsValue::from_str("payload"),
350 &js_sys::Uint8Array::from(outgoing.payload.as_slice()),
351 )?;
352 js_sys::Reflect::set(
353 &result,
354 &JsValue::from_str("remainingMaxAgeMs"),
355 &outgoing
356 .remaining_max_age_ms
357 .map(|value| JsValue::from_f64(value as f64))
358 .unwrap_or(JsValue::NULL),
359 )?;
360 Ok(result.into())
361 }
362
363 #[wasm_bindgen(js_name = recordSent)]
364 pub fn record_sent(&self, bytes: usize) {
365 self.inner.borrow_mut().record_sent(bytes);
366 }
367
368 #[wasm_bindgen(js_name = recordExpiredOutgoing)]
369 pub fn record_expired_outgoing(&self) {
370 self.inner.borrow_mut().record_expired_outgoing();
371 }
372
373 #[wasm_bindgen(js_name = recordSendFailure)]
374 pub fn record_send_failure(&self) {
375 self.inner.borrow_mut().record_send_failure();
376 }
377
378 #[wasm_bindgen(js_name = getStats)]
379 pub fn get_stats(&self, now_ms: f64) -> Result<JsValue, JsValue> {
380 let stats = self.inner.borrow_mut().stats(now_ms.max(0.0) as u64);
381 serde::Serialize::serialize(&stats, &serde_wasm_bindgen::Serializer::json_compatible())
382 .map_err(|error| JsValue::from_str(&error.to_string()))
383 }
384
385 #[wasm_bindgen(js_name = closeIncoming)]
386 pub fn close_incoming(&self) {
387 self.inner.borrow_mut().close_incoming();
388 }
389
390 #[wasm_bindgen(js_name = closeOutgoing)]
391 pub fn close_outgoing(&self) {
392 self.inner.borrow_mut().close_outgoing();
393 }
394
395 pub fn close(&self) {
396 self.inner.borrow_mut().close();
397 }
398 }
399
400 #[cfg(any(feature = "transport-webrtc", feature = "transport-moq"))]
401 fn emit_wasm_carrier_action(
402 handler: &Rc<RefCell<Option<js_sys::Function>>>,
403 action: serde_json::Value,
404 ) {
405 let Some(handler) = handler.borrow().as_ref().cloned() else {
406 return;
407 };
408 let Ok(value) = serde::Serialize::serialize(
414 &action,
415 &serde_wasm_bindgen::Serializer::json_compatible(),
416 ) else {
417 return;
418 };
419 if let Err(error) = handler.call1(&JsValue::UNDEFINED, &value) {
420 web_sys::console::error_2(
421 &JsValue::from_str("[OpenRTC][WASM carrier] action handler failed"),
422 &error,
423 );
424 }
425 }
426
427 #[cfg(any(feature = "transport-webrtc", feature = "transport-moq"))]
428 fn wasm_carrier_failure_code(error: &anyhow::Error) -> &'static str {
429 let message = format!("{error:#}");
430 if message.contains("base generation changed before candidate acknowledgement")
431 || message.contains("incumbent generation changed")
432 || message.contains("replacement incumbent is stale")
433 {
434 "carrier-base-generation-stale"
435 } else if message.contains("authorization epoch changed") {
436 "carrier-authorization-stale"
437 } else if message.contains("stale before atomic commit")
438 || message.contains("became stale during atomic commit")
439 {
440 "carrier-logical-generation-stale"
441 } else if message.contains("attempt was retired") || message.contains("retired upgrade") {
442 "carrier-attempt-retired"
443 } else {
444 "carrier-proof-failed"
445 }
446 }
447
448 struct TicketMeshStartGuard<'a> {
449 starts: &'a RefCell<HashSet<String>>,
450 id: String,
451 }
452
453 impl Drop for TicketMeshStartGuard<'_> {
454 fn drop(&mut self) {
455 self.starts.borrow_mut().remove(&self.id);
456 }
457 }
458
459 #[wasm_bindgen]
460 pub struct WasmClient {
461 inner: Arc<Client>,
462 portable_media: RefCell<crate::media::PortableMediaSession>,
463 broadcast_sessions: RefCell<HashMap<String, crate::broadcast::BroadcastSession>>,
464 broadcast_signers: RefCell<HashMap<String, crate::broadcast::BroadcastPublisherSigner>>,
465 ticket_meshes:
466 RefCell<HashMap<String, Rc<crate::ticket_mesh::wasm_session::WasmTicketMesh>>>,
467 ticket_mesh_starts: RefCell<HashSet<String>>,
468 identity_credential: Arc<Mutex<Option<String>>>,
469 last_auth_log: Arc<Mutex<Option<(bool, usize)>>>,
470 #[cfg(feature = "iroh-protocols-wasm")]
471 persistent_protocols:
472 Rc<tokio::sync::Mutex<Option<crate::wasm_docs_persistence::WasmPersistentDocsActor>>>,
473 #[cfg(feature = "managed-group-encryption")]
474 managed_group_controllers:
475 Rc<RefCell<HashMap<String, crate::managed_group_controller::ManagedGroupController>>>,
476 #[cfg(feature = "transport-webrtc")]
477 wasm_webrtc_carrier_sessions:
478 Rc<RefCell<HashMap<String, crate::wasm_webrtc_carrier::WasmWebRtcCarrierSession>>>,
479 #[cfg(feature = "transport-webrtc")]
480 wasm_webrtc_carrier_attempts: Rc<RefCell<HashMap<String, WasmWebRtcCarrierAttempt>>>,
481 #[cfg(any(feature = "transport-webrtc", feature = "transport-moq"))]
482 wasm_carrier_action_handler: Rc<RefCell<Option<js_sys::Function>>>,
483 #[cfg(feature = "transport-moq")]
484 wasm_moq_carrier_sessions:
485 Rc<RefCell<HashMap<String, crate::wasm_moq_carrier::WasmMoqCarrierSession>>>,
486 #[cfg(feature = "transport-moq")]
487 wasm_moq_carrier_attempts: Rc<RefCell<HashMap<String, WasmMoqCarrierAttempt>>>,
488 #[cfg(any(feature = "transport-webrtc", feature = "transport-moq"))]
489 wasm_remote_carrier_capabilities:
490 Rc<RefCell<HashMap<String, WasmRemoteCarrierCapabilities>>>,
491 #[cfg(any(feature = "transport-webrtc", feature = "transport-moq"))]
497 wasm_carrier_peer_lifecycle: Rc<tokio::sync::Mutex<()>>,
498 }
499
500 #[cfg(feature = "transport-webrtc")]
501 async fn fail_wasm_webrtc_attempt(
502 inner: Arc<Client>,
503 attempts: Rc<RefCell<HashMap<String, WasmWebRtcCarrierAttempt>>>,
504 sessions: Rc<
505 RefCell<HashMap<String, crate::wasm_webrtc_carrier::WasmWebRtcCarrierSession>>,
506 >,
507 handler: Rc<RefCell<Option<js_sys::Function>>>,
508 attempt: WasmWebRtcCarrierAttempt,
509 failure_code: &'static str,
510 notify_peer: bool,
511 ) {
512 let kind = crate::client::IrohPathKind::WebRtc;
513 if !inner
514 .retire_wasm_carrier_upgrade(
515 &attempt.connection_id,
516 kind,
517 &attempt.bootstrap.upgrade_id,
518 attempt.generation,
519 )
520 .await
521 {
522 return;
523 }
524 if attempts
525 .borrow()
526 .get(&attempt.connection_id)
527 .is_some_and(|current| current.bootstrap.upgrade_id == attempt.bootstrap.upgrade_id)
528 {
529 attempts.borrow_mut().remove(&attempt.connection_id);
530 }
531 sessions.borrow_mut().remove(&attempt.bootstrap.upgrade_id);
532 if notify_peer {
533 if let Ok(failed) = crate::iroh_carrier_bootstrap::CarrierBootstrapFrame::failed_from(
534 &attempt.bootstrap,
535 failure_code,
536 ) {
537 emit_wasm_carrier_action(
538 &handler,
539 serde_json::json!({
540 "type": "send-control",
541 "connectionId": attempt.connection_id,
542 "remoteEndpointId": attempt.remote_endpoint_id.to_string(),
543 "envelope": failed,
544 }),
545 );
546 }
547 }
548 emit_wasm_carrier_action(
549 &handler,
550 serde_json::json!({
551 "type": "retire-webrtc",
552 "connectionId": attempt.connection_id,
553 "upgradeId": attempt.bootstrap.upgrade_id,
554 "failureCode": failure_code,
555 }),
556 );
557 let retry_pending = attempt.retry_count == 0
558 && matches!(
559 failure_code,
560 "data-channel-failed"
561 | "ice-failed"
562 | "signaling-failed"
563 | "carrier-base-generation-stale"
564 );
565 if attempt.role == "initiator" && !retry_pending {
566 emit_wasm_carrier_action(
567 &handler,
568 serde_json::json!({
569 "type": "advance-carrier",
570 "connectionId": attempt.connection_id,
571 "remoteEndpointId": attempt.remote_endpoint_id.to_string(),
572 "failedRoute": "webrtc",
573 }),
574 );
575 }
576 }
577
578 #[cfg(feature = "transport-webrtc")]
579 async fn retire_selected_wasm_webrtc_carrier(
580 inner: Arc<Client>,
581 attempts: Rc<RefCell<HashMap<String, WasmWebRtcCarrierAttempt>>>,
582 sessions: Rc<
583 RefCell<HashMap<String, crate::wasm_webrtc_carrier::WasmWebRtcCarrierSession>>,
584 >,
585 handler: Rc<RefCell<Option<js_sys::Function>>>,
586 attempt: &WasmWebRtcCarrierAttempt,
587 failure_code: &'static str,
588 lifecycle_reason: &'static str,
589 ) -> bool {
590 if !inner
591 .close_current_iroh_carrier_generation_with_reason(
592 &attempt.connection_id,
593 attempt.remote_endpoint_id,
594 crate::client::IrohPathKind::WebRtc,
595 attempt.generation.transport_generation.saturating_add(1),
596 lifecycle_reason,
597 )
598 .await
599 {
600 return false;
601 }
602 web_sys::console::warn_1(&JsValue::from_str(&format!(
603 "[OpenRTC][WebRTC carrier] selected mechanism ended connection_id={} upgrade_id={} failure_code={failure_code}",
604 attempt.connection_id, attempt.bootstrap.upgrade_id,
605 )));
606 if attempts
607 .borrow()
608 .get(&attempt.connection_id)
609 .is_some_and(|current| current.bootstrap.upgrade_id == attempt.bootstrap.upgrade_id)
610 {
611 attempts.borrow_mut().remove(&attempt.connection_id);
612 }
613 sessions.borrow_mut().remove(&attempt.bootstrap.upgrade_id);
614 emit_wasm_carrier_action(
615 &handler,
616 serde_json::json!({
617 "type": "retire-webrtc",
618 "connectionId": attempt.connection_id,
619 "upgradeId": attempt.bootstrap.upgrade_id,
620 "failureCode": failure_code,
621 }),
622 );
623 true
624 }
625
626 #[cfg(feature = "transport-webrtc")]
627 impl WasmClient {
628 fn schedule_wasm_webrtc_carrier_watchdog(
629 &self,
630 bootstrap: crate::iroh_carrier_bootstrap::CarrierBootstrapFrame,
631 ) {
632 let inner = self.inner.clone();
633 let attempts = self.wasm_webrtc_carrier_attempts.clone();
634 let sessions = self.wasm_webrtc_carrier_sessions.clone();
635 let handler = self.wasm_carrier_action_handler.clone();
636 let lifecycle = self.wasm_carrier_peer_lifecycle.clone();
637 spawn_local(async move {
638 let mut completion_phase_observed = false;
639 loop {
640 let timeout = if completion_phase_observed { 45 } else { 30 };
641 gloo_timers::future::sleep(std::time::Duration::from_secs(timeout)).await;
642 let _lifecycle = lifecycle.lock().await;
643 let attempt = attempts
644 .borrow()
645 .values()
646 .find(|attempt| attempt.bootstrap.upgrade_id == bootstrap.upgrade_id)
647 .cloned();
648 let Some(attempt) = attempt else {
649 return;
650 };
651 if !inner
652 .wasm_carrier_upgrade_is_current(
653 &attempt.connection_id,
654 crate::client::IrohPathKind::WebRtc,
655 &attempt.bootstrap.upgrade_id,
656 attempt.generation,
657 )
658 .await
659 {
660 return;
661 }
662 if attempt.completion_started && !completion_phase_observed {
663 completion_phase_observed = true;
670 continue;
671 }
672 fail_wasm_webrtc_attempt(
673 inner,
674 attempts,
675 sessions,
676 handler,
677 attempt,
678 "carrier-timeout",
679 true,
680 )
681 .await;
682 return;
683 }
684 });
685 }
686
687 async fn fail_wasm_webrtc_carrier_attempt(
688 &self,
689 attempt: WasmWebRtcCarrierAttempt,
690 failure_code: &'static str,
691 notify_peer: bool,
692 ) {
693 fail_wasm_webrtc_attempt(
694 self.inner.clone(),
695 self.wasm_webrtc_carrier_attempts.clone(),
696 self.wasm_webrtc_carrier_sessions.clone(),
697 self.wasm_carrier_action_handler.clone(),
698 attempt,
699 failure_code,
700 notify_peer,
701 )
702 .await;
703 }
704
705 fn spawn_outbound_wasm_webrtc_carrier_completion(&self, attempt: WasmWebRtcCarrierAttempt) {
706 let inner = self.inner.clone();
707 let attempts = self.wasm_webrtc_carrier_attempts.clone();
708 let sessions = self.wasm_webrtc_carrier_sessions.clone();
709 let handler = self.wasm_carrier_action_handler.clone();
710 let lifecycle = self.wasm_carrier_peer_lifecycle.clone();
711 spawn_local(async move {
712 let kind = crate::client::IrohPathKind::WebRtc;
713 let result = inner
714 .complete_outbound_wasm_carrier_upgrade(
715 &attempt.connection_id,
716 &attempt.remote_endpoint_id.to_string(),
717 &attempt.bootstrap.upgrade_id,
718 attempt.generation,
719 kind,
720 )
721 .await;
722 let _lifecycle = lifecycle.lock().await;
723 if let Err(error) = result {
724 let failure_code = wasm_carrier_failure_code(&error);
725 web_sys::console::error_1(&JsValue::from_str(&format!(
726 "[OpenRTC][WebRTC carrier] outbound candidate completion failed connection_id={} upgrade_id={} failure_code={} error={error:#}",
727 attempt.connection_id, attempt.bootstrap.upgrade_id, failure_code,
728 )));
729 fail_wasm_webrtc_attempt(
730 inner,
731 attempts,
732 sessions,
733 handler,
734 attempt,
735 failure_code,
736 true,
737 )
738 .await;
739 return;
740 }
741 if !inner
742 .retire_wasm_carrier_upgrade(
743 &attempt.connection_id,
744 kind,
745 &attempt.bootstrap.upgrade_id,
746 attempt.generation,
747 )
748 .await
749 {
750 return;
751 }
752 emit_wasm_carrier_action(
753 &handler,
754 serde_json::json!({
755 "type": "selected",
756 "connectionId": attempt.connection_id,
757 "remoteEndpointId": attempt.remote_endpoint_id.to_string(),
758 "upgradeId": attempt.bootstrap.upgrade_id,
759 "family": "iroh",
760 "carrier": "webrtc",
761 "transportGeneration": attempt.generation.transport_generation.saturating_add(1),
762 "routeGeneration": 0,
763 }),
764 );
765 });
766 }
767
768 fn take_ready_outbound_wasm_webrtc_carrier_attempt(
769 &self,
770 connection_id: &str,
771 upgrade_id: &str,
772 ) -> Option<WasmWebRtcCarrierAttempt> {
773 if !self
774 .wasm_webrtc_carrier_sessions
775 .borrow()
776 .contains_key(upgrade_id)
777 {
778 return None;
779 }
780 let mut attempts = self.wasm_webrtc_carrier_attempts.borrow_mut();
781 let attempt = attempts.get_mut(connection_id).filter(|attempt| {
782 attempt.bootstrap.upgrade_id == upgrade_id
783 && attempt.role == "initiator"
784 && attempt.remote_ready
785 && !attempt.completion_started
786 })?;
787 attempt.completion_started = true;
788 Some(attempt.clone())
789 }
790
791 fn spawn_inbound_wasm_webrtc_carrier_completion(&self, attempt: WasmWebRtcCarrierAttempt) {
792 let inner = self.inner.clone();
793 let attempts = self.wasm_webrtc_carrier_attempts.clone();
794 let sessions = self.wasm_webrtc_carrier_sessions.clone();
795 let handler = self.wasm_carrier_action_handler.clone();
796 let lifecycle = self.wasm_carrier_peer_lifecycle.clone();
797 spawn_local(async move {
798 let kind = crate::client::IrohPathKind::WebRtc;
799 let node = inner.iroh_node.read().await.as_ref().cloned();
800 let result = async {
801 let node = node.ok_or_else(|| anyhow::anyhow!("Iroh node is unavailable"))?;
802 let authorization_expires_at_ms = attempt
803 .inbound_authorization_expires_at_ms
804 .ok_or_else(|| anyhow::anyhow!("browser WebRTC inbound authorization is missing"))?;
805 let candidate = node
806 .wait_for_inbound_replacement_candidate(
807 attempt.remote_endpoint_id,
808 crate::iroh_carrier_kind::EXPERIMENTAL_WEBRTC_TRANSPORT_ID,
809 authorization_expires_at_ms,
810 std::time::Duration::from_secs(40),
811 )
812 .await?;
813 let policy_epoch = inner
814 .require_current_wasm_carrier_upgrade_fence(
815 &attempt.connection_id,
816 kind,
817 &attempt.bootstrap.upgrade_id,
818 attempt.generation,
819 )
820 .await?;
821 let authorization_fence = inner
822 .capture_wasm_iroh_carrier_authorization_fence(
823 &attempt.connection_id,
824 attempt.generation,
825 kind,
826 policy_epoch,
827 )?;
828 let proof = inner.wasm_candidate_proof_probe(
829 &attempt.connection_id,
830 &attempt.bootstrap.upgrade_id,
831 attempt.bootstrap.base.transport_generation,
832 attempt.bootstrap.base.route_generation,
833 kind,
834 crate::iroh_carrier_kind::EXPERIMENTAL_WEBRTC_TRANSPORT_ID,
835 )?;
836 let (send, probe, ack) = inner
837 .receive_inbound_wasm_carrier_candidate_proof(
838 &attempt.connection_id,
839 &candidate,
840 &proof,
841 )
842 .await?;
843 anyhow::ensure!(
844 inner
845 .current_wasm_peer_data_generation(&attempt.connection_id, None)
846 .await
847 == Some(attempt.generation),
848 "browser WebRTC inbound carrier base generation changed before candidate acknowledgement"
849 );
850 inner
851 .send_inbound_wasm_carrier_candidate_ack(send, &ack)
852 .await?;
853 let (commit_send, committed) = inner
854 .receive_inbound_wasm_carrier_commit(
855 &attempt.connection_id,
856 &candidate,
857 &probe,
858 )
859 .await?;
860 let committed_replacement = inner
861 .commit_proven_wasm_carrier_candidate(
862 &attempt.connection_id,
863 &attempt.bootstrap.upgrade_id,
864 attempt.generation,
865 kind,
866 authorization_fence,
867 candidate,
868 )
869 .await?;
870 inner
871 .send_inbound_wasm_carrier_candidate_ack(commit_send, &committed)
872 .await?;
873 let replacement_transport_stable_id = committed_replacement
874 .logical_result()
875 .transport_stable_id
876 .ok_or_else(|| {
877 anyhow::anyhow!(
878 "browser WebRTC replacement has no stable ID"
879 )
880 })?;
881 committed_replacement.finish(b"wasm-custom-transport-upgrade");
882 inner
883 .publish_committed_wasm_carrier_route(
884 &attempt.connection_id,
885 kind,
886 replacement_transport_stable_id,
887 )
888 .await?;
889 Ok::<(), anyhow::Error>(())
890 }
891 .await;
892 if let Some(expires_at_ms) = attempt.inbound_authorization_expires_at_ms {
893 if let Some(node) = inner.iroh_node.read().await.as_ref().cloned() {
894 node.revoke_inbound_replacement_if_current(
895 attempt.remote_endpoint_id,
896 crate::iroh_carrier_kind::EXPERIMENTAL_WEBRTC_TRANSPORT_ID,
897 expires_at_ms,
898 )
899 .await;
900 }
901 }
902 let _lifecycle = lifecycle.lock().await;
903 if let Err(error) = result {
904 let failure_code = wasm_carrier_failure_code(&error);
905 web_sys::console::error_1(&JsValue::from_str(&format!(
906 "[OpenRTC][WebRTC carrier] inbound candidate completion failed connection_id={} upgrade_id={} failure_code={} error={error:#}",
907 attempt.connection_id, attempt.bootstrap.upgrade_id, failure_code,
908 )));
909 fail_wasm_webrtc_attempt(
910 inner,
911 attempts,
912 sessions,
913 handler,
914 attempt,
915 failure_code,
916 true,
917 )
918 .await;
919 return;
920 }
921 if !inner
922 .retire_wasm_carrier_upgrade(
923 &attempt.connection_id,
924 kind,
925 &attempt.bootstrap.upgrade_id,
926 attempt.generation,
927 )
928 .await
929 {
930 return;
931 }
932 emit_wasm_carrier_action(
933 &handler,
934 serde_json::json!({
935 "type": "selected",
936 "connectionId": attempt.connection_id,
937 "remoteEndpointId": attempt.remote_endpoint_id.to_string(),
938 "upgradeId": attempt.bootstrap.upgrade_id,
939 "family": "iroh",
940 "carrier": "webrtc",
941 "transportGeneration": attempt.generation.transport_generation.saturating_add(1),
942 "routeGeneration": 0,
943 }),
944 );
945 });
946 }
947 }
948
949 #[cfg(feature = "transport-moq")]
950 fn wasm_moq_carrier_namespaces(
951 local_endpoint_id: &str,
952 remote_endpoint_id: &str,
953 carrier_session_id: &str,
954 ) -> (String, String, &'static str) {
955 let (first, second) = if local_endpoint_id <= remote_endpoint_id {
956 (local_endpoint_id, remote_endpoint_id)
957 } else {
958 (remote_endpoint_id, local_endpoint_id)
959 };
960 let base = format!("openrtc/iroh-carrier/moq/{first}/{second}/{carrier_session_id}");
961 (
962 format!("{base}/from/{local_endpoint_id}"),
963 format!("{base}/from/{remote_endpoint_id}"),
964 "iroh-packets",
965 )
966 }
967
968 #[cfg(feature = "transport-moq")]
969 async fn fail_wasm_moq_attempt(
970 inner: Arc<Client>,
971 attempts: Rc<RefCell<HashMap<String, WasmMoqCarrierAttempt>>>,
972 sessions: Rc<RefCell<HashMap<String, crate::wasm_moq_carrier::WasmMoqCarrierSession>>>,
973 handler: Rc<RefCell<Option<js_sys::Function>>>,
974 attempt: WasmMoqCarrierAttempt,
975 failure_code: &'static str,
976 notify_peer: bool,
977 ) {
978 let kind = crate::client::IrohPathKind::Moq;
979 if !inner
980 .retire_wasm_carrier_upgrade(
981 &attempt.connection_id,
982 kind,
983 &attempt.bootstrap.upgrade_id,
984 attempt.generation,
985 )
986 .await
987 {
988 return;
989 }
990 if attempts
991 .borrow()
992 .get(&attempt.connection_id)
993 .is_some_and(|current| current.bootstrap.upgrade_id == attempt.bootstrap.upgrade_id)
994 {
995 attempts.borrow_mut().remove(&attempt.connection_id);
996 }
997 sessions.borrow_mut().remove(&attempt.bootstrap.upgrade_id);
998 if notify_peer {
999 if let Ok(failed) = crate::iroh_carrier_bootstrap::CarrierBootstrapFrame::failed_from(
1000 &attempt.bootstrap,
1001 failure_code,
1002 ) {
1003 emit_wasm_carrier_action(
1004 &handler,
1005 serde_json::json!({
1006 "type": "send-control",
1007 "connectionId": attempt.connection_id,
1008 "remoteEndpointId": attempt.remote_endpoint_id.to_string(),
1009 "envelope": failed,
1010 }),
1011 );
1012 }
1013 }
1014 emit_wasm_carrier_action(
1015 &handler,
1016 serde_json::json!({
1017 "type": "retire-moq",
1018 "connectionId": attempt.connection_id,
1019 "upgradeId": attempt.bootstrap.upgrade_id,
1020 "failureCode": failure_code,
1021 }),
1022 );
1023 let retry_pending = attempt.role == "initiator"
1024 && attempt.retry_count == 0
1025 && failure_code == "carrier-base-generation-stale";
1026 if attempt.role == "initiator" && !retry_pending {
1027 emit_wasm_carrier_action(
1028 &handler,
1029 serde_json::json!({
1030 "type": "advance-carrier",
1031 "connectionId": attempt.connection_id,
1032 "remoteEndpointId": attempt.remote_endpoint_id.to_string(),
1033 "failedRoute": "moq",
1034 }),
1035 );
1036 }
1037 }
1038
1039 #[cfg(feature = "transport-moq")]
1040 async fn retire_selected_wasm_moq_carrier(
1041 inner: Arc<Client>,
1042 attempts: Rc<RefCell<HashMap<String, WasmMoqCarrierAttempt>>>,
1043 sessions: Rc<RefCell<HashMap<String, crate::wasm_moq_carrier::WasmMoqCarrierSession>>>,
1044 handler: Rc<RefCell<Option<js_sys::Function>>>,
1045 attempt: &WasmMoqCarrierAttempt,
1046 failure_code: &'static str,
1047 lifecycle_reason: &'static str,
1048 ) -> bool {
1049 if !inner
1050 .close_current_iroh_carrier_generation_with_reason(
1051 &attempt.connection_id,
1052 attempt.remote_endpoint_id,
1053 crate::client::IrohPathKind::Moq,
1054 attempt.generation.transport_generation.saturating_add(1),
1055 lifecycle_reason,
1056 )
1057 .await
1058 {
1059 return false;
1060 }
1061 web_sys::console::warn_1(&JsValue::from_str(&format!(
1062 "[OpenRTC][MoQ carrier] selected mechanism ended connection_id={} upgrade_id={} failure_code={failure_code}",
1063 attempt.connection_id, attempt.bootstrap.upgrade_id,
1064 )));
1065 if attempts
1066 .borrow()
1067 .get(&attempt.connection_id)
1068 .is_some_and(|current| current.bootstrap.upgrade_id == attempt.bootstrap.upgrade_id)
1069 {
1070 attempts.borrow_mut().remove(&attempt.connection_id);
1071 }
1072 sessions.borrow_mut().remove(&attempt.bootstrap.upgrade_id);
1073 emit_wasm_carrier_action(
1074 &handler,
1075 serde_json::json!({
1076 "type": "retire-moq",
1077 "connectionId": attempt.connection_id,
1078 "upgradeId": attempt.bootstrap.upgrade_id,
1079 "failureCode": failure_code,
1080 }),
1081 );
1082 true
1083 }
1084
1085 #[cfg(feature = "transport-moq")]
1086 impl WasmClient {
1087 fn schedule_wasm_moq_carrier_watchdog(
1088 &self,
1089 bootstrap: crate::iroh_carrier_bootstrap::CarrierBootstrapFrame,
1090 ) {
1091 let inner = self.inner.clone();
1092 let attempts = self.wasm_moq_carrier_attempts.clone();
1093 let sessions = self.wasm_moq_carrier_sessions.clone();
1094 let handler = self.wasm_carrier_action_handler.clone();
1095 let lifecycle = self.wasm_carrier_peer_lifecycle.clone();
1096 spawn_local(async move {
1097 gloo_timers::future::sleep(std::time::Duration::from_secs(30)).await;
1098 let _lifecycle = lifecycle.lock().await;
1099 let attempt = attempts
1100 .borrow()
1101 .values()
1102 .find(|attempt| attempt.bootstrap.upgrade_id == bootstrap.upgrade_id)
1103 .cloned();
1104 if let Some(attempt) = attempt {
1105 if !inner
1106 .wasm_carrier_upgrade_is_current(
1107 &attempt.connection_id,
1108 crate::client::IrohPathKind::Moq,
1109 &attempt.bootstrap.upgrade_id,
1110 attempt.generation,
1111 )
1112 .await
1113 {
1114 return;
1115 }
1116 fail_wasm_moq_attempt(
1117 inner,
1118 attempts,
1119 sessions,
1120 handler,
1121 attempt,
1122 "carrier-timeout",
1123 true,
1124 )
1125 .await;
1126 }
1127 });
1128 }
1129
1130 async fn fail_wasm_moq_carrier_attempt(
1131 &self,
1132 attempt: WasmMoqCarrierAttempt,
1133 failure_code: &'static str,
1134 notify_peer: bool,
1135 ) {
1136 fail_wasm_moq_attempt(
1137 self.inner.clone(),
1138 self.wasm_moq_carrier_attempts.clone(),
1139 self.wasm_moq_carrier_sessions.clone(),
1140 self.wasm_carrier_action_handler.clone(),
1141 attempt,
1142 failure_code,
1143 notify_peer,
1144 )
1145 .await;
1146 }
1147
1148 fn spawn_outbound_wasm_moq_carrier_completion(&self, attempt: WasmMoqCarrierAttempt) {
1149 let inner = self.inner.clone();
1150 let attempts = self.wasm_moq_carrier_attempts.clone();
1151 let sessions = self.wasm_moq_carrier_sessions.clone();
1152 let handler = self.wasm_carrier_action_handler.clone();
1153 let lifecycle = self.wasm_carrier_peer_lifecycle.clone();
1154 spawn_local(async move {
1155 let kind = crate::client::IrohPathKind::Moq;
1156 let result = inner
1157 .complete_outbound_wasm_carrier_upgrade(
1158 &attempt.connection_id,
1159 &attempt.remote_endpoint_id.to_string(),
1160 &attempt.bootstrap.upgrade_id,
1161 attempt.generation,
1162 kind,
1163 )
1164 .await;
1165 let _lifecycle = lifecycle.lock().await;
1166 if let Err(error) = result {
1167 let failure_code = wasm_carrier_failure_code(&error);
1168 web_sys::console::error_1(&JsValue::from_str(&format!(
1169 "[OpenRTC][MoQ carrier] outbound candidate completion failed connection_id={} upgrade_id={} failure_code={} error={error:#}",
1170 attempt.connection_id, attempt.bootstrap.upgrade_id, failure_code,
1171 )));
1172 fail_wasm_moq_attempt(
1173 inner,
1174 attempts,
1175 sessions,
1176 handler,
1177 attempt,
1178 failure_code,
1179 true,
1180 )
1181 .await;
1182 return;
1183 }
1184 if !inner
1185 .retire_wasm_carrier_upgrade(
1186 &attempt.connection_id,
1187 kind,
1188 &attempt.bootstrap.upgrade_id,
1189 attempt.generation,
1190 )
1191 .await
1192 {
1193 return;
1194 }
1195 emit_wasm_carrier_action(
1196 &handler,
1197 serde_json::json!({
1198 "type": "selected",
1199 "connectionId": attempt.connection_id,
1200 "remoteEndpointId": attempt.remote_endpoint_id.to_string(),
1201 "upgradeId": attempt.bootstrap.upgrade_id,
1202 "family": "iroh",
1203 "carrier": "moq",
1204 "transportGeneration": attempt.generation.transport_generation.saturating_add(1),
1205 "routeGeneration": 0,
1206 }),
1207 );
1208 });
1209 }
1210
1211 fn spawn_inbound_wasm_moq_carrier_completion(&self, attempt: WasmMoqCarrierAttempt) {
1212 let inner = self.inner.clone();
1213 let attempts = self.wasm_moq_carrier_attempts.clone();
1214 let sessions = self.wasm_moq_carrier_sessions.clone();
1215 let handler = self.wasm_carrier_action_handler.clone();
1216 let lifecycle = self.wasm_carrier_peer_lifecycle.clone();
1217 spawn_local(async move {
1218 let kind = crate::client::IrohPathKind::Moq;
1219 let node = inner.iroh_node.read().await.as_ref().cloned();
1220 let result = async {
1221 let node = node.ok_or_else(|| anyhow::anyhow!("Iroh node is unavailable"))?;
1222 let authorization_expires_at_ms = attempt
1223 .inbound_authorization_expires_at_ms
1224 .ok_or_else(|| anyhow::anyhow!("browser MoQ inbound authorization is missing"))?;
1225 let candidate = node
1226 .wait_for_inbound_replacement_candidate(
1227 attempt.remote_endpoint_id,
1228 crate::iroh_carrier_kind::EXPERIMENTAL_MOQ_TRANSPORT_ID,
1229 authorization_expires_at_ms,
1230 std::time::Duration::from_secs(40),
1231 )
1232 .await?;
1233 let policy_epoch = inner
1234 .require_current_wasm_carrier_upgrade_fence(
1235 &attempt.connection_id,
1236 kind,
1237 &attempt.bootstrap.upgrade_id,
1238 attempt.generation,
1239 )
1240 .await?;
1241 let authorization_fence = inner
1242 .capture_wasm_iroh_carrier_authorization_fence(
1243 &attempt.connection_id,
1244 attempt.generation,
1245 kind,
1246 policy_epoch,
1247 )?;
1248 let proof = inner.wasm_candidate_proof_probe(
1249 &attempt.connection_id,
1250 &attempt.bootstrap.upgrade_id,
1251 attempt.bootstrap.base.transport_generation,
1252 attempt.bootstrap.base.route_generation,
1253 kind,
1254 crate::iroh_carrier_kind::EXPERIMENTAL_MOQ_TRANSPORT_ID,
1255 )?;
1256 let (send, probe, ack) = inner
1257 .receive_inbound_wasm_carrier_candidate_proof(
1258 &attempt.connection_id,
1259 &candidate,
1260 &proof,
1261 )
1262 .await?;
1263 anyhow::ensure!(
1264 inner
1265 .current_wasm_peer_data_generation(&attempt.connection_id, None)
1266 .await
1267 == Some(attempt.generation),
1268 "browser MoQ inbound carrier base generation changed before candidate acknowledgement"
1269 );
1270 inner
1271 .send_inbound_wasm_carrier_candidate_ack(send, &ack)
1272 .await?;
1273 let (commit_send, committed) = inner
1274 .receive_inbound_wasm_carrier_commit(
1275 &attempt.connection_id,
1276 &candidate,
1277 &probe,
1278 )
1279 .await?;
1280 let committed_replacement = inner
1281 .commit_proven_wasm_carrier_candidate(
1282 &attempt.connection_id,
1283 &attempt.bootstrap.upgrade_id,
1284 attempt.generation,
1285 kind,
1286 authorization_fence,
1287 candidate,
1288 )
1289 .await?;
1290 inner
1291 .send_inbound_wasm_carrier_candidate_ack(commit_send, &committed)
1292 .await?;
1293 let replacement_transport_stable_id = committed_replacement
1294 .logical_result()
1295 .transport_stable_id
1296 .ok_or_else(|| {
1297 anyhow::anyhow!("browser MoQ replacement has no stable ID")
1298 })?;
1299 committed_replacement.finish(b"wasm-custom-transport-upgrade");
1300 inner
1301 .publish_committed_wasm_carrier_route(
1302 &attempt.connection_id,
1303 kind,
1304 replacement_transport_stable_id,
1305 )
1306 .await?;
1307 Ok::<(), anyhow::Error>(())
1308 }
1309 .await;
1310 if let Some(expires_at_ms) = attempt.inbound_authorization_expires_at_ms {
1311 if let Some(node) = inner.iroh_node.read().await.as_ref().cloned() {
1312 node.revoke_inbound_replacement_if_current(
1313 attempt.remote_endpoint_id,
1314 crate::iroh_carrier_kind::EXPERIMENTAL_MOQ_TRANSPORT_ID,
1315 expires_at_ms,
1316 )
1317 .await;
1318 }
1319 }
1320 let _lifecycle = lifecycle.lock().await;
1321 if let Err(error) = result {
1322 let failure_code = wasm_carrier_failure_code(&error);
1323 web_sys::console::error_1(&JsValue::from_str(&format!(
1324 "[OpenRTC][MoQ carrier] inbound candidate completion failed connection_id={} upgrade_id={} failure_code={} error={error:#}",
1325 attempt.connection_id, attempt.bootstrap.upgrade_id, failure_code,
1326 )));
1327 fail_wasm_moq_attempt(
1328 inner,
1329 attempts,
1330 sessions,
1331 handler,
1332 attempt,
1333 failure_code,
1334 true,
1335 )
1336 .await;
1337 return;
1338 }
1339 if !inner
1340 .retire_wasm_carrier_upgrade(
1341 &attempt.connection_id,
1342 kind,
1343 &attempt.bootstrap.upgrade_id,
1344 attempt.generation,
1345 )
1346 .await
1347 {
1348 return;
1349 }
1350 emit_wasm_carrier_action(
1351 &handler,
1352 serde_json::json!({
1353 "type": "selected",
1354 "connectionId": attempt.connection_id,
1355 "remoteEndpointId": attempt.remote_endpoint_id.to_string(),
1356 "upgradeId": attempt.bootstrap.upgrade_id,
1357 "family": "iroh",
1358 "carrier": "moq",
1359 "transportGeneration": attempt.generation.transport_generation.saturating_add(1),
1360 "routeGeneration": 0,
1361 }),
1362 );
1363 });
1364 }
1365
1366 async fn handle_wasm_moq_bootstrap(
1367 &self,
1368 connection_id: String,
1369 remote_endpoint_id: String,
1370 bootstrap: crate::iroh_carrier_bootstrap::CarrierBootstrapFrame,
1371 ) -> Result<bool, JsValue> {
1372 let endpoint_id = remote_endpoint_id
1373 .parse::<iroh::EndpointId>()
1374 .map_err(|error| JsValue::from_str(&error.to_string()))?;
1375 let kind = crate::client::IrohPathKind::Moq;
1376 match bootstrap.action {
1377 crate::iroh_carrier_bootstrap::CarrierBootstrapAction::Request => {
1378 let local_endpoint_id = self
1379 .inner
1380 .current_node_id()
1381 .await
1382 .ok_or_else(|| JsValue::from_str("local endpoint id is unavailable"))?;
1383 if local_endpoint_id.as_str() >= remote_endpoint_id.as_str()
1384 || !self.inner.is_moq_carrier_enabled().await
1385 {
1386 return Ok(true);
1387 }
1388 if !crate::iroh_connection_policy::custom_carrier_base_allows(
1389 self.inner.iroh_path_kind(&remote_endpoint_id).await,
1390 crate::client::IrohPathKind::Moq,
1391 false,
1392 ) {
1393 return Ok(true);
1394 }
1395 let base_connection = self
1396 .inner
1397 .get_connection(endpoint_id)
1398 .await
1399 .ok_or_else(|| JsValue::from_str("MoQ carrier base is unavailable"))?;
1400 let generation = self
1401 .inner
1402 .current_wasm_peer_data_generation(&connection_id, None)
1403 .await
1404 .ok_or_else(|| {
1405 JsValue::from_str("MoQ carrier generation is unavailable")
1406 })?;
1407 if crate::transport_generation::for_connection(&base_connection)
1408 != generation.transport_stable_id
1409 {
1410 let failed =
1411 crate::iroh_carrier_bootstrap::CarrierBootstrapFrame::failed_from(
1412 &bootstrap,
1413 "carrier-base-generation-stale",
1414 )
1415 .map_err(|error| JsValue::from_str(&error.to_string()))?;
1416 emit_wasm_carrier_action(
1417 &self.wasm_carrier_action_handler,
1418 serde_json::json!({
1419 "type": "send-control",
1420 "connectionId": connection_id,
1421 "remoteEndpointId": remote_endpoint_id,
1422 "envelope": failed,
1423 }),
1424 );
1425 return Ok(true);
1426 }
1427 if !matches!(
1428 self.inner
1429 .reserve_wasm_carrier_upgrade(
1430 &connection_id,
1431 kind,
1432 &bootstrap.upgrade_id,
1433 generation,
1434 )
1435 .await,
1436 crate::client::WasmCarrierUpgradeReservation::Reserved
1437 ) {
1438 return Ok(true);
1439 }
1440 let node = self
1441 .inner
1442 .iroh_node
1443 .read()
1444 .await
1445 .as_ref()
1446 .cloned()
1447 .ok_or_else(|| JsValue::from_str("Iroh node is unavailable"))?;
1448 let authorization_expiry = node
1449 .authorize_pending_inbound_replacement(
1450 endpoint_id,
1451 crate::iroh_carrier_kind::EXPERIMENTAL_MOQ_TRANSPORT_ID,
1452 std::time::Duration::from_secs(45),
1453 )
1454 .await;
1455 let previous = self.wasm_moq_carrier_attempts.borrow_mut().insert(
1456 connection_id.clone(),
1457 WasmMoqCarrierAttempt {
1458 connection_id: connection_id.clone(),
1459 remote_endpoint_id: endpoint_id,
1460 bootstrap: bootstrap.clone(),
1461 generation,
1462 role: "responder",
1463 prepared: false,
1464 retry_sent: false,
1465 retry_count: bootstrap.attempt.saturating_sub(1),
1466 inbound_authorization_expires_at_ms: Some(authorization_expiry),
1467 },
1468 );
1469 if let Some(previous) = previous {
1470 self.wasm_moq_carrier_sessions
1471 .borrow_mut()
1472 .remove(&previous.bootstrap.upgrade_id);
1473 }
1474 let (publish_namespace, subscribe_namespace, track_name) =
1475 wasm_moq_carrier_namespaces(
1476 &local_endpoint_id,
1477 &remote_endpoint_id,
1478 &bootstrap.carrier_session_id,
1479 );
1480 emit_wasm_carrier_action(
1481 &self.wasm_carrier_action_handler,
1482 serde_json::json!({
1483 "type": "prepare-moq",
1484 "connectionId": connection_id,
1485 "remoteEndpointId": remote_endpoint_id,
1486 "role": "responder",
1487 "upgradeId": bootstrap.upgrade_id,
1488 "carrierSessionId": bootstrap.carrier_session_id,
1489 "transportGeneration": generation.transport_generation.saturating_add(1),
1490 "publishNamespace": publish_namespace,
1491 "subscribeNamespace": subscribe_namespace,
1492 "trackName": track_name,
1493 }),
1494 );
1495 self.schedule_wasm_moq_carrier_watchdog(bootstrap);
1496 }
1497 crate::iroh_carrier_bootstrap::CarrierBootstrapAction::Ready => {
1498 let attempt = self
1499 .wasm_moq_carrier_attempts
1500 .borrow()
1501 .get(&connection_id)
1502 .filter(|attempt| bootstrap.is_response_to(&attempt.bootstrap))
1503 .cloned();
1504 if let Some(attempt) = attempt {
1505 emit_wasm_carrier_action(
1506 &self.wasm_carrier_action_handler,
1507 serde_json::json!({
1508 "type": "activate-moq",
1509 "connectionId": attempt.connection_id,
1510 "upgradeId": attempt.bootstrap.upgrade_id,
1511 }),
1512 );
1513 }
1514 }
1515 crate::iroh_carrier_bootstrap::CarrierBootstrapAction::Failed => {
1516 let attempt = self
1517 .wasm_moq_carrier_attempts
1518 .borrow()
1519 .get(&connection_id)
1520 .filter(|attempt| bootstrap.is_response_to(&attempt.bootstrap))
1521 .cloned();
1522 if let Some(attempt) = attempt {
1523 let failure_code = crate::client::wasm_peer_carrier_failure_code(
1524 bootstrap.failure_code.as_deref(),
1525 );
1526 if crate::client::should_rearm_wasm_carrier_event_retry(
1527 failure_code,
1528 attempt.role == "initiator",
1529 false,
1530 ) {
1531 if let Some(current) = self
1532 .wasm_moq_carrier_attempts
1533 .borrow_mut()
1534 .get_mut(&connection_id)
1535 .filter(|current| {
1536 current.bootstrap.upgrade_id == attempt.bootstrap.upgrade_id
1537 })
1538 {
1539 current.retry_sent = false;
1545 }
1546 return Ok(true);
1547 }
1548 let should_retry = attempt.role == "initiator"
1549 && attempt.retry_count == 0
1550 && failure_code == "carrier-base-generation-stale";
1551 let remote_endpoint_id = attempt.remote_endpoint_id.to_string();
1552 self.fail_wasm_moq_carrier_attempt(attempt, failure_code, false)
1553 .await;
1554 if should_retry {
1555 gloo_timers::future::sleep(std::time::Duration::from_millis(500)).await;
1559 let _ = self
1560 .begin_iroh_moq_carrier_attempt(
1561 connection_id,
1562 remote_endpoint_id,
1563 1,
1564 )
1565 .await;
1566 }
1567 }
1568 }
1569 }
1570 Ok(true)
1571 }
1572 }
1573
1574 #[wasm_bindgen]
1575 impl WasmClient {
1576 #[wasm_bindgen(constructor)]
1580 pub fn new(api_key: String) -> Result<WasmClient, JsValue> {
1581 let api_key = crate::validate_api_key(&api_key)
1582 .map_err(|error| JsValue::from_str(&error.to_string()))?;
1583 let app_tag = crate::app_tag_from_api_key(api_key);
1584 let identity_credential: Arc<Mutex<Option<String>>> = Arc::new(Mutex::new(None));
1585 let credential_state = identity_credential.clone();
1586 let identity_credential_provider = Box::new(move || -> Option<String> {
1587 credential_state.lock().ok().and_then(|guard| guard.clone())
1588 });
1589
1590 Ok(Self {
1591 inner: Arc::new(Client::new_provider_neutral(
1592 app_tag,
1593 identity_credential_provider,
1594 )),
1595 portable_media: RefCell::new(crate::media::PortableMediaSession::default()),
1596 broadcast_sessions: RefCell::new(HashMap::new()),
1597 broadcast_signers: RefCell::new(HashMap::new()),
1598 ticket_meshes: RefCell::new(HashMap::new()),
1599 ticket_mesh_starts: RefCell::new(HashSet::new()),
1600 identity_credential,
1601 last_auth_log: Arc::new(Mutex::new(None)),
1602 #[cfg(feature = "iroh-protocols-wasm")]
1603 persistent_protocols: Rc::new(tokio::sync::Mutex::new(None)),
1604 #[cfg(feature = "managed-group-encryption")]
1605 managed_group_controllers: Rc::new(RefCell::new(HashMap::new())),
1606 #[cfg(feature = "transport-webrtc")]
1607 wasm_webrtc_carrier_sessions: Rc::new(RefCell::new(HashMap::new())),
1608 #[cfg(feature = "transport-webrtc")]
1609 wasm_webrtc_carrier_attempts: Rc::new(RefCell::new(HashMap::new())),
1610 #[cfg(any(feature = "transport-webrtc", feature = "transport-moq"))]
1611 wasm_carrier_action_handler: Rc::new(RefCell::new(None)),
1612 #[cfg(feature = "transport-moq")]
1613 wasm_moq_carrier_sessions: Rc::new(RefCell::new(HashMap::new())),
1614 #[cfg(feature = "transport-moq")]
1615 wasm_moq_carrier_attempts: Rc::new(RefCell::new(HashMap::new())),
1616 #[cfg(any(feature = "transport-webrtc", feature = "transport-moq"))]
1617 wasm_remote_carrier_capabilities: Rc::new(RefCell::new(HashMap::new())),
1618 #[cfg(any(feature = "transport-webrtc", feature = "transport-moq"))]
1619 wasm_carrier_peer_lifecycle: Rc::new(tokio::sync::Mutex::new(())),
1620 })
1621 }
1622
1623 #[cfg(feature = "managed-group-encryption")]
1627 #[wasm_bindgen(js_name = __initManagedRoomGroup)]
1628 pub fn init_managed_room_group(
1629 &self,
1630 avenue_key: String,
1631 device_id: String,
1632 wrapping_key: Vec<u8>,
1633 sealed_state: Option<Vec<u8>>,
1634 ) -> Result<JsValue, JsValue> {
1635 let wrapping_key: [u8; 32] = wrapping_key
1636 .try_into()
1637 .map_err(|_| JsValue::from_str("managed room wrapping key must be 32 bytes"))?;
1638 let mut controllers = self.managed_group_controllers.borrow_mut();
1639 if !controllers.contains_key(&avenue_key) {
1640 let controller = crate::managed_group_controller::ManagedGroupController::new(
1641 device_id,
1642 wrapping_key,
1643 sealed_state.as_deref(),
1644 )
1645 .map_err(|error| JsValue::from_str(&format!("{error:#}")))?;
1646 controllers.insert(avenue_key.clone(), controller);
1647 }
1648 let controller = controllers
1649 .get(&avenue_key)
1650 .ok_or_else(|| JsValue::from_str("managed room group initialization failed"))?;
1651 let action = controller
1652 .publish_key_package()
1653 .map_err(|error| JsValue::from_str(&format!("{error:#}")))?;
1654 serde::Serialize::serialize(&action, &serde_wasm_bindgen::Serializer::json_compatible())
1655 .map_err(|error| JsValue::from_str(&error.to_string()))
1656 }
1657
1658 #[cfg(feature = "managed-group-encryption")]
1659 #[wasm_bindgen(js_name = __handleManagedRoomPreparePage)]
1660 pub fn handle_managed_room_prepare_page(
1661 &self,
1662 avenue_key: String,
1663 page: JsValue,
1664 ) -> Result<JsValue, JsValue> {
1665 let page = serde_wasm_bindgen::from_value(page)
1666 .map_err(|error| JsValue::from_str(&error.to_string()))?;
1667 let mut controllers = self.managed_group_controllers.borrow_mut();
1668 let controller = controllers
1669 .get_mut(&avenue_key)
1670 .ok_or_else(|| JsValue::from_str("managed room group is not initialized"))?;
1671 let actions = controller
1672 .handle_prepare_page(page)
1673 .map_err(|error| JsValue::from_str(&format!("{error:#}")))?;
1674 serde::Serialize::serialize(
1675 &actions,
1676 &serde_wasm_bindgen::Serializer::json_compatible(),
1677 )
1678 .map_err(|error| JsValue::from_str(&error.to_string()))
1679 }
1680
1681 #[cfg(feature = "managed-group-encryption")]
1682 #[wasm_bindgen(js_name = __handleManagedRoomArtifactChunk)]
1683 pub fn handle_managed_room_artifact_chunk(
1684 &self,
1685 avenue_key: String,
1686 chunk: JsValue,
1687 ) -> Result<JsValue, JsValue> {
1688 let chunk = serde_wasm_bindgen::from_value(chunk)
1689 .map_err(|error| JsValue::from_str(&error.to_string()))?;
1690 let mut controllers = self.managed_group_controllers.borrow_mut();
1691 let controller = controllers
1692 .get_mut(&avenue_key)
1693 .ok_or_else(|| JsValue::from_str("managed room group is not initialized"))?;
1694 let actions = controller
1695 .handle_artifact_chunk(chunk)
1696 .map_err(|error| JsValue::from_str(&format!("{error:#}")))?;
1697 serde::Serialize::serialize(
1698 &actions,
1699 &serde_wasm_bindgen::Serializer::json_compatible(),
1700 )
1701 .map_err(|error| JsValue::from_str(&error.to_string()))
1702 }
1703
1704 #[cfg(feature = "managed-group-encryption")]
1705 #[wasm_bindgen(js_name = __sealManagedRoomPayload)]
1706 #[allow(clippy::too_many_arguments)]
1707 pub fn seal_managed_room_payload(
1708 &self,
1709 avenue_key: String,
1710 architecture_epoch: u64,
1711 encryption_epoch: u64,
1712 message_id: String,
1713 channel: String,
1714 priority: u8,
1715 zone_id: Option<String>,
1716 payload: Vec<u8>,
1717 ) -> Result<JsValue, JsValue> {
1718 let protected = self
1719 .managed_group_controllers
1720 .borrow_mut()
1721 .get_mut(&avenue_key)
1722 .ok_or_else(|| JsValue::from_str("managed room group is not initialized"))?
1723 .seal_payload(
1724 architecture_epoch,
1725 encryption_epoch,
1726 &message_id,
1727 &channel,
1728 priority,
1729 zone_id.as_deref(),
1730 &payload,
1731 )
1732 .map_err(|error| JsValue::from_str(&format!("{error:#}")))?;
1733 serde_wasm_bindgen::to_value(&serde_json::json!({
1734 "payload": base64::Engine::encode(
1735 &base64::engine::general_purpose::URL_SAFE_NO_PAD,
1736 protected.data,
1737 ),
1738 "sealedState": base64::Engine::encode(
1739 &base64::engine::general_purpose::URL_SAFE_NO_PAD,
1740 protected.sealed_state,
1741 ),
1742 }))
1743 .map_err(|error| JsValue::from_str(&error.to_string()))
1744 }
1745
1746 #[cfg(feature = "managed-group-encryption")]
1747 #[wasm_bindgen(js_name = __openManagedRoomPayload)]
1748 #[allow(clippy::too_many_arguments)]
1749 pub fn open_managed_room_payload(
1750 &self,
1751 avenue_key: String,
1752 architecture_epoch: u64,
1753 encryption_epoch: u64,
1754 message_id: String,
1755 channel: String,
1756 priority: u8,
1757 zone_id: Option<String>,
1758 ciphertext: Vec<u8>,
1759 ) -> Result<JsValue, JsValue> {
1760 let protected = self
1761 .managed_group_controllers
1762 .borrow_mut()
1763 .get_mut(&avenue_key)
1764 .ok_or_else(|| JsValue::from_str("managed room group is not initialized"))?
1765 .open_payload(
1766 architecture_epoch,
1767 encryption_epoch,
1768 &message_id,
1769 &channel,
1770 priority,
1771 zone_id.as_deref(),
1772 &ciphertext,
1773 )
1774 .map_err(|error| JsValue::from_str(&format!("{error:#}")))?;
1775 serde_wasm_bindgen::to_value(&serde_json::json!({
1776 "payload": base64::Engine::encode(
1777 &base64::engine::general_purpose::URL_SAFE_NO_PAD,
1778 protected.data,
1779 ),
1780 "sealedState": base64::Engine::encode(
1781 &base64::engine::general_purpose::URL_SAFE_NO_PAD,
1782 protected.sealed_state,
1783 ),
1784 }))
1785 .map_err(|error| JsValue::from_str(&error.to_string()))
1786 }
1787
1788 #[cfg(feature = "managed-group-encryption")]
1789 #[wasm_bindgen(js_name = __forgetManagedRoomGroup)]
1790 pub fn forget_managed_room_group(&self, avenue_key: String) {
1791 self.managed_group_controllers
1792 .borrow_mut()
1793 .remove(&avenue_key);
1794 }
1795
1796 #[wasm_bindgen(js_name = __prepareOpenRtcBroadcastGrant)]
1800 pub fn prepare_openrtc_broadcast_grant(
1801 &self,
1802 grant_token: String,
1803 issuer_public_key: Vec<u8>,
1804 now_ms: f64,
1805 ) -> Result<JsValue, JsValue> {
1806 let issuer_public_key: [u8; 32] = issuer_public_key
1807 .try_into()
1808 .map_err(|_| JsValue::from_str("broadcast issuer key must be 32 bytes"))?;
1809 let issuer = ed25519_dalek::VerifyingKey::from_bytes(&issuer_public_key)
1810 .map_err(|_| JsValue::from_str("broadcast issuer key is invalid"))?;
1811 if !now_ms.is_finite()
1812 || now_ms < 0.0
1813 || now_ms.fract() != 0.0
1814 || now_ms > crate::media::MAX_JAVASCRIPT_SAFE_INTEGER as f64
1815 {
1816 return Err(JsValue::from_str(
1817 "broadcast now must be a non-negative JavaScript-safe integer",
1818 ));
1819 }
1820 let challenge = self
1821 .inner
1822 .broadcasts()
1823 .prepare_grant_verification(&grant_token, &issuer, now_ms as u64)
1824 .map_err(|error| JsValue::from_str(&error.to_string()))?;
1825 let object = js_sys::Object::new();
1826 js_sys::Reflect::set(
1827 &object,
1828 &JsValue::from_str("handle"),
1829 &JsValue::from_str(&challenge.handle),
1830 )?;
1831 js_sys::Reflect::set(
1832 &object,
1833 &JsValue::from_str("signingBytes"),
1834 &js_sys::Uint8Array::from(challenge.signing_bytes.as_slice()),
1835 )?;
1836 js_sys::Reflect::set(
1837 &object,
1838 &JsValue::from_str("expiresAtMs"),
1839 &JsValue::from_f64(challenge.expires_at_ms as f64),
1840 )?;
1841 Ok(object.into())
1842 }
1843
1844 #[wasm_bindgen(js_name = __openOpenRtcBroadcast)]
1848 pub fn open_openrtc_broadcast(
1849 &self,
1850 grant_token: String,
1851 issuer_public_key: Vec<u8>,
1852 binding_challenge_handle: String,
1853 binding_signature: Vec<u8>,
1854 publication_signer_handle: Option<String>,
1855 now_ms: f64,
1856 ) -> Result<JsValue, JsValue> {
1857 let issuer_public_key: [u8; 32] = issuer_public_key
1858 .try_into()
1859 .map_err(|_| JsValue::from_str("broadcast issuer key must be 32 bytes"))?;
1860 let issuer = ed25519_dalek::VerifyingKey::from_bytes(&issuer_public_key)
1861 .map_err(|_| JsValue::from_str("broadcast issuer key is invalid"))?;
1862 if !now_ms.is_finite()
1863 || now_ms < 0.0
1864 || now_ms.fract() != 0.0
1865 || now_ms > crate::media::MAX_JAVASCRIPT_SAFE_INTEGER as f64
1866 {
1867 return Err(JsValue::from_str(
1868 "broadcast now must be a non-negative JavaScript-safe integer",
1869 ));
1870 }
1871 let now_ms = now_ms as u64;
1872 let grant = self
1873 .inner
1874 .broadcasts()
1875 .complete_grant_verification(
1876 &grant_token,
1877 &issuer,
1878 &binding_challenge_handle,
1879 &binding_signature,
1880 now_ms,
1881 )
1882 .map_err(|error| JsValue::from_str(&error.to_string()))?;
1883 let grant_generation = grant.grant_generation();
1884 let session = match publication_signer_handle {
1885 Some(handle) => {
1886 let signers = self.broadcast_signers.borrow();
1887 let signer = signers
1888 .get(&handle)
1889 .ok_or_else(|| JsValue::from_str("broadcast signer handle is invalid"))?;
1890 self.inner
1891 .broadcasts()
1892 .open_publisher(grant, signer, now_ms)
1893 }
1894 None => self.inner.broadcasts().open(grant, now_ms),
1895 }
1896 .map_err(|error| JsValue::from_str(&error.to_string()))?;
1897 let handle_id = format!("{}:{grant_generation}", session.id());
1898 let result = serde_json::json!({
1899 "id": session.id(),
1900 "handleId": handle_id,
1901 "role": session.role(),
1902 "state": session.state(),
1903 "budget": session.budget(),
1904 "sourceSlots": session.source_slots(),
1905 });
1906 self.broadcast_sessions
1907 .borrow_mut()
1908 .insert(handle_id, session);
1909 serde::Serialize::serialize(&result, &serde_wasm_bindgen::Serializer::json_compatible())
1910 .map_err(|error| JsValue::from_str(&error.to_string()))
1911 }
1912
1913 #[wasm_bindgen(js_name = __createOpenRtcBroadcastPublisherSigner)]
1917 pub fn create_openrtc_broadcast_publisher_signer(&self) -> Result<JsValue, JsValue> {
1918 let signer = crate::broadcast::BroadcastPublisherSigner::generate()
1919 .map_err(|error| JsValue::from_str(&error.to_string()))?;
1920 let mut handle_bytes = [0_u8; 16];
1921 getrandom::getrandom(&mut handle_bytes)
1922 .map_err(|_| JsValue::from_str("broadcast signer handle entropy failed"))?;
1923 let handle = hex::encode(handle_bytes);
1924 let public_key = signer.verifying_key();
1925 let object = js_sys::Object::new();
1926 js_sys::Reflect::set(
1927 &object,
1928 &JsValue::from_str("handle"),
1929 &JsValue::from_str(&handle),
1930 )?;
1931 js_sys::Reflect::set(
1932 &object,
1933 &JsValue::from_str("publicKey"),
1934 &js_sys::Uint8Array::from(public_key.as_slice()),
1935 )?;
1936 self.broadcast_signers.borrow_mut().insert(handle, signer);
1937 Ok(object.into())
1938 }
1939
1940 #[wasm_bindgen(js_name = __releaseOpenRtcBroadcastPublisherSigner)]
1941 pub fn release_openrtc_broadcast_publisher_signer(&self, handle: String) -> bool {
1942 self.broadcast_signers
1943 .borrow_mut()
1944 .remove(&handle)
1945 .is_some()
1946 }
1947
1948 #[wasm_bindgen(js_name = __takeOpenRtcBroadcastActions)]
1949 pub fn take_openrtc_broadcast_actions(
1950 &self,
1951 handle_id: String,
1952 max: usize,
1953 ) -> Result<JsValue, JsValue> {
1954 let sessions = self.broadcast_sessions.borrow();
1955 let session = sessions
1956 .get(&handle_id)
1957 .ok_or_else(|| JsValue::from_str("broadcast session is unavailable"))?;
1958 serde::Serialize::serialize(
1959 &session.take_actions(max),
1960 &serde_wasm_bindgen::Serializer::json_compatible(),
1961 )
1962 .map_err(|error| JsValue::from_str(&error.to_string()))
1963 }
1964
1965 #[wasm_bindgen(js_name = __getOpenRtcBroadcastStats)]
1966 pub fn get_openrtc_broadcast_stats(&self, handle_id: String) -> Result<JsValue, JsValue> {
1967 let sessions = self.broadcast_sessions.borrow();
1968 let stats = sessions
1969 .get(&handle_id)
1970 .ok_or_else(|| JsValue::from_str("broadcast session is unavailable"))?
1971 .stats();
1972 serde::Serialize::serialize(&stats, &serde_wasm_bindgen::Serializer::json_compatible())
1973 .map_err(|error| JsValue::from_str(&error.to_string()))
1974 }
1975
1976 #[wasm_bindgen(js_name = __observeOpenRtcBroadcastAdapter)]
1977 pub fn observe_openrtc_broadcast_adapter(
1978 &self,
1979 handle_id: String,
1980 observation: JsValue,
1981 ) -> Result<(), JsValue> {
1982 let observation = serde_wasm_bindgen::from_value(observation)
1983 .map_err(|error| JsValue::from_str(&error.to_string()))?;
1984 self.broadcast_sessions
1985 .borrow()
1986 .get(&handle_id)
1987 .ok_or_else(|| JsValue::from_str("broadcast session is unavailable"))?
1988 .observe(observation)
1989 .map_err(|error| JsValue::from_str(&error.to_string()))
1990 }
1991
1992 #[wasm_bindgen(js_name = __closeOpenRtcBroadcast)]
1993 pub fn close_openrtc_broadcast(&self, handle_id: String) -> Result<(), JsValue> {
1994 let session = self
1995 .broadcast_sessions
1996 .borrow_mut()
1997 .remove(&handle_id)
1998 .ok_or_else(|| JsValue::from_str("broadcast session is unavailable"))?;
1999 session.close();
2000 Ok(())
2001 }
2002
2003 #[wasm_bindgen(js_name = __beginOpenRtcBroadcastPublication)]
2006 #[allow(clippy::too_many_arguments)]
2007 pub fn begin_openrtc_broadcast_publication(
2008 &self,
2009 handle_id: String,
2010 source_slot: String,
2011 publication_id: Option<String>,
2012 kind: String,
2013 codec: String,
2014 clock_rate: u32,
2015 coded_width: Option<u32>,
2016 coded_height: Option<u32>,
2017 channels: Option<u16>,
2018 ) -> Result<JsValue, JsValue> {
2019 let publication_id = match publication_id {
2020 Some(value) if !value.is_empty() => {
2021 value
2022 .parse()
2023 .map_err(|error: crate::media::MediaProtocolError| {
2024 JsValue::from_str(&error.to_string())
2025 })?
2026 }
2027 _ => {
2028 let mut bytes = [0_u8; 16];
2029 getrandom::getrandom(&mut bytes)
2030 .map_err(|_| JsValue::from_str("media publication entropy failed"))?;
2031 crate::media::PublicationId::from_bytes(bytes)
2032 }
2033 };
2034 let kind: crate::media::MediaKind =
2035 kind.parse()
2036 .map_err(|error: crate::media::MediaProtocolError| {
2037 JsValue::from_str(&error.to_string())
2038 })?;
2039 if !matches!(
2040 kind,
2041 crate::media::MediaKind::Audio | crate::media::MediaKind::Video
2042 ) {
2043 return Err(JsValue::from_str(
2044 "browser broadcast publications must be audio or video",
2045 ));
2046 }
2047 let codec: crate::media::MediaCodec =
2048 codec
2049 .parse()
2050 .map_err(|error: crate::media::MediaProtocolError| {
2051 JsValue::from_str(&error.to_string())
2052 })?;
2053 let publication = self
2054 .broadcast_sessions
2055 .borrow()
2056 .get(&handle_id)
2057 .ok_or_else(|| JsValue::from_str("broadcast session is unavailable"))?
2058 .begin_publication(
2059 &source_slot,
2060 crate::media::MediaPublicationConfig {
2061 publication_id,
2062 media_generation: 0,
2063 kind,
2064 codec,
2065 clock_rate,
2066 coded_width,
2067 coded_height,
2068 channels,
2069 },
2070 )
2071 .map_err(|error| JsValue::from_str(&error.to_string()))?;
2072 serde::Serialize::serialize(
2073 &serde_json::json!({
2074 "publicationId": publication.publication_id.to_string(),
2075 "mediaGeneration": publication.media_generation,
2076 "control": Vec::<u8>::new(),
2077 }),
2078 &serde_wasm_bindgen::Serializer::json_compatible(),
2079 )
2080 .map_err(|error| JsValue::from_str(&error.to_string()))
2081 }
2082
2083 #[wasm_bindgen(js_name = __publishOpenRtcBroadcastSample)]
2086 #[allow(clippy::too_many_arguments)]
2087 pub fn publish_openrtc_broadcast_sample(
2088 &self,
2089 handle_id: String,
2090 publication_id: String,
2091 timestamp_us: f64,
2092 duration_us: u32,
2093 keyframe: bool,
2094 discardable: bool,
2095 payload: Vec<u8>,
2096 ) -> Result<(), JsValue> {
2097 if !timestamp_us.is_finite()
2098 || timestamp_us < 0.0
2099 || timestamp_us.fract() != 0.0
2100 || timestamp_us > crate::media::MAX_JAVASCRIPT_SAFE_INTEGER as f64
2101 {
2102 return Err(JsValue::from_str(
2103 "media timestamp must be a non-negative JavaScript-safe integer",
2104 ));
2105 }
2106 let publication_id =
2107 publication_id
2108 .parse()
2109 .map_err(|error: crate::media::MediaProtocolError| {
2110 JsValue::from_str(&error.to_string())
2111 })?;
2112 self.broadcast_sessions
2113 .borrow()
2114 .get(&handle_id)
2115 .ok_or_else(|| JsValue::from_str("broadcast session is unavailable"))?
2116 .publish(
2117 publication_id,
2118 crate::media::EncodedMediaSample {
2119 timestamp_us: timestamp_us as u64,
2120 duration_us,
2121 keyframe,
2122 discardable,
2123 payload,
2124 },
2125 )
2126 .map_err(|error| JsValue::from_str(&error.to_string()))
2127 }
2128
2129 #[wasm_bindgen(js_name = __retireOpenRtcBroadcastPublication)]
2130 pub fn retire_openrtc_broadcast_publication(
2131 &self,
2132 handle_id: String,
2133 publication_id: String,
2134 ) -> Result<(), JsValue> {
2135 let publication_id =
2136 publication_id
2137 .parse()
2138 .map_err(|error: crate::media::MediaProtocolError| {
2139 JsValue::from_str(&error.to_string())
2140 })?;
2141 self.broadcast_sessions
2142 .borrow()
2143 .get(&handle_id)
2144 .ok_or_else(|| JsValue::from_str("broadcast session is unavailable"))?
2145 .retire_publication(publication_id)
2146 .map_err(|error| JsValue::from_str(&error.to_string()))
2147 }
2148
2149 #[wasm_bindgen(js_name = __pauseOpenRtcBroadcastPublication)]
2150 pub fn pause_openrtc_broadcast_publication(
2151 &self,
2152 handle_id: String,
2153 publication_id: String,
2154 ) -> Result<(), JsValue> {
2155 let publication_id =
2156 publication_id
2157 .parse()
2158 .map_err(|error: crate::media::MediaProtocolError| {
2159 JsValue::from_str(&error.to_string())
2160 })?;
2161 self.broadcast_sessions
2162 .borrow()
2163 .get(&handle_id)
2164 .ok_or_else(|| JsValue::from_str("broadcast session is unavailable"))?
2165 .pause_publication(publication_id)
2166 .map_err(|error| JsValue::from_str(&error.to_string()))
2167 }
2168
2169 #[wasm_bindgen(js_name = __acceptOpenRtcBroadcastObject)]
2172 pub fn accept_openrtc_broadcast_object(
2173 &self,
2174 handle_id: String,
2175 source_slot: String,
2176 encoded: Vec<u8>,
2177 ) -> Result<JsValue, JsValue> {
2178 let accepted = self
2179 .broadcast_sessions
2180 .borrow()
2181 .get(&handle_id)
2182 .ok_or_else(|| JsValue::from_str("broadcast session is unavailable"))?
2183 .accept_media_for_source(&source_slot, &encoded)
2184 .map_err(|error| JsValue::from_str(&error.to_string()))?;
2185 serde::Serialize::serialize(
2186 &accepted,
2187 &serde_wasm_bindgen::Serializer::json_compatible(),
2188 )
2189 .map_err(|error| JsValue::from_str(&error.to_string()))
2190 }
2191
2192 #[wasm_bindgen(js_name = __beginOpenRtcMediaPublication)]
2196 #[allow(clippy::too_many_arguments)]
2197 pub fn begin_openrtc_media_publication(
2198 &self,
2199 publication_id: Option<String>,
2200 kind: String,
2201 codec: String,
2202 clock_rate: u32,
2203 coded_width: Option<u32>,
2204 coded_height: Option<u32>,
2205 channels: Option<u16>,
2206 ) -> Result<JsValue, JsValue> {
2207 let publication_id = match publication_id {
2208 Some(value) if !value.is_empty() => {
2209 value
2210 .parse()
2211 .map_err(|error: crate::media::MediaProtocolError| {
2212 JsValue::from_str(&error.to_string())
2213 })?
2214 }
2215 _ => {
2216 let mut bytes = [0_u8; 16];
2217 getrandom::getrandom(&mut bytes)
2218 .map_err(|_| JsValue::from_str("media publication entropy failed"))?;
2219 crate::media::PublicationId::from_bytes(bytes)
2220 }
2221 };
2222 let kind: crate::media::MediaKind =
2223 kind.parse()
2224 .map_err(|error: crate::media::MediaProtocolError| {
2225 JsValue::from_str(&error.to_string())
2226 })?;
2227 if !matches!(
2228 kind,
2229 crate::media::MediaKind::Audio | crate::media::MediaKind::Video
2230 ) {
2231 return Err(JsValue::from_str(
2232 "browser media publications must be audio or video",
2233 ));
2234 }
2235 let codec: crate::media::MediaCodec =
2236 codec
2237 .parse()
2238 .map_err(|error: crate::media::MediaProtocolError| {
2239 JsValue::from_str(&error.to_string())
2240 })?;
2241 let publication = crate::media::MediaPublicationConfig {
2242 publication_id,
2243 media_generation: 0,
2244 kind,
2245 codec,
2246 clock_rate,
2247 coded_width,
2248 coded_height,
2249 channels,
2250 };
2251 let (publication, control) = self
2252 .portable_media
2253 .borrow_mut()
2254 .begin_publication(publication)
2255 .map_err(|error| JsValue::from_str(&error.to_string()))?;
2256
2257 let object = js_sys::Object::new();
2258 js_sys::Reflect::set(
2259 &object,
2260 &JsValue::from_str("publicationId"),
2261 &JsValue::from_str(&publication_id.to_string()),
2262 )?;
2263 js_sys::Reflect::set(
2264 &object,
2265 &JsValue::from_str("mediaGeneration"),
2266 &JsValue::from_f64(publication.media_generation as f64),
2267 )?;
2268 js_sys::Reflect::set(
2269 &object,
2270 &JsValue::from_str("control"),
2271 &js_sys::Uint8Array::from(control.as_slice()),
2272 )?;
2273 Ok(object.into())
2274 }
2275
2276 #[wasm_bindgen(js_name = __encodeOpenRtcMediaSample)]
2279 pub fn encode_openrtc_media_sample(
2280 &self,
2281 publication_id: String,
2282 timestamp_us: f64,
2283 duration_us: u32,
2284 keyframe: bool,
2285 discardable: bool,
2286 payload: Vec<u8>,
2287 ) -> Result<Vec<u8>, JsValue> {
2288 fn safe_u64(value: f64, field: &str) -> Result<u64, JsValue> {
2289 if !value.is_finite()
2290 || value < 0.0
2291 || value.fract() != 0.0
2292 || value > crate::media::MAX_JAVASCRIPT_SAFE_INTEGER as f64
2293 {
2294 return Err(JsValue::from_str(&format!(
2295 "media {field} must be a non-negative JavaScript-safe integer"
2296 )));
2297 }
2298 Ok(value as u64)
2299 }
2300 let publication_id: crate::media::PublicationId =
2301 publication_id
2302 .parse()
2303 .map_err(|error: crate::media::MediaProtocolError| {
2304 JsValue::from_str(&error.to_string())
2305 })?;
2306 self.portable_media
2307 .borrow_mut()
2308 .encode_sample(
2309 publication_id,
2310 crate::media::EncodedMediaSample {
2311 timestamp_us: safe_u64(timestamp_us, "timestamp")?,
2312 duration_us,
2313 keyframe,
2314 discardable,
2315 payload,
2316 },
2317 )
2318 .map_err(|error| JsValue::from_str(&error.to_string()))
2319 }
2320
2321 #[wasm_bindgen(js_name = __pauseOpenRtcMediaPublication)]
2322 pub fn pause_openrtc_media_publication(
2323 &self,
2324 publication_id: String,
2325 ) -> Result<(), JsValue> {
2326 let publication_id: crate::media::PublicationId =
2327 publication_id
2328 .parse()
2329 .map_err(|error: crate::media::MediaProtocolError| {
2330 JsValue::from_str(&error.to_string())
2331 })?;
2332 self.portable_media
2333 .borrow_mut()
2334 .pause_publication(publication_id)
2335 .map_err(|error| JsValue::from_str(&error.to_string()))
2336 }
2337
2338 #[wasm_bindgen(js_name = __retireOpenRtcMediaPublication)]
2339 pub fn retire_openrtc_media_publication(
2340 &self,
2341 publication_id: String,
2342 ) -> Result<(), JsValue> {
2343 let publication_id: crate::media::PublicationId =
2344 publication_id
2345 .parse()
2346 .map_err(|error: crate::media::MediaProtocolError| {
2347 JsValue::from_str(&error.to_string())
2348 })?;
2349 self.portable_media
2350 .borrow_mut()
2351 .retire_publication(publication_id);
2352 Ok(())
2353 }
2354
2355 #[wasm_bindgen(js_name = __retireOpenRtcMediaReceiver)]
2356 pub fn retire_openrtc_media_receiver(
2357 &self,
2358 publication_id: String,
2359 media_generation: u32,
2360 ) -> Result<(), JsValue> {
2361 let publication_id: crate::media::PublicationId =
2362 publication_id
2363 .parse()
2364 .map_err(|error: crate::media::MediaProtocolError| {
2365 JsValue::from_str(&error.to_string())
2366 })?;
2367 self.portable_media
2368 .borrow_mut()
2369 .retire_receiver(publication_id, media_generation);
2370 Ok(())
2371 }
2372
2373 #[wasm_bindgen(js_name = __decodeOpenRtcMediaChunk)]
2377 pub fn decode_openrtc_media_chunk(&self, encoded: Vec<u8>) -> Result<JsValue, JsValue> {
2378 let chunk = self
2379 .portable_media
2380 .borrow_mut()
2381 .decode_chunk(&encoded)
2382 .map_err(|error| JsValue::from_str(&error.to_string()))?;
2383 crate::media::ensure_javascript_safe_integer(chunk.sequence, "sequence")
2384 .and_then(|_| {
2385 crate::media::ensure_javascript_safe_integer(chunk.timestamp_us, "timestamp")
2386 })
2387 .map_err(|error| JsValue::from_str(&error.to_string()))?;
2388
2389 let object = js_sys::Object::new();
2390 let set = |name: &str, value: JsValue| -> Result<(), JsValue> {
2391 js_sys::Reflect::set(&object, &JsValue::from_str(name), &value).map(|_| ())
2392 };
2393 set(
2394 "publicationId",
2395 JsValue::from_str(&chunk.publication_id.to_string()),
2396 )?;
2397 set(
2398 "mediaGeneration",
2399 JsValue::from_f64(chunk.media_generation as f64),
2400 )?;
2401 set("sequence", JsValue::from_f64(chunk.sequence as f64))?;
2402 set("timestampUs", JsValue::from_f64(chunk.timestamp_us as f64))?;
2403 set("durationUs", JsValue::from_f64(chunk.duration_us as f64))?;
2404 set(
2405 "kind",
2406 JsValue::from_str(match chunk.kind {
2407 crate::media::MediaKind::Audio => "audio",
2408 crate::media::MediaKind::Video => "video",
2409 crate::media::MediaKind::Screen => "screen",
2410 crate::media::MediaKind::Data => "data",
2411 }),
2412 )?;
2413 set(
2414 "codec",
2415 JsValue::from_str(match chunk.codec {
2416 crate::media::MediaCodec::Opus => "opus",
2417 crate::media::MediaCodec::H264 => "h264",
2418 crate::media::MediaCodec::Vp8 => "vp8",
2419 crate::media::MediaCodec::Vp9 => "vp9",
2420 crate::media::MediaCodec::Av1 => "av1",
2421 crate::media::MediaCodec::Pcm => "pcm",
2422 crate::media::MediaCodec::Opaque => "opaque",
2423 }),
2424 )?;
2425 set("keyframe", JsValue::from_bool(chunk.keyframe))?;
2426 set("discardable", JsValue::from_bool(chunk.discardable))?;
2427 set(
2428 "payload",
2429 js_sys::Uint8Array::from(chunk.payload.as_slice()).into(),
2430 )?;
2431 Ok(object.into())
2432 }
2433
2434 #[wasm_bindgen(js_name = __decodeOpenRtcMediaControl)]
2435 pub fn decode_openrtc_media_control(&self, encoded: Vec<u8>) -> Result<JsValue, JsValue> {
2436 let control = self
2437 .portable_media
2438 .borrow_mut()
2439 .decode_control(&encoded)
2440 .map_err(|error| JsValue::from_str(&error.to_string()))?;
2441 let portable = match control {
2446 crate::media::MediaControlFrame::Publish {
2447 publication_id,
2448 media_generation,
2449 kind,
2450 codec,
2451 clock_rate,
2452 coded_width,
2453 coded_height,
2454 channels,
2455 } => serde_json::json!({
2456 "type": "publish",
2457 "publicationId": publication_id.to_string(),
2458 "mediaGeneration": media_generation,
2459 "kind": kind,
2460 "codec": codec,
2461 "clockRate": clock_rate,
2462 "codedWidth": coded_width,
2463 "codedHeight": coded_height,
2464 "channels": channels,
2465 }),
2466 crate::media::MediaControlFrame::SetEnabled {
2467 publication_id,
2468 media_generation,
2469 enabled,
2470 } => serde_json::json!({
2471 "type": "set-enabled",
2472 "publicationId": publication_id.to_string(),
2473 "mediaGeneration": media_generation,
2474 "enabled": enabled,
2475 }),
2476 crate::media::MediaControlFrame::RequestKeyframe {
2477 publication_id,
2478 media_generation,
2479 } => serde_json::json!({
2480 "type": "request-keyframe",
2481 "publicationId": publication_id.to_string(),
2482 "mediaGeneration": media_generation,
2483 }),
2484 crate::media::MediaControlFrame::Stop {
2485 publication_id,
2486 media_generation,
2487 reason,
2488 } => serde_json::json!({
2489 "type": "stop",
2490 "publicationId": publication_id.to_string(),
2491 "mediaGeneration": media_generation,
2492 "reason": reason,
2493 }),
2494 };
2495 serde::Serialize::serialize(
2496 &portable,
2497 &serde_wasm_bindgen::Serializer::json_compatible(),
2498 )
2499 .map_err(|error| JsValue::from_str(&error.to_string()))
2500 }
2501
2502 #[cfg(any(feature = "transport-webrtc", feature = "transport-moq"))]
2506 #[wasm_bindgen(js_name = __setIrohCarrierActionHandler)]
2507 pub fn set_iroh_carrier_action_handler(&self, handler: Option<js_sys::Function>) {
2508 *self.wasm_carrier_action_handler.borrow_mut() = handler;
2509 }
2510
2511 #[cfg(any(feature = "transport-webrtc", feature = "transport-moq"))]
2515 #[wasm_bindgen(js_name = __getIrohCarrierReadiness)]
2516 pub async fn get_iroh_carrier_readiness(&self) -> Result<JsValue, JsValue> {
2517 let readiness = serde_json::json!({
2518 "webrtcConfigured": self.inner.is_webrtc_carrier_configured().await,
2519 "moqConfigured": self.inner.is_moq_carrier_configured().await,
2520 "webrtcAvailable": self.inner.is_webrtc_carrier_enabled().await,
2521 "moqAvailable": self.inner.is_moq_carrier_enabled().await,
2522 "actionHandlerInstalled": self.wasm_carrier_action_handler.borrow().is_some(),
2523 });
2524 serde::Serialize::serialize(
2525 &readiness,
2526 &serde_wasm_bindgen::Serializer::json_compatible(),
2527 )
2528 .map_err(|error| JsValue::from_str(&error.to_string()))
2529 }
2530
2531 #[cfg(feature = "transport-webrtc")]
2532 #[wasm_bindgen(js_name = __configureWebRtcCarrier)]
2533 pub async fn configure_iroh_webrtc_carrier(
2534 &self,
2535 enabled: bool,
2536 privacy_mode: bool,
2537 ) -> Result<(), JsValue> {
2538 let _lifecycle = self.wasm_carrier_peer_lifecycle.lock().await;
2539 self.inner.set_webrtc_carrier(enabled, privacy_mode).await;
2540 Ok(())
2541 }
2542
2543 #[cfg(any(feature = "transport-webrtc", feature = "transport-moq"))]
2546 #[wasm_bindgen(js_name = __configureIrohRoutePolicy)]
2547 pub async fn configure_iroh_route_policy(
2548 &self,
2549 relay_only: bool,
2550 optimize_for: Option<String>,
2551 route_priority: JsValue,
2552 ) -> Result<(), JsValue> {
2553 let _lifecycle = self.wasm_carrier_peer_lifecycle.lock().await;
2554 let optimize_for = match optimize_for.as_deref().unwrap_or("balanced") {
2555 "balanced" => crate::route_policy::RouteOptimization::Balanced,
2556 "lowest-latency" => crate::route_policy::RouteOptimization::LowestLatency,
2557 _ => {
2558 return Err(JsValue::from_str(
2559 "transport optimization must be balanced or lowest-latency",
2560 ))
2561 }
2562 };
2563 let route_priority = if route_priority.is_null() || route_priority.is_undefined() {
2564 Vec::new()
2565 } else {
2566 serde_wasm_bindgen::from_value(route_priority)
2567 .map_err(|error| JsValue::from_str(&error.to_string()))?
2568 };
2569 self.inner
2570 .configure_wasm_route_policy(relay_only, optimize_for, route_priority)
2571 .await;
2572 Ok(())
2573 }
2574
2575 #[cfg(any(feature = "transport-webrtc", feature = "transport-moq"))]
2579 #[wasm_bindgen(js_name = __applyIrohCarrierPolicy)]
2580 pub async fn apply_iroh_carrier_policy(&self) -> Result<u32, JsValue> {
2581 let _lifecycle = self.wasm_carrier_peer_lifecycle.lock().await;
2582 let capabilities: Vec<_> = self
2583 .wasm_remote_carrier_capabilities
2584 .borrow()
2585 .iter()
2586 .map(|(connection_id, capabilities)| (connection_id.clone(), *capabilities))
2587 .collect();
2588 let mut reconciled = 0u32;
2589 for (connection_id, capabilities) in capabilities {
2590 let Some(remote_endpoint_id) = self
2591 .inner
2592 .wasm_iroh_carrier_remote_endpoint_id(&connection_id)
2593 .await
2594 else {
2595 continue;
2596 };
2597 if self
2598 .begin_preferred_iroh_carrier_locked(
2599 connection_id,
2600 remote_endpoint_id,
2601 capabilities.webrtc,
2602 capabilities.moq,
2603 )
2604 .await?
2605 {
2606 reconciled = reconciled.saturating_add(1);
2607 }
2608 }
2609 Ok(reconciled)
2610 }
2611
2612 #[cfg(any(feature = "transport-webrtc", feature = "transport-moq"))]
2619 #[wasm_bindgen(js_name = __reconcileIrohCarriersAfterNetworkChange)]
2620 pub async fn reconcile_iroh_carriers_after_network_change(&self) -> u32 {
2621 let _lifecycle = self.wasm_carrier_peer_lifecycle.lock().await;
2622 let mut reconciled = 0u32;
2623 let mut recoveries = Vec::new();
2624 #[cfg(feature = "transport-webrtc")]
2625 {
2626 let attempts: Vec<_> = self
2627 .wasm_webrtc_carrier_attempts
2628 .borrow()
2629 .values()
2630 .cloned()
2631 .collect();
2632 let probed = futures::future::join_all(attempts.into_iter().map(|attempt| {
2633 let inner = self.inner.clone();
2634 async move {
2635 let needs_recovery = inner
2636 .wasm_carrier_generation_needs_network_recovery(
2637 &attempt.connection_id,
2638 attempt.remote_endpoint_id,
2639 crate::client::IrohPathKind::WebRtc,
2640 attempt.generation.transport_generation.saturating_add(1),
2641 )
2642 .await;
2643 (attempt, needs_recovery)
2644 }
2645 }))
2646 .await;
2647 for (attempt, needs_recovery) in probed {
2648 if !needs_recovery {
2649 continue;
2650 }
2651 if retire_selected_wasm_webrtc_carrier(
2652 self.inner.clone(),
2653 self.wasm_webrtc_carrier_attempts.clone(),
2654 self.wasm_webrtc_carrier_sessions.clone(),
2655 self.wasm_carrier_action_handler.clone(),
2656 &attempt,
2657 "network-change",
2658 crate::lifecycle_reason::REASON_NETWORK_CHANGE_RECONNECT,
2659 )
2660 .await
2661 {
2662 reconciled = reconciled.saturating_add(1);
2663 recoveries.push((
2664 attempt.connection_id.clone(),
2665 attempt.remote_endpoint_id,
2666 attempt.generation.transport_generation.saturating_add(1),
2667 ));
2668 }
2669 }
2670 }
2671 #[cfg(feature = "transport-moq")]
2672 {
2673 let attempts: Vec<_> = self
2674 .wasm_moq_carrier_attempts
2675 .borrow()
2676 .values()
2677 .cloned()
2678 .collect();
2679 let probed = futures::future::join_all(attempts.into_iter().map(|attempt| {
2680 let inner = self.inner.clone();
2681 async move {
2682 let needs_recovery = inner
2683 .wasm_carrier_generation_needs_network_recovery(
2684 &attempt.connection_id,
2685 attempt.remote_endpoint_id,
2686 crate::client::IrohPathKind::Moq,
2687 attempt.generation.transport_generation.saturating_add(1),
2688 )
2689 .await;
2690 (attempt, needs_recovery)
2691 }
2692 }))
2693 .await;
2694 for (attempt, needs_recovery) in probed {
2695 if !needs_recovery {
2696 continue;
2697 }
2698 if retire_selected_wasm_moq_carrier(
2699 self.inner.clone(),
2700 self.wasm_moq_carrier_attempts.clone(),
2701 self.wasm_moq_carrier_sessions.clone(),
2702 self.wasm_carrier_action_handler.clone(),
2703 &attempt,
2704 "network-change",
2705 crate::lifecycle_reason::REASON_NETWORK_CHANGE_RECONNECT,
2706 )
2707 .await
2708 {
2709 reconciled = reconciled.saturating_add(1);
2710 recoveries.push((
2711 attempt.connection_id.clone(),
2712 attempt.remote_endpoint_id,
2713 attempt.generation.transport_generation.saturating_add(1),
2714 ));
2715 }
2716 }
2717 }
2718 for (connection_id, remote_endpoint_id, transport_generation) in recoveries {
2719 let recovered = self
2720 .inner
2721 .recover_retired_wasm_carrier_generation(
2722 &connection_id,
2723 remote_endpoint_id,
2724 transport_generation,
2725 )
2726 .await;
2727 if !recovered {
2728 web_sys::console::warn_1(&JsValue::from_str(&format!(
2729 "[OpenRTC][Iroh carrier] base recovery remained pending connection_id={connection_id} transport_generation={transport_generation}"
2730 )));
2731 }
2732 }
2733 if reconciled > 0 {
2734 self.inner.wake_browser_auto_connect();
2741 }
2742 reconciled
2743 }
2744
2745 #[cfg(any(feature = "transport-webrtc", feature = "transport-moq"))]
2749 #[wasm_bindgen(js_name = __forgetIrohCarrierPeer)]
2750 pub async fn forget_iroh_carrier_peer(&self, connection_id: String) {
2751 let _lifecycle = self.wasm_carrier_peer_lifecycle.lock().await;
2752 self.forget_iroh_carrier_peer_locked(&connection_id);
2753 }
2754
2755 #[cfg(any(feature = "transport-webrtc", feature = "transport-moq"))]
2756 fn forget_iroh_carrier_peer_locked(&self, connection_id: &str) {
2757 self.wasm_remote_carrier_capabilities
2758 .borrow_mut()
2759 .remove(connection_id);
2760 self.inner
2761 .forget_iroh_carrier_peer_policy_epoch(connection_id);
2762 }
2763
2764 #[cfg(any(feature = "transport-webrtc", feature = "transport-moq"))]
2769 #[wasm_bindgen(js_name = __retireIrohCarrierPeer)]
2770 pub async fn retire_iroh_carrier_peer(
2771 &self,
2772 connection_id: String,
2773 terminal_reason: Option<String>,
2774 ) {
2775 let _lifecycle = self.wasm_carrier_peer_lifecycle.lock().await;
2776 #[cfg(feature = "transport-webrtc")]
2777 self.retire_iroh_webrtc_carrier_locked(
2778 connection_id.clone(),
2779 terminal_reason.as_deref(),
2780 )
2781 .await;
2782 #[cfg(feature = "transport-moq")]
2783 self.retire_iroh_moq_carrier_locked(connection_id.clone(), terminal_reason.as_deref())
2784 .await;
2785 self.forget_iroh_carrier_peer_locked(&connection_id);
2786 }
2787
2788 #[cfg(any(feature = "transport-webrtc", feature = "transport-moq"))]
2789 fn has_matching_in_flight_wasm_carrier_attempt(
2790 &self,
2791 connection_id: &str,
2792 capabilities: WasmRemoteCarrierCapabilities,
2793 ) -> bool {
2794 #[cfg(feature = "transport-webrtc")]
2795 if capabilities.webrtc
2796 && self
2797 .wasm_webrtc_carrier_attempts
2798 .borrow()
2799 .get(connection_id)
2800 .is_some_and(|attempt| {
2801 attempt.role == "responder" && !attempt.completion_started
2802 })
2803 {
2804 return true;
2805 }
2806 #[cfg(feature = "transport-moq")]
2807 if capabilities.moq
2808 && self
2809 .wasm_moq_carrier_attempts
2810 .borrow()
2811 .get(connection_id)
2812 .is_some_and(|attempt| attempt.role == "responder")
2813 {
2814 return true;
2815 }
2816 false
2817 }
2818
2819 #[cfg(any(feature = "transport-webrtc", feature = "transport-moq"))]
2820 #[cfg(any(feature = "transport-webrtc", feature = "transport-moq"))]
2823 #[wasm_bindgen(js_name = __beginPreferredIrohCarrier)]
2824 pub async fn begin_preferred_iroh_carrier(
2825 &self,
2826 connection_id: String,
2827 remote_endpoint_id: String,
2828 remote_supports_webrtc: bool,
2829 remote_supports_moq: bool,
2830 ) -> Result<bool, JsValue> {
2831 let _lifecycle = self.wasm_carrier_peer_lifecycle.lock().await;
2832 self.begin_preferred_iroh_carrier_locked(
2833 connection_id,
2834 remote_endpoint_id,
2835 remote_supports_webrtc,
2836 remote_supports_moq,
2837 )
2838 .await
2839 }
2840
2841 #[cfg(any(feature = "transport-webrtc", feature = "transport-moq"))]
2842 async fn begin_preferred_iroh_carrier_locked(
2843 &self,
2844 connection_id: String,
2845 remote_endpoint_id: String,
2846 remote_supports_webrtc: bool,
2847 remote_supports_moq: bool,
2848 ) -> Result<bool, JsValue> {
2849 let capabilities = WasmRemoteCarrierCapabilities {
2850 webrtc: remote_supports_webrtc,
2851 moq: remote_supports_moq,
2852 };
2853 let previous_capabilities = self
2854 .wasm_remote_carrier_capabilities
2855 .borrow()
2856 .get(&connection_id)
2857 .copied();
2858 if previous_capabilities != Some(capabilities) {
2859 let matching_first_attempt = previous_capabilities.is_none()
2864 && self
2865 .has_matching_in_flight_wasm_carrier_attempt(&connection_id, capabilities);
2866 if crate::client::should_bump_remote_carrier_peer_policy_epoch(
2867 previous_capabilities.is_some(),
2868 true,
2869 matching_first_attempt,
2870 ) {
2871 self.inner
2872 .bump_wasm_iroh_carrier_peer_policy_epoch(&connection_id)
2873 .await;
2874 }
2875 self.wasm_remote_carrier_capabilities
2876 .borrow_mut()
2877 .insert(connection_id.clone(), capabilities);
2878 }
2879 if self
2880 .inner
2881 .reconcile_wasm_iroh_carrier_policy(
2882 &connection_id,
2883 remote_supports_webrtc,
2884 remote_supports_moq,
2885 )
2886 .await
2887 .map_err(|error| JsValue::from_str(&error.to_string()))?
2888 {
2889 return Ok(true);
2890 }
2891 let ranked = self
2892 .inner
2893 .ranked_wasm_iroh_carriers(remote_supports_webrtc, remote_supports_moq)
2894 .await;
2895 match ranked.first() {
2896 #[cfg(feature = "transport-webrtc")]
2897 Some(crate::route_policy::KnownRoute::WebRtc) => {
2898 self.begin_iroh_webrtc_carrier_attempt(connection_id, remote_endpoint_id, 0)
2899 .await
2900 }
2901 #[cfg(feature = "transport-moq")]
2902 Some(crate::route_policy::KnownRoute::Moq) => {
2903 self.begin_iroh_moq_carrier_attempt(connection_id, remote_endpoint_id, 0)
2904 .await
2905 }
2906 _ => Ok(false),
2907 }
2908 }
2909
2910 #[cfg(any(feature = "transport-webrtc", feature = "transport-moq"))]
2913 #[wasm_bindgen(js_name = __advancePreferredIrohCarrier)]
2914 pub async fn advance_preferred_iroh_carrier(
2915 &self,
2916 connection_id: String,
2917 remote_endpoint_id: String,
2918 failed_route: String,
2919 ) -> Result<bool, JsValue> {
2920 let _lifecycle = self.wasm_carrier_peer_lifecycle.lock().await;
2921 let Some(capabilities) = self
2922 .wasm_remote_carrier_capabilities
2923 .borrow()
2924 .get(&connection_id)
2925 .copied()
2926 else {
2927 return Ok(false);
2928 };
2929 let failed = crate::route_policy::normalize_route(&failed_route)
2930 .ok_or_else(|| JsValue::from_str("unknown failed carrier route"))?;
2931 let ranked = self
2932 .inner
2933 .ranked_wasm_iroh_carriers(capabilities.webrtc, capabilities.moq)
2934 .await;
2935 let Some(next_index) = ranked
2936 .iter()
2937 .position(|route| *route == failed)
2938 .map(|index| index + 1)
2939 .filter(|index| *index < ranked.len())
2940 else {
2941 return Ok(false);
2942 };
2943 let next = ranked[next_index];
2944 match next {
2945 #[cfg(feature = "transport-webrtc")]
2946 crate::route_policy::KnownRoute::WebRtc => {
2947 self.begin_iroh_webrtc_carrier_attempt(connection_id, remote_endpoint_id, 0)
2948 .await
2949 }
2950 #[cfg(feature = "transport-moq")]
2951 crate::route_policy::KnownRoute::Moq => {
2952 self.begin_iroh_moq_carrier_attempt(connection_id, remote_endpoint_id, 0)
2953 .await
2954 }
2955 _ => Ok(false),
2956 }
2957 }
2958
2959 #[cfg(feature = "transport-moq")]
2960 #[wasm_bindgen(js_name = __configureMoqCarrier)]
2961 pub async fn configure_iroh_moq_carrier(
2962 &self,
2963 enabled: bool,
2964 relay_url: Option<String>,
2965 ) -> Result<(), JsValue> {
2966 let _lifecycle = self.wasm_carrier_peer_lifecycle.lock().await;
2967 let relay_url = relay_url.map(|value| value.trim().to_string());
2968 if enabled && relay_url.as_deref().unwrap_or_default().is_empty() {
2969 return Err(JsValue::from_str(
2970 "MoQ Iroh carrier requires an explicit relay URL",
2971 ));
2972 }
2973 self.inner.set_moq_carrier(enabled, relay_url).await;
2974 Ok(())
2975 }
2976
2977 #[cfg(feature = "transport-moq")]
2980 #[wasm_bindgen(js_name = __beginMoqCarrier)]
2981 pub async fn begin_iroh_moq_carrier(
2982 &self,
2983 connection_id: String,
2984 remote_endpoint_id: String,
2985 ) -> Result<bool, JsValue> {
2986 let _lifecycle = self.wasm_carrier_peer_lifecycle.lock().await;
2987 self.begin_iroh_moq_carrier_attempt(connection_id, remote_endpoint_id, 0)
2988 .await
2989 }
2990
2991 #[cfg(feature = "transport-moq")]
2992 async fn begin_iroh_moq_carrier_attempt(
2993 &self,
2994 connection_id: String,
2995 remote_endpoint_id: String,
2996 retry_count: u8,
2997 ) -> Result<bool, JsValue> {
2998 if !self.inner.is_moq_carrier_enabled().await {
2999 return Ok(false);
3000 }
3001 let local_endpoint_id = self
3002 .inner
3003 .current_node_id()
3004 .await
3005 .ok_or_else(|| JsValue::from_str("local Iroh endpoint id is unavailable"))?;
3006 if local_endpoint_id.as_str() <= remote_endpoint_id.as_str()
3007 || !crate::iroh_connection_policy::custom_carrier_base_allows(
3008 self.inner.iroh_path_kind(&remote_endpoint_id).await,
3009 crate::client::IrohPathKind::Moq,
3010 false,
3011 )
3012 {
3013 return Ok(false);
3014 }
3015 let endpoint_id = remote_endpoint_id
3016 .parse::<iroh::EndpointId>()
3017 .map_err(|error| JsValue::from_str(&error.to_string()))?;
3018 self.inner
3019 .get_connection(endpoint_id)
3020 .await
3021 .ok_or_else(|| JsValue::from_str("MoQ carrier base is unavailable"))?;
3022 let generation = self
3023 .inner
3024 .current_wasm_peer_data_generation(&connection_id, None)
3025 .await
3026 .ok_or_else(|| JsValue::from_str("MoQ carrier generation is unavailable"))?;
3027 let bootstrap = crate::iroh_carrier_bootstrap::CarrierBootstrapFrame::request(
3028 crate::iroh_carrier_bootstrap::CarrierBootstrapKind::MoqDraft14,
3029 crate::iroh_carrier_bootstrap::CarrierGenerationFence {
3030 transport_stable_id: generation.transport_stable_id,
3031 transport_generation: generation.transport_generation,
3032 route_generation: generation.route_generation,
3033 },
3034 retry_count.saturating_add(1),
3035 )
3036 .map_err(|error| JsValue::from_str(&error.to_string()))?;
3037 let kind = crate::client::IrohPathKind::Moq;
3038 if !matches!(
3039 self.inner
3040 .reserve_wasm_carrier_upgrade(
3041 &connection_id,
3042 kind,
3043 &bootstrap.upgrade_id,
3044 generation,
3045 )
3046 .await,
3047 crate::client::WasmCarrierUpgradeReservation::Reserved
3048 ) {
3049 let pending = self
3050 .wasm_moq_carrier_attempts
3051 .borrow_mut()
3052 .get_mut(&connection_id)
3053 .filter(|attempt| {
3054 attempt.role == "initiator"
3055 && attempt.generation == generation
3056 && attempt.prepared
3057 && !attempt.retry_sent
3058 })
3059 .map(|attempt| {
3060 attempt.retry_sent = true;
3061 attempt.clone()
3062 });
3063 if let Some(pending) = pending {
3064 emit_wasm_carrier_action(
3070 &self.wasm_carrier_action_handler,
3071 serde_json::json!({
3072 "type": "send-control",
3073 "connectionId": connection_id,
3074 "remoteEndpointId": pending.remote_endpoint_id.to_string(),
3075 "envelope": pending.bootstrap,
3076 }),
3077 );
3078 return Ok(true);
3079 }
3080 return Ok(false);
3081 }
3082 let previous = self.wasm_moq_carrier_attempts.borrow_mut().insert(
3083 connection_id.clone(),
3084 WasmMoqCarrierAttempt {
3085 connection_id: connection_id.clone(),
3086 remote_endpoint_id: endpoint_id,
3087 bootstrap: bootstrap.clone(),
3088 generation,
3089 role: "initiator",
3090 prepared: false,
3091 retry_sent: false,
3092 retry_count,
3093 inbound_authorization_expires_at_ms: None,
3094 },
3095 );
3096 if let Some(previous) = previous {
3097 self.wasm_moq_carrier_sessions
3098 .borrow_mut()
3099 .remove(&previous.bootstrap.upgrade_id);
3100 }
3101 let (publish_namespace, subscribe_namespace, track_name) = wasm_moq_carrier_namespaces(
3102 &local_endpoint_id,
3103 &remote_endpoint_id,
3104 &bootstrap.carrier_session_id,
3105 );
3106 emit_wasm_carrier_action(
3107 &self.wasm_carrier_action_handler,
3108 serde_json::json!({
3109 "type": "prepare-moq",
3110 "connectionId": connection_id,
3111 "remoteEndpointId": remote_endpoint_id,
3112 "role": "initiator",
3113 "upgradeId": bootstrap.upgrade_id,
3114 "carrierSessionId": bootstrap.carrier_session_id,
3115 "transportGeneration": generation.transport_generation.saturating_add(1),
3116 "publishNamespace": publish_namespace,
3117 "subscribeNamespace": subscribe_namespace,
3118 "trackName": track_name,
3119 }),
3120 );
3121 self.schedule_wasm_moq_carrier_watchdog(bootstrap);
3122 Ok(true)
3123 }
3124
3125 #[cfg(feature = "transport-moq")]
3126 #[wasm_bindgen(js_name = __moqCarrierPrepared)]
3127 pub async fn iroh_moq_carrier_prepared(
3128 &self,
3129 connection_id: String,
3130 upgrade_id: String,
3131 ) -> Result<(), JsValue> {
3132 let _lifecycle = self.wasm_carrier_peer_lifecycle.lock().await;
3133 let attempt = {
3134 let mut attempts = self.wasm_moq_carrier_attempts.borrow_mut();
3135 let attempt = attempts
3136 .get_mut(&connection_id)
3137 .filter(|attempt| attempt.bootstrap.upgrade_id == upgrade_id)
3138 .ok_or_else(|| JsValue::from_str("MoQ carrier attempt is stale"))?;
3139 attempt.prepared = true;
3140 attempt.clone()
3141 };
3142 if !self
3143 .inner
3144 .wasm_carrier_upgrade_is_current(
3145 &connection_id,
3146 crate::client::IrohPathKind::Moq,
3147 &upgrade_id,
3148 attempt.generation,
3149 )
3150 .await
3151 {
3152 return Err(JsValue::from_str("MoQ carrier attempt was retired"));
3153 }
3154 if attempt.role == "initiator" {
3155 emit_wasm_carrier_action(
3156 &self.wasm_carrier_action_handler,
3157 serde_json::json!({
3158 "type": "send-control",
3159 "connectionId": connection_id,
3160 "remoteEndpointId": attempt.remote_endpoint_id.to_string(),
3161 "envelope": attempt.bootstrap,
3162 }),
3163 );
3164 } else {
3165 emit_wasm_carrier_action(
3171 &self.wasm_carrier_action_handler,
3172 serde_json::json!({
3173 "type": "activate-moq",
3174 "connectionId": attempt.connection_id,
3175 "upgradeId": upgrade_id,
3176 }),
3177 );
3178 }
3179 Ok(())
3180 }
3181
3182 #[cfg(feature = "transport-moq")]
3183 #[wasm_bindgen(js_name = __moqCarrierFailed)]
3184 pub async fn iroh_moq_carrier_failed(
3185 &self,
3186 connection_id: String,
3187 upgrade_id: String,
3188 failure_code: String,
3189 ) {
3190 let _lifecycle = self.wasm_carrier_peer_lifecycle.lock().await;
3191 let attempt = self
3192 .wasm_moq_carrier_attempts
3193 .borrow()
3194 .get(&connection_id)
3195 .filter(|attempt| attempt.bootstrap.upgrade_id == upgrade_id)
3196 .cloned();
3197 if let Some(attempt) = attempt {
3198 let failure_code = match failure_code.as_str() {
3199 "relay-failed" => "relay-failed",
3200 "draft14-datagram-unavailable" => "draft14-datagram-unavailable",
3201 "carrier-backpressure" => "carrier-backpressure",
3202 _ => "browser-adapter-failed",
3203 };
3204 if retire_selected_wasm_moq_carrier(
3205 self.inner.clone(),
3206 self.wasm_moq_carrier_attempts.clone(),
3207 self.wasm_moq_carrier_sessions.clone(),
3208 self.wasm_carrier_action_handler.clone(),
3209 &attempt,
3210 failure_code,
3211 crate::lifecycle_reason::REASON_IROH_CARRIER_FAILED,
3212 )
3213 .await
3214 {
3215 return;
3216 }
3217 self.fail_wasm_moq_carrier_attempt(attempt, failure_code, true)
3218 .await;
3219 }
3220 }
3221
3222 #[cfg(feature = "transport-moq")]
3223 #[wasm_bindgen(js_name = __retireMoqCarrier)]
3224 pub async fn retire_iroh_moq_carrier(
3225 &self,
3226 connection_id: String,
3227 terminal_reason: Option<String>,
3228 ) {
3229 let _lifecycle = self.wasm_carrier_peer_lifecycle.lock().await;
3230 self.retire_iroh_moq_carrier_locked(connection_id, terminal_reason.as_deref())
3231 .await;
3232 }
3233
3234 #[cfg(feature = "transport-moq")]
3235 async fn retire_iroh_moq_carrier_locked(
3236 &self,
3237 connection_id: String,
3238 terminal_reason: Option<&str>,
3239 ) {
3240 let attempt = self
3241 .wasm_moq_carrier_attempts
3242 .borrow_mut()
3243 .remove(&connection_id);
3244 if let Some(attempt) = attempt {
3245 self.inner
3246 .retire_wasm_carrier_upgrade(
3247 &connection_id,
3248 crate::client::IrohPathKind::Moq,
3249 &attempt.bootstrap.upgrade_id,
3250 attempt.generation,
3251 )
3252 .await;
3253 let carrier = self
3254 .wasm_moq_carrier_sessions
3255 .borrow_mut()
3256 .remove(&attempt.bootstrap.upgrade_id);
3257 if let (Some(reason), Some(session)) = (terminal_reason, carrier.as_ref()) {
3258 let _ = session.send_terminal(reason).await;
3259 }
3260 emit_wasm_carrier_action(
3261 &self.wasm_carrier_action_handler,
3262 serde_json::json!({
3263 "type": "retire-moq",
3264 "connectionId": connection_id,
3265 "upgradeId": attempt.bootstrap.upgrade_id,
3266 "failureCode": "logical-connection-retired",
3267 }),
3268 );
3269 }
3270 }
3271
3272 #[cfg(feature = "transport-webrtc")]
3275 #[wasm_bindgen(js_name = __beginWebRtcCarrier)]
3276 pub async fn begin_iroh_webrtc_carrier(
3277 &self,
3278 connection_id: String,
3279 remote_endpoint_id: String,
3280 ) -> Result<bool, JsValue> {
3281 let _lifecycle = self.wasm_carrier_peer_lifecycle.lock().await;
3282 self.begin_iroh_webrtc_carrier_attempt(connection_id, remote_endpoint_id, 0)
3283 .await
3284 }
3285
3286 #[cfg(feature = "transport-webrtc")]
3287 async fn begin_iroh_webrtc_carrier_attempt(
3288 &self,
3289 connection_id: String,
3290 remote_endpoint_id: String,
3291 retry_count: u8,
3292 ) -> Result<bool, JsValue> {
3293 if !self.inner.is_webrtc_carrier_enabled().await {
3294 return Ok(false);
3295 }
3296 let local_endpoint_id = self
3297 .inner
3298 .current_node_id()
3299 .await
3300 .ok_or_else(|| JsValue::from_str("local Iroh endpoint id is unavailable"))?;
3301 if local_endpoint_id.as_str() <= remote_endpoint_id.as_str()
3302 || !crate::iroh_connection_policy::custom_carrier_base_allows(
3303 self.inner.iroh_path_kind(&remote_endpoint_id).await,
3304 crate::client::IrohPathKind::WebRtc,
3305 false,
3306 )
3307 {
3308 return Ok(false);
3309 }
3310 let endpoint_id = remote_endpoint_id
3311 .parse::<iroh::EndpointId>()
3312 .map_err(|error| JsValue::from_str(&error.to_string()))?;
3313 self.inner
3314 .get_connection(endpoint_id)
3315 .await
3316 .ok_or_else(|| JsValue::from_str("WebRTC carrier base is unavailable"))?;
3317 let generation = self
3318 .inner
3319 .current_wasm_peer_data_generation(&connection_id, None)
3320 .await
3321 .ok_or_else(|| JsValue::from_str("WebRTC carrier generation is unavailable"))?;
3322 let bootstrap = crate::iroh_carrier_bootstrap::CarrierBootstrapFrame::request(
3323 crate::iroh_carrier_bootstrap::CarrierBootstrapKind::WebRtc,
3324 crate::iroh_carrier_bootstrap::CarrierGenerationFence {
3325 transport_stable_id: generation.transport_stable_id,
3326 transport_generation: generation.transport_generation,
3327 route_generation: generation.route_generation,
3328 },
3329 1,
3330 )
3331 .map_err(|error| JsValue::from_str(&error.to_string()))?;
3332 let kind = crate::client::IrohPathKind::WebRtc;
3333 if !matches!(
3334 self.inner
3335 .reserve_wasm_carrier_upgrade(
3336 &connection_id,
3337 kind,
3338 &bootstrap.upgrade_id,
3339 generation,
3340 )
3341 .await,
3342 crate::client::WasmCarrierUpgradeReservation::Reserved
3343 ) {
3344 let pending = self
3345 .wasm_webrtc_carrier_attempts
3346 .borrow_mut()
3347 .get_mut(&connection_id)
3348 .filter(|attempt| {
3349 attempt.role == "initiator"
3350 && attempt.generation == generation
3351 && attempt.prepared
3352 && !attempt.retry_sent
3353 && !attempt.completion_started
3354 })
3355 .map(|attempt| {
3356 attempt.retry_sent = true;
3357 attempt.clone()
3358 });
3359 if let Some(pending) = pending {
3360 emit_wasm_carrier_action(
3364 &self.wasm_carrier_action_handler,
3365 serde_json::json!({
3366 "type": "send-control",
3367 "connectionId": connection_id,
3368 "remoteEndpointId": pending.remote_endpoint_id.to_string(),
3369 "envelope": pending.bootstrap,
3370 }),
3371 );
3372 return Ok(true);
3373 }
3374 return Ok(false);
3375 }
3376 let previous = self.wasm_webrtc_carrier_attempts.borrow_mut().insert(
3377 connection_id.clone(),
3378 WasmWebRtcCarrierAttempt {
3379 connection_id: connection_id.clone(),
3380 remote_endpoint_id: endpoint_id,
3381 bootstrap: bootstrap.clone(),
3382 generation,
3383 role: "initiator",
3384 prepared: false,
3385 retry_sent: false,
3386 offer_started: false,
3387 remote_ready: false,
3388 completion_started: false,
3389 retry_count,
3390 inbound_authorization_expires_at_ms: None,
3391 },
3392 );
3393 if let Some(previous) = previous {
3394 self.wasm_webrtc_carrier_sessions
3395 .borrow_mut()
3396 .remove(&previous.bootstrap.upgrade_id);
3397 }
3398 emit_wasm_carrier_action(
3399 &self.wasm_carrier_action_handler,
3400 serde_json::json!({
3401 "type": "prepare-webrtc",
3402 "connectionId": connection_id,
3403 "remoteEndpointId": remote_endpoint_id,
3404 "role": "initiator",
3405 "upgradeId": bootstrap.upgrade_id,
3406 "carrierSessionId": bootstrap.carrier_session_id,
3407 "transportGeneration": generation.transport_generation.saturating_add(1),
3408 }),
3409 );
3410 self.schedule_wasm_webrtc_carrier_watchdog(bootstrap);
3411 Ok(true)
3412 }
3413
3414 #[cfg(feature = "transport-webrtc")]
3417 #[wasm_bindgen(js_name = __webRtcCarrierPrepared)]
3418 pub async fn iroh_webrtc_carrier_prepared(
3419 &self,
3420 connection_id: String,
3421 upgrade_id: String,
3422 ) -> Result<(), JsValue> {
3423 let _lifecycle = self.wasm_carrier_peer_lifecycle.lock().await;
3424 let attempt = {
3425 let mut attempts = self.wasm_webrtc_carrier_attempts.borrow_mut();
3426 let attempt = attempts
3427 .get_mut(&connection_id)
3428 .filter(|attempt| attempt.bootstrap.upgrade_id == upgrade_id)
3429 .ok_or_else(|| JsValue::from_str("WebRTC carrier attempt is stale"))?;
3430 attempt.prepared = true;
3431 attempt.clone()
3432 };
3433 let kind = crate::client::IrohPathKind::WebRtc;
3434 if !self
3435 .inner
3436 .wasm_carrier_upgrade_is_current(
3437 &connection_id,
3438 kind,
3439 &upgrade_id,
3440 attempt.generation,
3441 )
3442 .await
3443 {
3444 return Err(JsValue::from_str("WebRTC carrier attempt was retired"));
3445 }
3446 if attempt.role == "initiator" {
3447 emit_wasm_carrier_action(
3448 &self.wasm_carrier_action_handler,
3449 serde_json::json!({
3450 "type": "send-control",
3451 "connectionId": connection_id,
3452 "remoteEndpointId": attempt.remote_endpoint_id.to_string(),
3453 "envelope": attempt.bootstrap,
3454 }),
3455 );
3456 } else {
3457 emit_wasm_carrier_action(
3462 &self.wasm_carrier_action_handler,
3463 serde_json::json!({
3464 "type": "send-control",
3465 "connectionId": attempt.connection_id,
3466 "remoteEndpointId": attempt.remote_endpoint_id.to_string(),
3467 "envelope": {
3468 "type": "#pluto-signal",
3469 "content": {
3470 "transport": "webrtc",
3471 "type": "renegotiate",
3472 "negotiationId": upgrade_id,
3473 }
3474 },
3475 }),
3476 );
3477 }
3478 Ok(())
3479 }
3480
3481 #[cfg(feature = "transport-webrtc")]
3482 #[wasm_bindgen(js_name = __webRtcCarrierFailed)]
3483 pub async fn iroh_webrtc_carrier_failed(
3484 &self,
3485 connection_id: String,
3486 upgrade_id: String,
3487 failure_code: String,
3488 ) {
3489 let _lifecycle = self.wasm_carrier_peer_lifecycle.lock().await;
3490 let attempt = self
3491 .wasm_webrtc_carrier_attempts
3492 .borrow()
3493 .get(&connection_id)
3494 .filter(|attempt| attempt.bootstrap.upgrade_id == upgrade_id)
3495 .cloned();
3496 if let Some(attempt) = attempt {
3497 let failure_code = match failure_code.as_str() {
3498 "data-channel-failed" => "data-channel-failed",
3499 "ice-failed" => "ice-failed",
3500 "signaling-failed" => "signaling-failed",
3501 "carrier-backpressure" => "carrier-backpressure",
3502 "carrier-base-generation-stale" => "carrier-base-generation-stale",
3503 _ => "browser-adapter-failed",
3504 };
3505 if retire_selected_wasm_webrtc_carrier(
3506 self.inner.clone(),
3507 self.wasm_webrtc_carrier_attempts.clone(),
3508 self.wasm_webrtc_carrier_sessions.clone(),
3509 self.wasm_carrier_action_handler.clone(),
3510 &attempt,
3511 failure_code,
3512 crate::lifecycle_reason::REASON_IROH_CARRIER_FAILED,
3513 )
3514 .await
3515 {
3516 return;
3517 }
3518 let should_retry = attempt.role == "initiator"
3519 && attempt.retry_count == 0
3520 && matches!(
3521 failure_code,
3522 "data-channel-failed"
3523 | "ice-failed"
3524 | "signaling-failed"
3525 | "carrier-base-generation-stale"
3526 );
3527 let remote_endpoint_id = attempt.remote_endpoint_id.to_string();
3528 self.fail_wasm_webrtc_carrier_attempt(attempt, failure_code, true)
3529 .await;
3530 if should_retry {
3531 gloo_timers::future::sleep(std::time::Duration::from_millis(500)).await;
3536 let _ = self
3537 .begin_iroh_webrtc_carrier_attempt(connection_id, remote_endpoint_id, 1)
3538 .await;
3539 }
3540 }
3541 }
3542
3543 #[cfg(feature = "transport-webrtc")]
3544 #[wasm_bindgen(js_name = __retireWebRtcCarrier)]
3545 pub async fn retire_iroh_webrtc_carrier(
3546 &self,
3547 connection_id: String,
3548 terminal_reason: Option<String>,
3549 ) {
3550 let _lifecycle = self.wasm_carrier_peer_lifecycle.lock().await;
3551 self.retire_iroh_webrtc_carrier_locked(connection_id, terminal_reason.as_deref())
3552 .await;
3553 }
3554
3555 #[cfg(feature = "transport-webrtc")]
3556 async fn retire_iroh_webrtc_carrier_locked(
3557 &self,
3558 connection_id: String,
3559 terminal_reason: Option<&str>,
3560 ) {
3561 let attempt = self
3562 .wasm_webrtc_carrier_attempts
3563 .borrow_mut()
3564 .remove(&connection_id);
3565 if let Some(attempt) = attempt {
3566 self.inner
3567 .retire_wasm_carrier_upgrade(
3568 &connection_id,
3569 crate::client::IrohPathKind::WebRtc,
3570 &attempt.bootstrap.upgrade_id,
3571 attempt.generation,
3572 )
3573 .await;
3574 let carrier = self
3575 .wasm_webrtc_carrier_sessions
3576 .borrow_mut()
3577 .remove(&attempt.bootstrap.upgrade_id);
3578 if let (Some(reason), Some(session)) = (terminal_reason, carrier.as_ref()) {
3579 let _ = session.send_terminal(reason).await;
3580 }
3581 emit_wasm_carrier_action(
3582 &self.wasm_carrier_action_handler,
3583 serde_json::json!({
3584 "type": "retire-webrtc",
3585 "connectionId": connection_id,
3586 "upgradeId": attempt.bootstrap.upgrade_id,
3587 "failureCode": "logical-connection-retired",
3588 }),
3589 );
3590 }
3591 }
3592
3593 #[cfg(feature = "transport-webrtc")]
3596 #[wasm_bindgen(js_name = __handleIrohCarrierControl)]
3597 pub async fn handle_iroh_carrier_control(
3598 &self,
3599 connection_id: String,
3600 remote_endpoint_id: String,
3601 frame: JsValue,
3602 ) -> Result<bool, JsValue> {
3603 let _lifecycle = self.wasm_carrier_peer_lifecycle.lock().await;
3604 let frame: serde_json::Value = serde_wasm_bindgen::from_value(frame)
3605 .map_err(|error| JsValue::from_str(&error.to_string()))?;
3606 if frame.get("type").and_then(serde_json::Value::as_str) == Some("#pluto-signal")
3607 && frame
3608 .get("content")
3609 .and_then(|content| content.get("transport"))
3610 .and_then(serde_json::Value::as_str)
3611 == Some("webrtc")
3612 {
3613 let negotiation_id = frame
3614 .get("content")
3615 .and_then(|content| content.get("negotiationId"))
3616 .and_then(serde_json::Value::as_str)
3617 .unwrap_or_default();
3618 let current = self
3619 .wasm_webrtc_carrier_attempts
3620 .borrow()
3621 .get(&connection_id)
3622 .is_some_and(|attempt| {
3623 attempt.bootstrap.upgrade_id == negotiation_id
3624 && attempt.remote_endpoint_id.to_string() == remote_endpoint_id
3625 });
3626 if current {
3627 let signal_type = frame
3628 .get("content")
3629 .and_then(|content| content.get("type"))
3630 .and_then(serde_json::Value::as_str);
3631 let start_offer = if signal_type == Some("renegotiate") {
3632 self.wasm_webrtc_carrier_attempts
3633 .borrow_mut()
3634 .get_mut(&connection_id)
3635 .filter(|attempt| {
3636 attempt.bootstrap.upgrade_id == negotiation_id
3637 && attempt.role == "initiator"
3638 && !attempt.offer_started
3639 })
3640 .map(|attempt| {
3641 attempt.offer_started = true;
3642 })
3643 .is_some()
3644 } else {
3645 false
3646 };
3647 if start_offer {
3648 emit_wasm_carrier_action(
3649 &self.wasm_carrier_action_handler,
3650 serde_json::json!({
3651 "type": "start-webrtc",
3652 "connectionId": connection_id,
3653 "upgradeId": negotiation_id,
3654 }),
3655 );
3656 return Ok(true);
3657 }
3658 emit_wasm_carrier_action(
3659 &self.wasm_carrier_action_handler,
3660 serde_json::json!({
3661 "type": "apply-webrtc-signal",
3662 "connectionId": connection_id,
3663 "upgradeId": negotiation_id,
3664 "signal": frame,
3665 }),
3666 );
3667 }
3668 return Ok(true);
3669 }
3670
3671 let Some(bootstrap) =
3672 crate::iroh_carrier_bootstrap::CarrierBootstrapFrame::from_json(&frame)
3673 else {
3674 return Ok(false);
3675 };
3676 #[cfg(feature = "transport-moq")]
3677 if bootstrap.carrier == crate::iroh_carrier_bootstrap::CarrierBootstrapKind::MoqDraft14
3678 {
3679 return self
3680 .handle_wasm_moq_bootstrap(connection_id, remote_endpoint_id, bootstrap)
3681 .await;
3682 }
3683 if bootstrap.carrier != crate::iroh_carrier_bootstrap::CarrierBootstrapKind::WebRtc {
3684 return Ok(false);
3685 }
3686 let endpoint_id = remote_endpoint_id
3687 .parse::<iroh::EndpointId>()
3688 .map_err(|error| JsValue::from_str(&error.to_string()))?;
3689 let kind = crate::client::IrohPathKind::WebRtc;
3690 match bootstrap.action {
3691 crate::iroh_carrier_bootstrap::CarrierBootstrapAction::Request => {
3692 let local_endpoint_id = self
3693 .inner
3694 .current_node_id()
3695 .await
3696 .ok_or_else(|| JsValue::from_str("local endpoint id is unavailable"))?;
3697 if local_endpoint_id.as_str() >= remote_endpoint_id.as_str()
3698 || !self.inner.is_webrtc_carrier_enabled().await
3699 {
3700 return Ok(true);
3701 }
3702 if !crate::iroh_connection_policy::custom_carrier_base_allows(
3703 self.inner.iroh_path_kind(&remote_endpoint_id).await,
3704 crate::client::IrohPathKind::WebRtc,
3705 false,
3706 ) {
3707 return Ok(true);
3708 }
3709 let base_connection = self
3710 .inner
3711 .get_connection(endpoint_id)
3712 .await
3713 .ok_or_else(|| JsValue::from_str("WebRTC carrier base is unavailable"))?;
3714 let generation = self
3715 .inner
3716 .current_wasm_peer_data_generation(&connection_id, None)
3717 .await
3718 .ok_or_else(|| {
3719 JsValue::from_str("WebRTC carrier generation is unavailable")
3720 })?;
3721 if crate::transport_generation::for_connection(&base_connection)
3722 != generation.transport_stable_id
3723 {
3724 let failed =
3729 crate::iroh_carrier_bootstrap::CarrierBootstrapFrame::failed_from(
3730 &bootstrap,
3731 "carrier-base-generation-stale",
3732 )
3733 .map_err(|error| JsValue::from_str(&error.to_string()))?;
3734 emit_wasm_carrier_action(
3735 &self.wasm_carrier_action_handler,
3736 serde_json::json!({
3737 "type": "send-control",
3738 "connectionId": connection_id,
3739 "remoteEndpointId": remote_endpoint_id,
3740 "envelope": failed,
3741 }),
3742 );
3743 return Ok(true);
3744 }
3745 if !matches!(
3746 self.inner
3747 .reserve_wasm_carrier_upgrade(
3748 &connection_id,
3749 kind,
3750 &bootstrap.upgrade_id,
3751 generation,
3752 )
3753 .await,
3754 crate::client::WasmCarrierUpgradeReservation::Reserved
3755 ) {
3756 return Ok(true);
3757 }
3758 let node = self
3759 .inner
3760 .iroh_node
3761 .read()
3762 .await
3763 .as_ref()
3764 .cloned()
3765 .ok_or_else(|| JsValue::from_str("Iroh node is unavailable"))?;
3766 let authorization_expiry = node
3767 .authorize_pending_inbound_replacement(
3768 endpoint_id,
3769 crate::iroh_carrier_kind::EXPERIMENTAL_WEBRTC_TRANSPORT_ID,
3770 std::time::Duration::from_secs(45),
3771 )
3772 .await;
3773 let previous = self.wasm_webrtc_carrier_attempts.borrow_mut().insert(
3774 connection_id.clone(),
3775 WasmWebRtcCarrierAttempt {
3776 connection_id: connection_id.clone(),
3777 remote_endpoint_id: endpoint_id,
3778 bootstrap: bootstrap.clone(),
3779 generation,
3780 role: "responder",
3781 prepared: false,
3782 retry_sent: false,
3783 offer_started: false,
3784 remote_ready: false,
3785 completion_started: false,
3786 retry_count: bootstrap.attempt.saturating_sub(1),
3787 inbound_authorization_expires_at_ms: Some(authorization_expiry),
3788 },
3789 );
3790 if let Some(previous) = previous {
3791 self.wasm_webrtc_carrier_sessions
3792 .borrow_mut()
3793 .remove(&previous.bootstrap.upgrade_id);
3794 }
3795 emit_wasm_carrier_action(
3796 &self.wasm_carrier_action_handler,
3797 serde_json::json!({
3798 "type": "prepare-webrtc",
3799 "connectionId": connection_id,
3800 "remoteEndpointId": remote_endpoint_id,
3801 "role": "responder",
3802 "upgradeId": bootstrap.upgrade_id,
3803 "carrierSessionId": bootstrap.carrier_session_id,
3804 "transportGeneration": generation.transport_generation.saturating_add(1),
3805 }),
3806 );
3807 self.schedule_wasm_webrtc_carrier_watchdog(bootstrap);
3808 }
3809 crate::iroh_carrier_bootstrap::CarrierBootstrapAction::Ready => {
3810 let upgrade_id = {
3811 let mut attempts = self.wasm_webrtc_carrier_attempts.borrow_mut();
3812 attempts
3813 .get_mut(&connection_id)
3814 .filter(|attempt| bootstrap.is_response_to(&attempt.bootstrap))
3815 .map(|attempt| {
3816 attempt.remote_ready = true;
3817 attempt.bootstrap.upgrade_id.clone()
3818 })
3819 };
3820 let attempt = upgrade_id.as_deref().and_then(|upgrade_id| {
3821 self.take_ready_outbound_wasm_webrtc_carrier_attempt(
3822 &connection_id,
3823 upgrade_id,
3824 )
3825 });
3826 if let Some(attempt) = attempt {
3827 self.spawn_outbound_wasm_webrtc_carrier_completion(attempt);
3828 }
3829 }
3830 crate::iroh_carrier_bootstrap::CarrierBootstrapAction::Failed => {
3831 let attempt = self
3832 .wasm_webrtc_carrier_attempts
3833 .borrow()
3834 .get(&connection_id)
3835 .filter(|attempt| bootstrap.is_response_to(&attempt.bootstrap))
3836 .cloned();
3837 if let Some(attempt) = attempt {
3838 let failure_code = crate::client::wasm_peer_carrier_failure_code(
3839 bootstrap.failure_code.as_deref(),
3840 );
3841 if crate::client::should_rearm_wasm_carrier_event_retry(
3842 failure_code,
3843 attempt.role == "initiator",
3844 attempt.completion_started,
3845 ) {
3846 if let Some(current) = self
3847 .wasm_webrtc_carrier_attempts
3848 .borrow_mut()
3849 .get_mut(&connection_id)
3850 .filter(|current| {
3851 current.bootstrap.upgrade_id == attempt.bootstrap.upgrade_id
3852 && !current.completion_started
3853 })
3854 {
3855 current.retry_sent = false;
3856 }
3857 return Ok(true);
3858 }
3859 let should_retry = attempt.role == "initiator"
3860 && attempt.retry_count == 0
3861 && failure_code == "carrier-base-generation-stale";
3862 let remote_endpoint_id = attempt.remote_endpoint_id.to_string();
3863 self.fail_wasm_webrtc_carrier_attempt(attempt, failure_code, false)
3864 .await;
3865 if should_retry {
3866 gloo_timers::future::sleep(std::time::Duration::from_millis(500)).await;
3867 let _ = self
3868 .begin_iroh_webrtc_carrier_attempt(
3869 connection_id,
3870 remote_endpoint_id,
3871 1,
3872 )
3873 .await;
3874 }
3875 }
3876 }
3877 }
3878 Ok(true)
3879 }
3880
3881 #[cfg(all(feature = "transport-moq", not(feature = "transport-webrtc")))]
3882 #[wasm_bindgen(js_name = __handleIrohCarrierControl)]
3883 pub async fn handle_iroh_carrier_control_moq_only(
3884 &self,
3885 connection_id: String,
3886 remote_endpoint_id: String,
3887 frame: JsValue,
3888 ) -> Result<bool, JsValue> {
3889 let _lifecycle = self.wasm_carrier_peer_lifecycle.lock().await;
3890 let frame: serde_json::Value = serde_wasm_bindgen::from_value(frame)
3891 .map_err(|error| JsValue::from_str(&error.to_string()))?;
3892 let Some(bootstrap) =
3893 crate::iroh_carrier_bootstrap::CarrierBootstrapFrame::from_json(&frame)
3894 else {
3895 return Ok(false);
3896 };
3897 if bootstrap.carrier != crate::iroh_carrier_bootstrap::CarrierBootstrapKind::MoqDraft14
3898 {
3899 return Ok(false);
3900 }
3901 self.handle_wasm_moq_bootstrap(connection_id, remote_endpoint_id, bootstrap)
3902 .await
3903 }
3904
3905 #[cfg(feature = "transport-webrtc")]
3909 #[wasm_bindgen(js_name = __attachWebRtcCarrier)]
3910 pub async fn attach_iroh_webrtc_carrier(
3911 &self,
3912 connection_id: String,
3913 remote_endpoint_id: String,
3914 channel: web_sys::RtcDataChannel,
3915 upgrade_id: String,
3916 ) -> Result<(), JsValue> {
3917 let _lifecycle = self.wasm_carrier_peer_lifecycle.lock().await;
3918 let endpoint_id = remote_endpoint_id
3919 .parse::<iroh::EndpointId>()
3920 .map_err(|error| JsValue::from_str(&error.to_string()))?;
3921 let attempt = self
3922 .wasm_webrtc_carrier_attempts
3923 .borrow()
3924 .get(&connection_id)
3925 .filter(|attempt| {
3926 attempt.bootstrap.upgrade_id == upgrade_id
3927 && attempt.remote_endpoint_id == endpoint_id
3928 })
3929 .cloned()
3930 .ok_or_else(|| JsValue::from_str("WebRTC carrier attempt is stale"))?;
3931 if !self
3932 .inner
3933 .wasm_carrier_upgrade_is_current(
3934 &connection_id,
3935 crate::client::IrohPathKind::WebRtc,
3936 &upgrade_id,
3937 attempt.generation,
3938 )
3939 .await
3940 {
3941 return Err(JsValue::from_str("WebRTC carrier attempt was retired"));
3942 }
3943 let packet_session = self
3944 .inner
3945 .activate_iroh_packet_carrier(
3946 crate::iroh_carrier_kind::IrohCarrierKind::WebRtc,
3947 endpoint_id,
3948 )
3949 .await
3950 .map_err(|error| JsValue::from_str(&error.to_string()))?;
3951 let expected = attempt
3952 .bootstrap
3953 .frame_expectation()
3954 .map_err(|error| JsValue::from_str(&error.to_string()))?;
3955 let application_key = self
3956 .inner
3957 .application_crypto_key_for_connection(Some(&connection_id))
3958 .ok_or_else(|| {
3959 JsValue::from_str("WebRTC carrier requires the admitted application crypto key")
3960 })?;
3961 let terminal_client = self.inner.clone();
3962 let terminal_connection_id = connection_id.clone();
3963 let terminal_upgrade_id = upgrade_id.clone();
3964 let terminal_generation = attempt.generation.transport_generation.saturating_add(1);
3965 let terminal_attempts = self.wasm_webrtc_carrier_attempts.clone();
3966 let terminal_sessions = self.wasm_webrtc_carrier_sessions.clone();
3967 let terminal_handler = self.wasm_carrier_action_handler.clone();
3968 let terminal_lifecycle = self.wasm_carrier_peer_lifecycle.clone();
3969 let on_terminal: Rc<dyn Fn(&'static str)> = Rc::new(move |reason| {
3970 let client = terminal_client.clone();
3971 let connection_id = terminal_connection_id.clone();
3972 let upgrade_id = terminal_upgrade_id.clone();
3973 let attempts = terminal_attempts.clone();
3974 let sessions = terminal_sessions.clone();
3975 let handler = terminal_handler.clone();
3976 let lifecycle = terminal_lifecycle.clone();
3977 spawn_local(async move {
3978 let _lifecycle = lifecycle.lock().await;
3979 if !client
3980 .close_current_iroh_carrier_generation_with_reason(
3981 &connection_id,
3982 endpoint_id,
3983 crate::client::IrohPathKind::WebRtc,
3984 terminal_generation,
3985 reason,
3986 )
3987 .await
3988 {
3989 return;
3990 }
3991 if attempts
3999 .borrow()
4000 .get(&connection_id)
4001 .is_some_and(|current| current.bootstrap.upgrade_id == upgrade_id)
4002 {
4003 attempts.borrow_mut().remove(&connection_id);
4004 }
4005 sessions.borrow_mut().remove(&upgrade_id);
4006 emit_wasm_carrier_action(
4007 &handler,
4008 serde_json::json!({
4009 "type": "retire-webrtc",
4010 "connectionId": connection_id,
4011 "upgradeId": upgrade_id,
4012 "failureCode": reason,
4013 }),
4014 );
4015 });
4016 });
4017 let carrier = crate::wasm_webrtc_carrier::WasmWebRtcCarrierSession::attach(
4018 channel,
4019 packet_session,
4020 expected,
4021 application_key,
4022 on_terminal,
4023 )?;
4024 self.wasm_webrtc_carrier_sessions
4025 .borrow_mut()
4026 .insert(upgrade_id.clone(), carrier);
4027 if attempt.role == "responder" {
4028 let ready = crate::iroh_carrier_bootstrap::CarrierBootstrapFrame::ready_from(
4029 &attempt.bootstrap,
4030 )
4031 .map_err(|error| JsValue::from_str(&error.to_string()))?;
4032 emit_wasm_carrier_action(
4033 &self.wasm_carrier_action_handler,
4034 serde_json::json!({
4035 "type": "send-control",
4036 "connectionId": connection_id,
4037 "remoteEndpointId": remote_endpoint_id,
4038 "envelope": ready,
4039 }),
4040 );
4041 self.spawn_inbound_wasm_webrtc_carrier_completion(attempt);
4042 } else if let Some(attempt) =
4043 self.take_ready_outbound_wasm_webrtc_carrier_attempt(&connection_id, &upgrade_id)
4044 {
4045 self.spawn_outbound_wasm_webrtc_carrier_completion(attempt);
4046 }
4047 Ok(())
4048 }
4049
4050 #[cfg(feature = "transport-moq")]
4054 #[wasm_bindgen(js_name = __attachMoqCarrier)]
4055 pub async fn attach_iroh_moq_carrier(
4056 &self,
4057 connection_id: String,
4058 remote_endpoint_id: String,
4059 datagrams: JsValue,
4060 upgrade_id: String,
4061 ) -> Result<(), JsValue> {
4062 let _lifecycle = self.wasm_carrier_peer_lifecycle.lock().await;
4063 let endpoint_id = remote_endpoint_id
4064 .parse::<iroh::EndpointId>()
4065 .map_err(|error| JsValue::from_str(&error.to_string()))?;
4066 let attempt = self
4067 .wasm_moq_carrier_attempts
4068 .borrow()
4069 .get(&connection_id)
4070 .filter(|attempt| {
4071 attempt.bootstrap.upgrade_id == upgrade_id
4072 && attempt.remote_endpoint_id == endpoint_id
4073 })
4074 .cloned()
4075 .ok_or_else(|| JsValue::from_str("MoQ carrier attempt is stale"))?;
4076 if !self
4077 .inner
4078 .wasm_carrier_upgrade_is_current(
4079 &connection_id,
4080 crate::client::IrohPathKind::Moq,
4081 &upgrade_id,
4082 attempt.generation,
4083 )
4084 .await
4085 {
4086 return Err(JsValue::from_str("MoQ carrier attempt was retired"));
4087 }
4088 let packet_session = self
4089 .inner
4090 .activate_iroh_packet_carrier(
4091 crate::iroh_carrier_kind::IrohCarrierKind::Moq,
4092 endpoint_id,
4093 )
4094 .await
4095 .map_err(|error| JsValue::from_str(&error.to_string()))?;
4096 let expected = attempt
4097 .bootstrap
4098 .frame_expectation()
4099 .map_err(|error| JsValue::from_str(&error.to_string()))?;
4100 let application_key = self
4101 .inner
4102 .application_crypto_key_for_connection(Some(&connection_id))
4103 .ok_or_else(|| {
4104 JsValue::from_str("MoQ carrier requires an installed application key")
4105 })?;
4106 let terminal_client = self.inner.clone();
4107 let terminal_connection_id = connection_id.clone();
4108 let terminal_upgrade_id = upgrade_id.clone();
4109 let terminal_generation = attempt.generation.transport_generation.saturating_add(1);
4110 let terminal_attempts = self.wasm_moq_carrier_attempts.clone();
4111 let terminal_sessions = self.wasm_moq_carrier_sessions.clone();
4112 let terminal_handler = self.wasm_carrier_action_handler.clone();
4113 let terminal_lifecycle = self.wasm_carrier_peer_lifecycle.clone();
4114 let on_terminal: Rc<dyn Fn(&'static str)> = Rc::new(move |reason| {
4115 let client = terminal_client.clone();
4116 let connection_id = terminal_connection_id.clone();
4117 let upgrade_id = terminal_upgrade_id.clone();
4118 let attempts = terminal_attempts.clone();
4119 let sessions = terminal_sessions.clone();
4120 let handler = terminal_handler.clone();
4121 let lifecycle = terminal_lifecycle.clone();
4122 spawn_local(async move {
4123 let _lifecycle = lifecycle.lock().await;
4124 if !client
4125 .close_current_iroh_carrier_generation_with_reason(
4126 &connection_id,
4127 endpoint_id,
4128 crate::client::IrohPathKind::Moq,
4129 terminal_generation,
4130 reason,
4131 )
4132 .await
4133 {
4134 return;
4135 }
4136 if attempts
4137 .borrow()
4138 .get(&connection_id)
4139 .is_some_and(|current| current.bootstrap.upgrade_id == upgrade_id)
4140 {
4141 attempts.borrow_mut().remove(&connection_id);
4142 }
4143 sessions.borrow_mut().remove(&upgrade_id);
4144 emit_wasm_carrier_action(
4145 &handler,
4146 serde_json::json!({
4147 "type": "retire-moq",
4148 "connectionId": connection_id,
4149 "upgradeId": upgrade_id,
4150 "failureCode": reason,
4151 }),
4152 );
4153 });
4154 });
4155 let carrier = crate::wasm_moq_carrier::WasmMoqCarrierSession::attach(
4156 datagrams,
4157 packet_session,
4158 expected,
4159 application_key,
4160 on_terminal,
4161 )?;
4162 if attempt.role == "initiator" {
4163 carrier
4164 .wait_for_peer_data_bidirectional_readiness(std::time::Duration::from_secs(15))
4165 .await?;
4166 }
4167 self.wasm_moq_carrier_sessions
4168 .borrow_mut()
4169 .insert(upgrade_id.clone(), carrier);
4170 if attempt.role == "initiator" {
4171 self.spawn_outbound_wasm_moq_carrier_completion(attempt);
4172 } else {
4173 let ready = crate::iroh_carrier_bootstrap::CarrierBootstrapFrame::ready_from(
4174 &attempt.bootstrap,
4175 )
4176 .map_err(|error| JsValue::from_str(&error.to_string()))?;
4177 emit_wasm_carrier_action(
4178 &self.wasm_carrier_action_handler,
4179 serde_json::json!({
4180 "type": "send-control",
4181 "connectionId": connection_id,
4182 "remoteEndpointId": attempt.remote_endpoint_id.to_string(),
4183 "envelope": ready,
4184 }),
4185 );
4186 self.spawn_inbound_wasm_moq_carrier_completion(attempt);
4187 }
4188 Ok(())
4189 }
4190
4191 #[wasm_bindgen(js_name = setIdentityCredential)]
4192 pub fn set_identity_credential(&self, credential: Option<String>) {
4193 let has_credential = credential
4194 .as_ref()
4195 .map(|value| !value.is_empty())
4196 .unwrap_or(false);
4197 let credential_len = credential.as_ref().map(|value| value.len()).unwrap_or(0);
4198
4199 if let Ok(mut guard) = self.identity_credential.lock() {
4200 *guard = credential.filter(|value| !value.is_empty());
4201 }
4202
4203 let should_log = if let Ok(mut guard) = self.last_auth_log.lock() {
4204 let next = (has_credential, credential_len);
4205 if guard.as_ref() == Some(&next) {
4206 false
4207 } else {
4208 *guard = Some(next);
4209 true
4210 }
4211 } else {
4212 true
4213 };
4214
4215 if should_log {
4216 web_sys::console::log_1(&JsValue::from_str(&format!(
4217 "[OPENRTC][WASM-IDENTITY] credential updated present={} len={}",
4218 has_credential, credential_len
4219 )));
4220 }
4221 }
4222
4223 #[wasm_bindgen(js_name = clearIdentityCredential)]
4229 pub fn clear_identity_credential(&self) {
4230 self.set_identity_credential(None);
4231 }
4232
4233 #[wasm_bindgen(js_name = rankRoutes)]
4236 pub fn rank_routes(
4237 &self,
4238 configured_priority: Vec<String>,
4239 candidates: Vec<String>,
4240 ) -> Vec<String> {
4241 crate::route_policy::rank_routes(&configured_priority, &candidates)
4242 }
4243
4244 #[wasm_bindgen(js_name = setRelay)]
4246 pub async fn set_relay(&self, enabled: bool) -> Result<(), JsValue> {
4247 self.inner
4248 .set_relay(enabled)
4249 .await
4250 .map_err(|error| JsValue::from_str(&error.to_string()))
4251 }
4252
4253 pub async fn init_iroh(&self, secret_key: Option<Vec<u8>>) -> Result<String, JsValue> {
4254 let started_at = js_sys::Date::now();
4255 web_sys::console::log_1(&JsValue::from_str(&format!(
4256 "[OPENRTC][WASM-API] init_iroh called has_secret_key={} secret_key_len={}",
4257 secret_key.as_ref().is_some(),
4258 secret_key.as_ref().map(|k| k.len()).unwrap_or(0)
4259 )));
4260 match self.inner.init_iroh(secret_key, vec![]).await {
4261 Ok(node_id) => {
4262 self.inner.clone().start_wasm_accept_bridge();
4263 let elapsed = js_sys::Date::now() - started_at;
4264 web_sys::console::log_1(&JsValue::from_str(&format!(
4265 "[OPENRTC][WASM-API] init_iroh success elapsed_ms={:.0} node_id={}",
4266 elapsed, node_id
4267 )));
4268 Ok(node_id)
4269 }
4270 Err(err) => {
4271 let elapsed = js_sys::Date::now() - started_at;
4272 web_sys::console::error_1(&JsValue::from_str(&format!(
4273 "[OPENRTC][WASM-API] init_iroh failed elapsed_ms={:.0} error={}",
4274 elapsed, err
4275 )));
4276 Err(JsValue::from_str(&err.to_string()))
4277 }
4278 }
4279 }
4280
4281 #[wasm_bindgen(js_name = initIrohWithTestRelay)]
4284 pub async fn init_iroh_with_test_relay(
4285 &self,
4286 secret_key: Option<Vec<u8>>,
4287 test_relay_url: Option<String>,
4288 ) -> Result<String, JsValue> {
4289 let node_id = self
4290 .inner
4291 .init_iroh_with_test_relay(secret_key, vec![], test_relay_url.as_deref())
4292 .await
4293 .map_err(|error| JsValue::from_str(&error.to_string()))?;
4294 self.inner.clone().start_wasm_accept_bridge();
4298 Ok(node_id)
4299 }
4300
4301 pub async fn iroh_secret_key(&self) -> Result<Vec<u8>, JsValue> {
4302 let node_guard = self.inner.iroh_node.read().await;
4303 if let Some(node) = node_guard.as_ref() {
4304 Ok(node.secret_key())
4305 } else {
4306 Err(JsValue::from_str("Iroh node not initialized"))
4307 }
4308 }
4309
4310 pub async fn node_addr(&self) -> Result<String, JsValue> {
4311 let node_guard = self.inner.iroh_node.read().await;
4312 if let Some(node) = node_guard.as_ref() {
4313 let addr = node
4314 .node_addr()
4315 .await
4316 .map_err(|e| JsValue::from_str(&e.to_string()))?;
4317 serde_json::to_string(&addr).map_err(|e| JsValue::from_str(&e.to_string()))
4318 } else {
4319 Err(JsValue::from_str("Iroh node not initialized"))
4320 }
4321 }
4322
4323 pub async fn endpoint_ticket(&self) -> Result<String, JsValue> {
4324 self.inner
4325 .endpoint_ticket()
4326 .await
4327 .map_err(|e| JsValue::from_str(&e.to_string()))
4328 }
4329
4330 pub async fn endpoint_ticket_with_token(
4334 &self,
4335 grant_scope: String,
4336 max_connections: u32,
4337 ) -> Result<String, JsValue> {
4338 self.inner
4339 .endpoint_ticket_with_token(&grant_scope, max_connections)
4340 .await
4341 .map_err(|e| JsValue::from_str(&e.to_string()))
4342 }
4343
4344 pub async fn issue_ticket_invite(
4347 &self,
4348 id: String,
4349 max_peers: u32,
4350 mesh: bool,
4351 ) -> Result<String, JsValue> {
4352 let invite = if mesh {
4353 self.inner
4354 .issue_ticket_invite(&id, crate::ticket_mesh::TicketMeshOptions { max_peers })
4355 .await
4356 } else {
4357 self.inner.issue_direct_ticket_invite(&id, max_peers).await
4358 };
4359 invite
4360 .map(|invite| invite.encode())
4361 .map_err(|error| JsValue::from_str(&error.to_string()))
4362 }
4363
4364 pub async fn issue_ticket_mesh(
4368 &self,
4369 id: String,
4370 max_peers: u32,
4371 action_handler: js_sys::Function,
4372 ) -> Result<String, JsValue> {
4373 if self.ticket_meshes.borrow().contains_key(&id)
4374 || !self.ticket_mesh_starts.borrow_mut().insert(id.clone())
4375 {
4376 return Err(JsValue::from_str("ticket mesh already has an owner"));
4377 }
4378 let _start = TicketMeshStartGuard {
4379 starts: &self.ticket_mesh_starts,
4380 id: id.clone(),
4381 };
4382 let mesh = crate::ticket_mesh::wasm_session::WasmTicketMesh::issue(
4383 self.inner.clone(),
4384 &id,
4385 crate::ticket_mesh::TicketMeshOptions { max_peers },
4386 action_handler,
4387 )
4388 .await
4389 .map_err(|error| JsValue::from_str(&error.to_string()))?;
4390 let invitation = mesh.invite();
4391 self.ticket_meshes.borrow_mut().insert(id, Rc::new(mesh));
4392 Ok(invitation)
4393 }
4394
4395 pub async fn join_ticket_mesh(
4396 &self,
4397 invitation: String,
4398 action_handler: js_sys::Function,
4399 ) -> Result<String, JsValue> {
4400 let invite = crate::ticket_mesh::TicketInvite::parse(&invitation)
4401 .map_err(|error| JsValue::from_str(&error.to_string()))?;
4402 let id = invite.id().to_string();
4403 if self.ticket_meshes.borrow().contains_key(&id)
4404 || !self.ticket_mesh_starts.borrow_mut().insert(id.clone())
4405 {
4406 return Err(JsValue::from_str("ticket mesh already has an owner"));
4407 }
4408 let _start = TicketMeshStartGuard {
4409 starts: &self.ticket_mesh_starts,
4410 id: id.clone(),
4411 };
4412 let mesh = crate::ticket_mesh::wasm_session::WasmTicketMesh::join(
4413 self.inner.clone(),
4414 &invitation,
4415 action_handler,
4416 )
4417 .await
4418 .map_err(|error| JsValue::from_str(&error.to_string()))?;
4419 self.ticket_meshes
4420 .borrow_mut()
4421 .insert(id.clone(), Rc::new(mesh));
4422 Ok(id)
4423 }
4424
4425 pub async fn ticket_mesh_peers(&self, id: String) -> Result<JsValue, JsValue> {
4426 let mesh = self
4427 .ticket_meshes
4428 .borrow()
4429 .get(&id)
4430 .cloned()
4431 .ok_or_else(|| JsValue::from_str("ticket mesh is not active"))?;
4432 serde_wasm_bindgen::to_value(&mesh.peers().await)
4433 .map_err(|error| JsValue::from_str(&error.to_string()))
4434 }
4435
4436 pub fn ticket_mesh_issuer_node(&self, id: String) -> Result<String, JsValue> {
4437 let mesh = self
4438 .ticket_meshes
4439 .borrow()
4440 .get(&id)
4441 .cloned()
4442 .ok_or_else(|| JsValue::from_str("ticket mesh is not active"))?;
4443 mesh.issuer_node()
4444 .map_err(|error| JsValue::from_str(&error.to_string()))
4445 }
4446
4447 pub fn receive_ticket_mesh_frame(
4448 &self,
4449 id: String,
4450 connection_id: String,
4451 frame: String,
4452 ) -> Result<(), JsValue> {
4453 let mesh = self
4454 .ticket_meshes
4455 .borrow()
4456 .get(&id)
4457 .cloned()
4458 .ok_or_else(|| JsValue::from_str("ticket mesh is not active"))?;
4459 mesh.receive(connection_id, frame.into_bytes());
4460 Ok(())
4461 }
4462
4463 pub async fn close_ticket_mesh(&self, id: String) -> Result<(), JsValue> {
4464 if self.ticket_mesh_starts.borrow().contains(&id) {
4465 return Err(JsValue::from_str(
4466 "ticket mesh is still starting; await startup before closing",
4467 ));
4468 }
4469 let mesh = self.ticket_meshes.borrow().get(&id).cloned();
4470 if let Some(mesh) = mesh {
4471 mesh.close().await;
4472 let mut meshes = self.ticket_meshes.borrow_mut();
4473 if meshes
4474 .get(&id)
4475 .is_some_and(|current| Rc::ptr_eq(current, &mesh))
4476 {
4477 meshes.remove(&id);
4478 }
4479 }
4480 Ok(())
4481 }
4482
4483 pub async fn connect_ticket_invite(&self, invitation: String) -> Result<JsValue, JsValue> {
4486 let result = self
4487 .inner
4488 .connect_ticket_invite(&invitation)
4489 .await
4490 .map_err(|error| JsValue::from_str(&error.to_string()))?;
4491 serde_wasm_bindgen::to_value(&result)
4492 .map_err(|error| JsValue::from_str(&error.to_string()))
4493 }
4494
4495 #[wasm_bindgen(js_name = __requireScopePeerAdmission)]
4497 pub fn require_scope_peer_admission(&self, scope: String) -> Result<(), JsValue> {
4498 self.inner
4499 .session_token_registry
4500 .require_scope_peer_admission(&scope)
4501 .map_err(|error| JsValue::from_str(&error))
4502 }
4503
4504 #[wasm_bindgen(js_name = __updateScopePeerAdmission)]
4507 pub fn update_scope_peer_admission(
4508 &self,
4509 scope: String,
4510 revision: f64,
4511 expires_at_ms: f64,
4512 peers_json: String,
4513 ) -> Result<(), JsValue> {
4514 let valid_integer = |value: f64| {
4515 value.is_finite()
4516 && value >= 0.0
4517 && value <= 9_007_199_254_740_991.0
4518 && value.fract() == 0.0
4519 };
4520 if !valid_integer(revision) || !valid_integer(expires_at_ms) || peers_json.len() > 4_000
4521 {
4522 return Err(JsValue::from_str("invalid peer admission lease"));
4523 }
4524 let peers = serde_json::from_str(&peers_json)
4525 .map_err(|_| JsValue::from_str("invalid peer admission peers"))?;
4526 self.inner
4527 .session_token_registry
4528 .update_scope_peer_admission(&scope, revision as u64, expires_at_ms as u64, peers)
4529 .map_err(|error| JsValue::from_str(&error))
4530 }
4531
4532 pub fn register_session_token(
4534 &self,
4535 token: String,
4536 grant_scope: String,
4537 max_connections: u32,
4538 ) {
4539 self.inner
4540 .register_session_token(token, grant_scope, max_connections);
4541 }
4542
4543 pub fn register_token_until(
4545 &self,
4546 token: String,
4547 grant_scope: String,
4548 max_connections: u32,
4549 expires_at_ms: u64,
4550 ) {
4551 self.inner
4552 .register_token_until(token, grant_scope, max_connections, expires_at_ms);
4553 }
4554
4555 #[wasm_bindgen(js_name = setConnectionApplicationCryptoRequired)]
4557 pub fn set_connection_application_crypto_required(
4558 &self,
4559 connection_id: String,
4560 ) -> Result<(), JsValue> {
4561 self.inner
4562 .set_connection_application_crypto_required(&connection_id);
4563 Ok(())
4564 }
4565
4566 #[wasm_bindgen(js_name = setConnectionApplicationCryptoKey)]
4568 pub async fn set_connection_application_crypto_key(
4569 &self,
4570 connection_id: String,
4571 key: Vec<u8>,
4572 ) -> Result<(), JsValue> {
4573 if key.len() != crate::application_crypto::APPLICATION_KEY_BYTES {
4574 return Err(JsValue::from_str("application crypto key must be 32 bytes"));
4575 }
4576 let mut key_bytes = [0u8; crate::application_crypto::APPLICATION_KEY_BYTES];
4577 key_bytes.copy_from_slice(&key);
4578 self.inner
4579 .set_connection_application_crypto_key(&connection_id, key_bytes);
4580 self.inner
4581 .emit_current_wasm_connection_state(&connection_id)
4582 .await;
4583 Ok(())
4584 }
4585
4586 #[wasm_bindgen(js_name = clearConnectionApplicationCryptoKey)]
4591 pub fn clear_connection_application_crypto_key(&self, connection_id: String) {
4592 self.inner
4593 .clear_connection_application_crypto_key(&connection_id);
4594 }
4595
4596 #[wasm_bindgen(js_name = ensureConnectionApplicationCrypto)]
4600 pub async fn ensure_connection_application_crypto(
4601 &self,
4602 connection_id: String,
4603 remote_node_id: String,
4604 timeout_ms: u64,
4605 send_frame: js_sys::Function,
4606 ) -> Result<(), JsValue> {
4607 let connection_id = connection_id.trim().to_string();
4608 if connection_id.is_empty() {
4609 return Err(JsValue::from_str("connection id is required"));
4610 }
4611 let remote_node_id = remote_node_id.trim().to_string();
4612 if remote_node_id.is_empty() {
4613 return Err(JsValue::from_str("remote node id is required"));
4614 }
4615 let remote_endpoint_id: iroh::EndpointId = remote_node_id.parse().map_err(|error| {
4616 JsValue::from_str(&format!("invalid remote endpoint id: {error}"))
4617 })?;
4618 self.inner
4624 .ensure_connection_manager_record_before_peer_stream(&remote_endpoint_id)
4625 .await
4626 .map_err(|error| JsValue::from_str(&error.to_string()))?;
4627 let timeout_ms = timeout_ms.max(1);
4628 let initial = self
4629 .inner
4630 .connection_manager
4631 .get_by_connection_id(&connection_id)
4632 .await
4633 .ok_or_else(|| JsValue::from_str("connection is not registered"))?;
4634 let initial_endpoint = initial
4635 .endpoint_id
4636 .as_deref()
4637 .or(initial.node_id.as_deref())
4638 .map(ToOwned::to_owned)
4639 .ok_or_else(|| JsValue::from_str("connection has no endpoint"))?;
4640 let initial_transport_stable_id = initial.transport_stable_id;
4641 let initial_transport_generation = initial.transport_generation;
4642 let initial_route_generation = initial.route_generation;
4643 self.inner
4644 .set_connection_application_crypto_required(&connection_id);
4645
4646 let started_at_ms = js_sys::Date::now();
4647 let retry_delays_ms = [0_u64, 250, 750];
4648 for (attempt, delay_ms) in retry_delays_ms.into_iter().enumerate() {
4649 if delay_ms > 0 {
4650 let remaining_ms = timeout_ms
4651 .saturating_sub((js_sys::Date::now() - started_at_ms).max(0.0) as u64);
4652 if remaining_ms == 0 {
4653 break;
4654 }
4655 gloo_timers::future::sleep(std::time::Duration::from_millis(
4656 delay_ms.min(remaining_ms),
4657 ))
4658 .await;
4659 }
4660 self.inner
4661 .assert_application_crypto_generation(
4662 &connection_id,
4663 &initial_endpoint,
4664 initial_transport_stable_id,
4665 initial_transport_generation,
4666 initial_route_generation,
4667 )
4668 .await
4669 .map_err(|error| JsValue::from_str(&error.to_string()))?;
4670 if self
4671 .inner
4672 .connection_application_crypto_key(&connection_id)
4673 .is_some()
4674 && self.inner.connection_application_crypto_is_confirmed(
4675 &connection_id,
4676 initial_transport_stable_id,
4677 )
4678 {
4679 let confirmed_transport_stable_id =
4680 initial_transport_stable_id.ok_or_else(|| {
4681 JsValue::from_str("confirmed connection has no stable ID")
4682 })?;
4683 if !self
4684 .inner
4685 .confirm_managed_connection_readiness_from_transport_proof(
4686 &connection_id,
4687 confirmed_transport_stable_id,
4688 )
4689 .await
4690 {
4691 return Err(JsValue::from_str(
4692 "confirmed application route belongs to a retired generation",
4693 ));
4694 }
4695 self.inner
4696 .emit_current_wasm_connection_state(&connection_id)
4697 .await;
4698 return Ok(());
4699 }
4700 let frame = self
4701 .inner
4702 .application_key_handshake_frame(&connection_id, "capability-update")
4703 .map_err(|error| JsValue::from_str(&error.to_string()))?;
4704 let frame = serde::Serialize::serialize(
4705 &frame,
4706 &serde_wasm_bindgen::Serializer::json_compatible(),
4707 )
4708 .map_err(|error| JsValue::from_str(&error.to_string()))?;
4709 let send_result = send_frame.call1(&JsValue::NULL, &frame)?;
4710 wasm_bindgen_futures::JsFuture::from(js_sys::Promise::resolve(&send_result))
4711 .await?;
4712
4713 let attempt_deadline_ms = ((attempt + 1) as u64 * timeout_ms / 3).max(1);
4714 loop {
4715 if self
4716 .inner
4717 .connection_application_crypto_key(&connection_id)
4718 .is_some()
4719 && self.inner.connection_application_crypto_is_confirmed(
4720 &connection_id,
4721 initial_transport_stable_id,
4722 )
4723 {
4724 let confirmed_transport_stable_id = initial_transport_stable_id
4725 .ok_or_else(|| {
4726 JsValue::from_str("confirmed connection has no stable ID")
4727 })?;
4728 if !self
4729 .inner
4730 .confirm_managed_connection_readiness_from_transport_proof(
4731 &connection_id,
4732 confirmed_transport_stable_id,
4733 )
4734 .await
4735 {
4736 return Err(JsValue::from_str(
4737 "confirmed application route belongs to a retired generation",
4738 ));
4739 }
4740 self.inner
4741 .emit_current_wasm_connection_state(&connection_id)
4742 .await;
4743 return Ok(());
4744 }
4745 let elapsed_ms = (js_sys::Date::now() - started_at_ms).max(0.0) as u64;
4746 if elapsed_ms >= attempt_deadline_ms || elapsed_ms >= timeout_ms {
4747 break;
4748 }
4749 self.inner
4750 .assert_application_crypto_generation(
4751 &connection_id,
4752 &initial_endpoint,
4753 initial_transport_stable_id,
4754 initial_transport_generation,
4755 initial_route_generation,
4756 )
4757 .await
4758 .map_err(|error| JsValue::from_str(&error.to_string()))?;
4759 gloo_timers::future::sleep(std::time::Duration::from_millis(25)).await;
4760 }
4761 }
4762 let current_manager_transport_stable_id = self
4763 .inner
4764 .connection_manager
4765 .get_by_connection_id(&connection_id)
4766 .await
4767 .and_then(|record| record.transport_stable_id);
4768 let current_physical_transport_stable_id = self
4769 .inner
4770 .get_connection(remote_endpoint_id)
4771 .await
4772 .map(|connection| crate::transport_generation::for_connection(&connection));
4773 let confirmation = self
4774 .inner
4775 .connection_application_crypto_confirmation_snapshot(&connection_id);
4776 return Err(JsValue::from_str(&format!(
4777 "connection {} reciprocal application key agreement timed out after {}ms: frozen_stable_id={:?} current_manager_stable_id={:?} current_physical_stable_id={:?} confirmation={:?}",
4778 connection_id,
4779 timeout_ms,
4780 initial_transport_stable_id,
4781 current_manager_transport_stable_id,
4782 current_physical_transport_stable_id,
4783 confirmation,
4784 )));
4785 }
4786
4787 #[wasm_bindgen(js_name = handleConnectionApplicationCryptoHandshake)]
4788 pub async fn handle_connection_application_crypto_handshake(
4789 &self,
4790 connection_id: String,
4791 remote_node_id: String,
4792 transport_stable_id: Option<u64>,
4793 action: Option<String>,
4794 remote_public_key: Vec<u8>,
4795 ) -> Result<JsValue, JsValue> {
4796 if remote_public_key.len() != crate::key_agreement::PUBLIC_KEY_BYTES {
4797 return Err(JsValue::from_str(
4798 "application key agreement public key must be 32 bytes",
4799 ));
4800 }
4801 let mut remote_public = [0_u8; crate::key_agreement::PUBLIC_KEY_BYTES];
4802 remote_public.copy_from_slice(&remote_public_key);
4803 let outcome = self
4804 .inner
4805 .accept_application_key_handshake(
4806 &connection_id,
4807 &remote_node_id,
4808 transport_stable_id,
4809 action.as_deref(),
4810 remote_public,
4811 )
4812 .await
4813 .map_err(|error| JsValue::from_str(&error.to_string()))?;
4814 let reply_frame = outcome
4815 .reply_action
4816 .map(|reply_action| {
4817 self.inner
4818 .application_key_handshake_frame(&connection_id, reply_action)
4819 })
4820 .transpose()
4821 .map_err(|error| JsValue::from_str(&error.to_string()))?;
4822 self.inner
4823 .emit_current_wasm_connection_state(&connection_id)
4824 .await;
4825 serde::Serialize::serialize(
4826 &reply_frame,
4827 &serde_wasm_bindgen::Serializer::json_compatible(),
4828 )
4829 .map_err(|error| JsValue::from_str(&error.to_string()))
4830 }
4831
4832 #[wasm_bindgen(js_name = connectionApplicationCryptoReady)]
4833 pub fn connection_application_crypto_ready(
4834 &self,
4835 connection_id: String,
4836 transport_stable_id: u64,
4837 ) -> bool {
4838 self.inner
4839 .connection_application_crypto_key(&connection_id)
4840 .is_some()
4841 && self.inner.connection_application_crypto_is_confirmed(
4842 &connection_id,
4843 Some(transport_stable_id),
4844 )
4845 }
4846
4847 #[wasm_bindgen(js_name = protectConnectionApplicationPayload)]
4848 pub fn protect_connection_application_payload(
4849 &self,
4850 connection_id: String,
4851 type_id: u8,
4852 payload: Vec<u8>,
4853 ) -> Result<Vec<u8>, JsValue> {
4854 self.inner
4855 .protect_outbound_application_payload_with_type(&connection_id, type_id, &payload)
4856 .map_err(|error| JsValue::from_str(&error.to_string()))
4857 }
4858
4859 #[wasm_bindgen(js_name = openConnectionApplicationPayload)]
4860 pub fn open_connection_application_payload(
4861 &self,
4862 connection_id: String,
4863 type_id: u8,
4864 payload: Vec<u8>,
4865 ) -> Result<Vec<u8>, JsValue> {
4866 self.inner
4867 .open_inbound_application_payload_with_type(&connection_id, type_id, &payload)
4868 .map_err(|error| JsValue::from_str(&error.to_string()))
4869 }
4870
4871 pub fn validate_session_token(&self, token: String) -> Result<String, JsValue> {
4875 self.inner
4876 .validate_session_token(&token)
4877 .map_err(|e| JsValue::from_str(&e))
4878 }
4879
4880 pub async fn validate_connection_token(
4882 &self,
4883 token: String,
4884 connection_id: String,
4885 token_payload: Option<String>,
4886 ) -> Result<String, JsValue> {
4887 self.inner
4888 .validate_connection_token(&token, &connection_id, token_payload.as_deref())
4889 .await
4890 .map_err(|e| JsValue::from_str(&e))
4891 }
4892
4893 pub async fn present_session_token(
4900 &self,
4901 endpoint_id: String,
4902 token: String,
4903 token_payload: Option<String>,
4904 device_id: Option<String>,
4905 ) -> Result<String, JsValue> {
4906 crate::console_log!(
4907 "[OpenRTC][session-admission][wasm-present] endpoint_id={} claimed_local_device_id={}",
4908 endpoint_id,
4909 device_id.as_deref().unwrap_or("<none>")
4910 );
4911 let endpoint_id_parsed: iroh::EndpointId = endpoint_id
4912 .parse()
4913 .map_err(|e| JsValue::from_str(&format!("{}", e)))?;
4914 let local_node_id = self.inner.current_node_id().await.ok_or_else(|| {
4915 JsValue::from_str("missing local node id for session-token presentation")
4916 })?;
4917 let connection_id =
4918 crate::client::Client::deterministic_connection_id(&local_node_id, &endpoint_id);
4919 let approval_scope = self
4920 .inner
4921 .present_and_accept_session_token_with_local_claim(
4922 endpoint_id_parsed,
4923 &connection_id,
4924 &token,
4925 token_payload.as_deref(),
4926 None,
4927 device_id,
4928 )
4929 .await
4930 .map_err(|e| JsValue::from_str(&e))?;
4931 self.inner
4932 .emit_current_wasm_connection_state(&connection_id)
4933 .await;
4934
4935 Ok(approval_scope)
4936 }
4937
4938 pub async fn is_remote_admitted(&self, endpoint_ticket: String) -> Result<bool, JsValue> {
4941 self.inner
4942 .is_remote_admitted(&endpoint_ticket)
4943 .await
4944 .map_err(|error| JsValue::from_str(&error.to_string()))
4945 }
4946
4947 #[allow(clippy::too_many_arguments)]
4950 pub async fn prepare_reciprocal_admission(
4951 &self,
4952 endpoint_id: String,
4953 expected_transport_stable_id: u64,
4954 stream_instance_id: String,
4955 presentation_id: String,
4956 token: String,
4957 token_payload: String,
4958 device_id: String,
4959 stream_contract: String,
4960 ) -> Result<bool, JsValue> {
4961 let endpoint_id: iroh::EndpointId = endpoint_id
4962 .parse()
4963 .map_err(|error| JsValue::from_str(&format!("{error}")))?;
4964 let stream_contract = match stream_contract.trim() {
4965 "one-shot-admission" => {
4966 crate::native_protocol::TokenStreamContract::OneShotAdmission
4967 }
4968 "persistent-control" => {
4969 crate::native_protocol::TokenStreamContract::PersistentControl
4970 }
4971 other => {
4972 return Err(JsValue::from_str(&format!(
4973 "unsupported reciprocal stream contract: {other}"
4974 )))
4975 }
4976 };
4977 self.inner
4978 .prepare_reciprocal_admission(
4979 endpoint_id,
4980 expected_transport_stable_id,
4981 stream_instance_id.as_str(),
4982 presentation_id.as_str(),
4983 token.as_str(),
4984 token_payload.as_str(),
4985 device_id.as_str(),
4986 stream_contract,
4987 )
4988 .await
4989 .map_err(|error| JsValue::from_str(&error))?;
4990 Ok(true)
4991 }
4992
4993 pub async fn confirm_reciprocal_admission(
4996 &self,
4997 endpoint_id: String,
4998 expected_transport_stable_id: u64,
4999 stream_instance_id: String,
5000 presentation_id: String,
5001 accepted: bool,
5002 approval_scope: Option<String>,
5003 ) -> Result<bool, JsValue> {
5004 let endpoint_id: iroh::EndpointId = endpoint_id
5005 .parse()
5006 .map_err(|error| JsValue::from_str(&format!("{error}")))?;
5007 self.inner
5008 .confirm_reciprocal_admission(
5009 endpoint_id,
5010 expected_transport_stable_id,
5011 stream_instance_id.as_str(),
5012 presentation_id.as_str(),
5013 accepted,
5014 approval_scope.as_deref(),
5015 )
5016 .await
5017 .map_err(|error| JsValue::from_str(&error))?;
5018 self.inner
5019 .emit_current_wasm_connection_state(
5020 &crate::client::Client::deterministic_connection_id(
5021 &self.inner.current_node_id().await.ok_or_else(|| {
5022 JsValue::from_str(
5023 "missing local node id after reciprocal admission ACK",
5024 )
5025 })?,
5026 &endpoint_id.to_string(),
5027 ),
5028 )
5029 .await;
5030 Ok(true)
5031 }
5032
5033 pub fn revoke_session_token(&self, token: String) -> Result<JsValue, JsValue> {
5035 serde_wasm_bindgen::to_value(&self.inner.revoke_session_token(&token))
5036 .map_err(|error| JsValue::from_str(&error.to_string()))
5037 }
5038
5039 pub async fn revoke_tokens_by_scope(
5041 &self,
5042 grant_scope: String,
5043 ) -> Result<JsValue, JsValue> {
5044 let affected = self.inner.begin_revoke_tokens_by_scope(&grant_scope);
5045 for connection_id in &affected {
5050 #[cfg(not(any(feature = "transport-webrtc", feature = "transport-moq")))]
5051 let _ = connection_id;
5052 #[cfg(feature = "transport-webrtc")]
5053 self.retire_iroh_webrtc_carrier(
5054 connection_id.clone(),
5055 Some(crate::lifecycle_reason::REASON_SESSION_TOKEN_REVOKED.to_string()),
5056 )
5057 .await;
5058 #[cfg(feature = "transport-moq")]
5059 self.retire_iroh_moq_carrier(
5060 connection_id.clone(),
5061 Some(crate::lifecycle_reason::REASON_SESSION_TOKEN_REVOKED.to_string()),
5062 )
5063 .await;
5064 }
5065 self.inner
5066 .finish_revoke_tokens_by_scope(&grant_scope, &affected)
5067 .await;
5068 serde_wasm_bindgen::to_value(&affected).map_err(|e| JsValue::from_str(&e.to_string()))
5069 }
5070
5071 pub fn clear_session_tokens(&self) {
5073 self.inner.clear_session_tokens();
5074 }
5075
5076 pub fn endpoint_id_from_ticket(&self, ticket: String) -> Result<String, JsValue> {
5077 let (iroh_ticket, _token_suffix) = split_ticket(ticket.trim());
5078 let parsed = EndpointTicket::from_str(iroh_ticket)
5079 .map_err(|e| JsValue::from_str(&format!("Invalid endpoint ticket: {}", e)))?;
5080 Ok(parsed.endpoint_addr().id.to_string())
5081 }
5082
5083 pub async fn disconnect(&self, endpoint_id: String) -> Result<(), JsValue> {
5084 let endpoint_id: iroh::EndpointId = endpoint_id
5085 .parse()
5086 .map_err(|e| JsValue::from_str(&format!("{}", e)))?;
5087 let node_guard = self.inner.iroh_node.read().await;
5088 if let Some(node) = node_guard.as_ref() {
5089 node.disconnect(endpoint_id)
5090 .await
5091 .map_err(|e| JsValue::from_str(&e.to_string()))
5092 } else {
5093 Err(JsValue::from_str("Iroh node not initialized"))
5094 }
5095 }
5096
5097 pub async fn disconnect_transient(&self, endpoint_id: String) -> Result<(), JsValue> {
5110 let endpoint_id: iroh::EndpointId = endpoint_id
5111 .parse()
5112 .map_err(|e| JsValue::from_str(&format!("{}", e)))?;
5113 self.inner
5114 .disconnect_with_reason(
5115 endpoint_id,
5116 crate::lifecycle_reason::REASON_NETWORK_CHANGE_RECONNECT,
5117 )
5118 .await
5119 .map_err(|e| JsValue::from_str(&e.to_string()))?;
5120 self.inner.wake_browser_auto_connect();
5126 Ok(())
5127 }
5128
5129 pub async fn is_connected(&self, endpoint_id: String) -> Result<bool, JsValue> {
5130 let endpoint_id: iroh::EndpointId = endpoint_id
5131 .parse()
5132 .map_err(|e| JsValue::from_str(&format!("{}", e)))?;
5133 Ok(self.inner.is_connected(endpoint_id).await)
5134 }
5135
5136 pub fn runtime_policy(&self) -> Result<JsValue, JsValue> {
5137 serde_wasm_bindgen::to_value(&self.inner.policy_snapshot())
5138 .map_err(|e| JsValue::from_str(&e.to_string()))
5139 }
5140
5141 pub async fn add_peer_scope(&self, id: String, scope: String) -> Result<JsValue, JsValue> {
5142 let scopes = self.inner.add_peer_scope(&id, &scope).await;
5143 serde_wasm_bindgen::to_value(&scopes).map_err(|e| JsValue::from_str(&e.to_string()))
5144 }
5145
5146 pub async fn release_peer_scope(
5147 &self,
5148 id: String,
5149 scope: Option<String>,
5150 ) -> Result<JsValue, JsValue> {
5151 let scopes = self.inner.release_peer_scope(&id, scope.as_deref()).await;
5152 serde_wasm_bindgen::to_value(&scopes).map_err(|e| JsValue::from_str(&e.to_string()))
5153 }
5154
5155 pub async fn peer_scopes(&self, id: String) -> Result<JsValue, JsValue> {
5156 let scopes = self.inner.peer_scopes(&id).await;
5157 serde_wasm_bindgen::to_value(&scopes).map_err(|e| JsValue::from_str(&e.to_string()))
5158 }
5159
5160 pub async fn same_peer(&self, left: String, right: String) -> Result<bool, JsValue> {
5161 Ok(self.inner.same_peer(&left, &right).await)
5162 }
5163
5164 pub async fn peer_snapshot(&self, id: String) -> Result<JsValue, JsValue> {
5165 let snapshot = self.inner.peer_snapshot(&id).await;
5166 serde_wasm_bindgen::to_value(&snapshot).map_err(|e| JsValue::from_str(&e.to_string()))
5167 }
5168
5169 pub async fn peer_session(&self, id: String) -> Result<JsValue, JsValue> {
5173 let snapshot = self.inner.peer_session(&id).await;
5174 serde_wasm_bindgen::to_value(&snapshot).map_err(|e| JsValue::from_str(&e.to_string()))
5175 }
5176
5177 pub async fn peer_sessions(&self) -> Result<JsValue, JsValue> {
5178 let snapshots = self.inner.peer_sessions().await;
5179 serde_wasm_bindgen::to_value(&snapshots).map_err(|e| JsValue::from_str(&e.to_string()))
5180 }
5181
5182 pub async fn connection_state(&self, connection_id: String) -> Result<JsValue, JsValue> {
5183 let snapshot = self.inner.connection_state(&connection_id).await;
5184 serde_wasm_bindgen::to_value(&snapshot).map_err(|e| JsValue::from_str(&e.to_string()))
5185 }
5186
5187 #[cfg(any(feature = "transport-webrtc", feature = "transport-moq"))]
5188 async fn revisit_preferred_iroh_carrier_after_settlement(&self, connection_id: &str) {
5189 let _lifecycle = self.wasm_carrier_peer_lifecycle.lock().await;
5190 let Some(record) = self
5191 .inner
5192 .connection_manager
5193 .get_by_connection_id(connection_id)
5194 .await
5195 else {
5196 return;
5197 };
5198 if matches!(
5201 record.active_transport.as_str(),
5202 crate::transport_label::WEBRTC | crate::transport_label::MOQ
5203 ) {
5204 return;
5205 }
5206 let Some(capabilities) = self
5207 .wasm_remote_carrier_capabilities
5208 .borrow()
5209 .get(connection_id)
5210 .copied()
5211 else {
5212 return;
5213 };
5214 let Some(remote_endpoint_id) = self
5215 .inner
5216 .wasm_iroh_carrier_remote_endpoint_id(connection_id)
5217 .await
5218 else {
5219 return;
5220 };
5221 if let Err(error) = self
5222 .begin_preferred_iroh_carrier_locked(
5223 connection_id.to_string(),
5224 remote_endpoint_id,
5225 capabilities.webrtc,
5226 capabilities.moq,
5227 )
5228 .await
5229 {
5230 web_sys::console::warn_1(&JsValue::from_str(&format!(
5231 "[OpenRTC][Iroh carrier] settled-generation revisit failed connection_id={} error={:?}",
5232 connection_id, error,
5233 )));
5234 }
5235 }
5236
5237 pub async fn connection_states(&self) -> Result<JsValue, JsValue> {
5238 let snapshots = self.inner.connection_states().await;
5239 serde_wasm_bindgen::to_value(&snapshots).map_err(|e| JsValue::from_str(&e.to_string()))
5240 }
5241
5242 pub async fn wait_for_peer(
5243 &self,
5244 id: String,
5245 timeout_ms: Option<u32>,
5246 ) -> Result<JsValue, JsValue> {
5247 let snapshot = self
5248 .inner
5249 .wait_for_peer(&id, timeout_ms.map(|value| value as u64))
5250 .await;
5251 serde_wasm_bindgen::to_value(&snapshot).map_err(|e| JsValue::from_str(&e.to_string()))
5252 }
5253
5254 pub async fn wait_for_settled_scope(
5255 &self,
5256 scope: String,
5257 timeout_ms: Option<u32>,
5258 ) -> Result<JsValue, JsValue> {
5259 let snapshot = self
5260 .inner
5261 .wait_for_settled_scope(&scope, timeout_ms.map(u64::from))
5262 .await;
5263 serde_wasm_bindgen::to_value(&snapshot).map_err(|e| JsValue::from_str(&e.to_string()))
5264 }
5265
5266 pub async fn resolve_peer_connection_records(
5267 &self,
5268 id: String,
5269 ) -> Result<JsValue, JsValue> {
5270 let records = self.inner.resolve_peer_connection_records(&id).await;
5271 serde_wasm_bindgen::to_value(&records).map_err(|e| JsValue::from_str(&e.to_string()))
5272 }
5273
5274 pub async fn list_managed_connections(&self) -> Result<JsValue, JsValue> {
5275 let records = self.inner.list_managed_connections().await;
5276 serde_wasm_bindgen::to_value(&records).map_err(|e| JsValue::from_str(&e.to_string()))
5277 }
5278
5279 pub async fn bind_connection_device_id(
5280 &self,
5281 connection_id: String,
5282 device_id: String,
5283 ) -> Result<JsValue, JsValue> {
5284 let snapshot = self
5285 .inner
5286 .bind_connection_device_id(&connection_id, &device_id)
5287 .await;
5288 serde_wasm_bindgen::to_value(&snapshot).map_err(|e| JsValue::from_str(&e.to_string()))
5289 }
5290
5291 pub async fn acknowledge_session_token_response(
5292 &self,
5293 connection_id: String,
5294 remote_node_id: String,
5295 transport_stable_id: u64,
5296 claimed_device_id: Option<String>,
5297 ) -> bool {
5298 self.inner
5299 .acknowledge_wasm_session_token_response(
5300 &connection_id,
5301 &remote_node_id,
5302 transport_stable_id,
5303 claimed_device_id.as_deref(),
5304 )
5305 .await
5306 }
5307
5308 pub async fn bind_node_device_id(
5309 &self,
5310 node_id: String,
5311 device_id: String,
5312 ) -> Result<(), JsValue> {
5313 self.inner.bind_node_device_id(&node_id, &device_id).await;
5314 Ok(())
5315 }
5316
5317 pub async fn reject_connection_admission(
5318 &self,
5319 connection_id: String,
5320 reason: String,
5321 ) -> Result<JsValue, JsValue> {
5322 self.inner
5323 .reject_session_connection(&connection_id, &reason);
5324 self.inner
5325 .emit_current_wasm_connection_state(&connection_id)
5326 .await;
5327 let snapshot = self.inner.connection_state(&connection_id).await;
5328 serde_wasm_bindgen::to_value(&snapshot).map_err(|e| JsValue::from_str(&e.to_string()))
5329 }
5330
5331 pub async fn report_managed_connection_settled(
5332 &self,
5333 connection_id: String,
5334 settled: bool,
5335 device_id: Option<String>,
5336 transport_stable_id: Option<u64>,
5337 transport_generation: Option<u64>,
5338 route_generation: Option<u64>,
5339 ) -> Result<JsValue, JsValue> {
5340 let snapshot = match (transport_stable_id, transport_generation, route_generation) {
5341 (Some(transport_stable_id), Some(transport_generation), Some(route_generation)) => {
5342 self.inner
5343 .report_managed_connection_settled_for_transport(
5344 &connection_id,
5345 settled,
5346 transport_stable_id,
5347 transport_generation,
5348 route_generation,
5349 )
5350 .await
5351 }
5352 _ => None,
5353 };
5354 let _ = device_id;
5355 #[cfg(any(feature = "transport-webrtc", feature = "transport-moq"))]
5356 if settled && snapshot.is_some() {
5357 self.revisit_preferred_iroh_carrier_after_settlement(&connection_id)
5363 .await;
5364 }
5365 serde_wasm_bindgen::to_value(&snapshot).map_err(|e| JsValue::from_str(&e.to_string()))
5366 }
5367
5368 pub async fn report_transport_status(
5369 &self,
5370 connection_id: String,
5371 active_transport: String,
5372 parallel_transport: Option<String>,
5373 transport_stable_id: Option<u64>,
5374 transport_generation: Option<u64>,
5375 route_generation: Option<u64>,
5376 ) -> Result<JsValue, JsValue> {
5377 let snapshot = match (transport_stable_id, transport_generation, route_generation) {
5378 (Some(transport_stable_id), Some(transport_generation), Some(route_generation)) => {
5379 self.inner
5380 .report_transport_status_for_generation(
5381 &connection_id,
5382 &active_transport,
5383 parallel_transport.as_deref(),
5384 transport_stable_id,
5385 transport_generation,
5386 route_generation,
5387 )
5388 .await
5389 }
5390 _ => None,
5391 };
5392 serde_wasm_bindgen::to_value(&snapshot).map_err(|e| JsValue::from_str(&e.to_string()))
5393 }
5394
5395 pub async fn is_current_transport_stable_id(
5396 &self,
5397 endpoint_id: String,
5398 transport_stable_id: u64,
5399 ) -> Result<bool, JsValue> {
5400 let endpoint_id = endpoint_id
5401 .parse::<iroh::EndpointId>()
5402 .map_err(|error| JsValue::from_str(&error.to_string()))?;
5403 Ok(self
5404 .inner
5405 .is_current_transport_stable_id(endpoint_id, transport_stable_id)
5406 .await)
5407 }
5408
5409 #[wasm_bindgen(js_name = isAuthenticatedReconnectCandidate)]
5413 pub async fn is_authenticated_reconnect_candidate(
5414 &self,
5415 endpoint_id: String,
5416 transport_stable_id: u64,
5417 ) -> Result<bool, JsValue> {
5418 let endpoint_id = endpoint_id
5419 .parse::<iroh::EndpointId>()
5420 .map_err(|error| JsValue::from_str(&error.to_string()))?;
5421 Ok(self
5422 .inner
5423 .incoming_stream_generation_is_authenticated_reconnect_candidate(
5424 endpoint_id,
5425 transport_stable_id,
5426 )
5427 .await)
5428 }
5429
5430 pub async fn open_bi(&self, endpoint_id: String) -> Result<BiStream, JsValue> {
5431 let endpoint_id: iroh::EndpointId = endpoint_id
5432 .parse()
5433 .map_err(|e| JsValue::from_str(&format!("{}", e)))?;
5434 self.inner
5435 .assert_raw_peer_stream_allowed(&endpoint_id)
5436 .await
5437 .map_err(|e| JsValue::from_str(&e.to_string()))?;
5438 self.inner
5447 .ensure_connection_manager_record_before_peer_stream(&endpoint_id)
5448 .await
5449 .map_err(|e| JsValue::from_str(&e.to_string()))?;
5450 let (send, recv) = self
5451 .inner
5452 .open_bi_internal(endpoint_id)
5453 .await
5454 .map_err(|e| JsValue::from_str(&e.to_string()))?;
5455 Ok(BiStream::from_parts(send, recv, endpoint_id.to_string()))
5456 }
5457
5458 pub async fn open_native_main_control_bi(
5487 &self,
5488 endpoint_id: String,
5489 ) -> Result<BiStream, JsValue> {
5490 let endpoint_id: iroh::EndpointId = endpoint_id
5491 .parse()
5492 .map_err(|e| JsValue::from_str(&format!("{}", e)))?;
5493 self.inner
5494 .ensure_connection_manager_record_before_peer_stream(&endpoint_id)
5495 .await
5496 .map_err(|e| JsValue::from_str(&e.to_string()))?;
5497 let (transport_stable_id, send, recv) = self
5498 .inner
5499 .open_bi_internal_with_transport_stable_id(endpoint_id)
5500 .await
5501 .map_err(|e| JsValue::from_str(&e.to_string()))?;
5502 Ok(BiStream::from_parts_with_transport_stable_id(
5503 send,
5504 recv,
5505 endpoint_id.to_string(),
5506 transport_stable_id,
5507 ))
5508 }
5509
5510 pub async fn send_native_main_control_frame(
5515 &self,
5516 endpoint_id: String,
5517 frame: Vec<u8>,
5518 ) -> Result<(), JsValue> {
5519 send_native_signal_control_frame(&self.inner, endpoint_id, frame).await
5520 }
5521
5522 pub async fn open_peer_bi(
5523 &self,
5524 id: String,
5525 timeout_ms: Option<u32>,
5526 ) -> Result<BiStream, JsValue> {
5527 let (_connection_id, remote_node_id, send, recv) = self
5528 .inner
5529 .open_peer_bi(&id, timeout_ms.map(|value| value as u64))
5530 .await
5531 .map_err(|e| JsValue::from_str(&e.to_string()))?;
5532 Ok(BiStream::from_peer_parts(send, recv, remote_node_id))
5533 }
5534
5535 pub async fn send_peer_application_frame(
5539 &self,
5540 id: String,
5541 frame: Vec<u8>,
5542 timeout_ms: Option<u32>,
5543 ) -> Result<(), JsValue> {
5544 self.inner
5545 .send_peer_application_frame(&id, &frame, timeout_ms.map(|value| value as u64))
5546 .await
5547 .map_err(|error| JsValue::from_str(&error.to_string()))
5548 }
5549
5550 pub async fn open_peer_bi_explicit_file_sender(
5556 &self,
5557 id: String,
5558 timeout_ms: Option<u32>,
5559 ) -> Result<PeerUniStream, JsValue> {
5560 let (_connection_id, _remote_node_id, send) = self
5561 .inner
5562 .open_peer_bi_explicit_file_sender(&id, timeout_ms.map(|value| value as u64))
5563 .await
5564 .map_err(|e| JsValue::from_str(&e.to_string()))?;
5565 Ok(peer_uni_stream_from_send(send))
5566 }
5567
5568 pub async fn open_peer_bi_transport_only(
5572 &self,
5573 id: String,
5574 timeout_ms: Option<u32>,
5575 ) -> Result<BiStream, JsValue> {
5576 let (_connection_id, remote_node_id, send, recv) = self
5577 .inner
5578 .open_peer_bi_transport_only(&id, timeout_ms.map(|value| value as u64))
5579 .await
5580 .map_err(|e| JsValue::from_str(&e.to_string()))?;
5581 Ok(BiStream::from_parts(send, recv, remote_node_id))
5582 }
5583
5584 pub async fn open_uni(&self, endpoint_id: String) -> Result<PeerUniStream, JsValue> {
5585 let endpoint_id: iroh::EndpointId = endpoint_id
5586 .parse()
5587 .map_err(|e| JsValue::from_str(&format!("{}", e)))?;
5588 self.inner
5589 .assert_raw_peer_stream_allowed(&endpoint_id)
5590 .await
5591 .map_err(|e| JsValue::from_str(&e.to_string()))?;
5592 self.inner
5595 .ensure_connection_manager_record_before_peer_stream(&endpoint_id)
5596 .await
5597 .map_err(|e| JsValue::from_str(&e.to_string()))?;
5598 let node_guard = self.inner.iroh_node.read().await;
5599 if let Some(node) = node_guard.as_ref() {
5600 let send = node
5601 .open_uni(endpoint_id.clone())
5602 .await
5603 .map_err(|e| JsValue::from_str(&e.to_string()))?;
5604 Ok(peer_uni_stream_from_send(
5605 crate::application_crypto_streams::PeerSendStream::plain(send),
5606 ))
5607 } else {
5608 Err(JsValue::from_str("Iroh node not initialized"))
5609 }
5610 }
5611
5612 pub async fn open_peer_uni(
5613 &self,
5614 id: String,
5615 timeout_ms: Option<u32>,
5616 ) -> Result<PeerUniStream, JsValue> {
5617 let (_connection_id, _remote_node_id, send) = self
5618 .inner
5619 .open_peer_uni(&id, timeout_ms.map(|value| value as u64))
5620 .await
5621 .map_err(|e| JsValue::from_str(&e.to_string()))?;
5622 Ok(peer_uni_stream_from_send(send))
5623 }
5624
5625 #[wasm_bindgen(js_name = sendPeerDatagram)]
5626 pub async fn send_peer_datagram(
5627 &self,
5628 id: String,
5629 payload: Vec<u8>,
5630 max_age_ms: Option<u32>,
5631 ) -> Result<bool, JsValue> {
5632 match max_age_ms {
5633 Some(max_age_ms) => {
5634 self.inner
5635 .send_peer_datagram_with_max_age(&id, &payload, u64::from(max_age_ms))
5636 .await
5637 }
5638 None => self
5639 .inner
5640 .send_peer_datagram(&id, &payload)
5641 .await
5642 .map(|()| true),
5643 }
5644 .map_err(|error| JsValue::from_str(&error.to_string()))
5645 }
5646
5647 #[wasm_bindgen(js_name = receivePeerDatagram)]
5648 pub async fn receive_peer_datagram(&self, id: String) -> Result<Vec<u8>, JsValue> {
5649 self.inner
5650 .receive_peer_datagram(&id)
5651 .await
5652 .map_err(|error| JsValue::from_str(&error.to_string()))
5653 }
5654
5655 #[wasm_bindgen(js_name = peerDatagramMaxSize)]
5656 pub fn peer_datagram_max_size(&self) -> usize {
5657 Client::MAX_PEER_DATAGRAM_BYTES
5658 }
5659
5660 #[wasm_bindgen(js_name = createPeerDatagramPolicy)]
5661 pub fn create_peer_datagram_policy(&self) -> WasmPeerDatagramPolicy {
5662 WasmPeerDatagramPolicy {
5663 inner: RefCell::new(crate::datagrams::PeerDatagramPolicy::new(
5664 Client::MAX_PEER_DATAGRAM_BYTES,
5665 )),
5666 }
5667 }
5668
5669 pub async fn send_peer(&self, id: String, data: Vec<u8>) -> Result<(), JsValue> {
5679 self.inner
5680 .send_peer(&id, &data)
5681 .await
5682 .map_err(|e| JsValue::from_str(&e.to_string()))
5683 }
5684
5685 pub async fn iroh_path_kind(&self, peer_id: String) -> String {
5689 match self.inner.iroh_path_kind(&peer_id).await {
5690 crate::client::IrohPathKind::DirectQuic => "direct-quic".to_string(),
5691 crate::client::IrohPathKind::DirectLan => "direct-lan".to_string(),
5692 crate::client::IrohPathKind::Relay => "relay".to_string(),
5693 crate::client::IrohPathKind::Ble => "ble".to_string(),
5694 crate::client::IrohPathKind::WebRtc => "webrtc".to_string(),
5695 crate::client::IrohPathKind::Moq => "moq".to_string(),
5696 crate::client::IrohPathKind::Unknown => "unknown".to_string(),
5697 }
5698 }
5699
5700 pub async fn iroh_transport_rtt_ms(&self, peer_id: String) -> Option<u32> {
5703 self.inner
5704 .iroh_transport_rtt_ms(&peer_id)
5705 .await
5706 .map(|value| value.min(u32::MAX as u64) as u32)
5707 }
5708
5709 pub async fn incoming_streams(&self) -> Result<JsReadableStream, JsValue> {
5710 let (stream_owner, stream) = {
5711 let node_guard = self.inner.iroh_node.read().await;
5712 if let Some(node) = node_guard.as_ref() {
5713 (self.inner.clone(), node.incoming_streams_stream())
5714 } else {
5715 return Err(JsValue::from_str("Iroh node not initialized"));
5716 }
5717 };
5718
5719 use futures::StreamExt;
5720 let mapped_stream = stream.filter_map(move |incoming| {
5721 let stream_owner = stream_owner.clone();
5722 async move {
5723 let owned = stream_owner
5724 .incoming_stream_generation_is_owned(
5725 incoming.endpoint_id,
5726 incoming.transport_stable_id,
5727 )
5728 .await;
5729 let admission_candidate = !owned
5730 && matches!(
5731 &incoming.stream,
5732 crate::wasm_node::IncomingStreamType::Bi(_, _)
5733 )
5734 && stream_owner
5735 .incoming_stream_generation_is_authenticated_reconnect_candidate(
5736 incoming.endpoint_id,
5737 incoming.transport_stable_id,
5738 )
5739 .await;
5740 (owned || admission_candidate).then(|| {
5741 crate::wasm_node::BiStream::incoming_to_js_value(
5742 incoming,
5743 admission_candidate,
5744 )
5745 })
5746 }
5747 });
5748
5749 Ok(wasm_streams::ReadableStream::from_stream(mapped_stream).into_raw())
5750 }
5751
5752 #[wasm_bindgen(js_name = inspectIncomingStreamPrefix)]
5756 pub fn inspect_incoming_stream_prefix(
5757 &self,
5758 bytes: Vec<u8>,
5759 reached_eof: bool,
5760 ) -> Result<JsValue, JsValue> {
5761 serde::Serialize::serialize(
5762 &crate::stream_metadata::inspect_prefix_for_binding(&bytes, reached_eof),
5763 &serde_wasm_bindgen::Serializer::json_compatible(),
5764 )
5765 .map_err(|error| JsValue::from_str(&error.to_string()))
5766 }
5767
5768 #[wasm_bindgen(js_name = decodeIncomingChannelEnvelope)]
5772 pub fn decode_incoming_channel_envelope(&self, bytes: Vec<u8>) -> Result<JsValue, JsValue> {
5773 serde::Serialize::serialize(
5774 &crate::stream_metadata::decode_prefix_for_binding(&bytes),
5775 &serde_wasm_bindgen::Serializer::json_compatible(),
5776 )
5777 .map_err(|error| JsValue::from_str(&error.to_string()))
5778 }
5779
5780 pub async fn update_presence(
5781 &self,
5782 user_id: String,
5783 device_name: String,
5784 ticket: String,
5785 metadata: Option<String>,
5786 ttl_ms: Option<u64>,
5787 ) -> Result<(), JsValue> {
5788 self.inner
5789 .update_presence_with_ttl(
5790 &user_id,
5791 &device_name,
5792 &ticket,
5793 ttl_ms.unwrap_or(300_000),
5794 metadata.as_deref(),
5795 )
5796 .await
5797 .map_err(|e| JsValue::from_str(&e.to_string()))
5798 }
5799
5800 pub async fn send_message(
5801 &self,
5802 target_id: String,
5803 payload: String,
5804 state: Option<String>,
5805 reply_payload: Option<String>,
5806 ) -> Result<String, JsValue> {
5807 self.inner
5808 .send_message(
5809 &target_id,
5810 &payload,
5811 state.as_deref(),
5812 reply_payload.as_deref(),
5813 )
5814 .await
5815 .map_err(|e| JsValue::from_str(&e.to_string()))
5816 }
5817
5818 pub async fn set_offline(&self, user_id: String) -> Result<(), JsValue> {
5819 self.inner
5820 .set_offline(&user_id)
5821 .await
5822 .map_err(|e| JsValue::from_str(&e.to_string()))
5823 }
5824
5825 pub async fn update_device(
5826 &self,
5827 user_id: String,
5828 device_id: String,
5829 device_name: Option<String>,
5830 capabilities: Option<JsValue>,
5831 metadata: Option<String>,
5832 ) -> Result<(), JsValue> {
5833 let parsed_capabilities = match capabilities {
5834 Some(value) if !value.is_null() && !value.is_undefined() => Some(
5835 serde_wasm_bindgen::from_value::<crate::signaling::DeviceCapabilities>(value)
5836 .map_err(|e| JsValue::from_str(&e.to_string()))?,
5837 ),
5838 _ => None,
5839 };
5840
5841 self.inner
5842 .update_device(
5843 &user_id,
5844 &device_id,
5845 device_name.as_deref(),
5846 parsed_capabilities,
5847 metadata.as_deref(),
5848 )
5849 .await
5850 .map_err(|e| JsValue::from_str(&e.to_string()))
5851 }
5852
5853 pub async fn delete_device(
5854 &self,
5855 user_id: String,
5856 device_id: String,
5857 ) -> Result<(), JsValue> {
5858 self.inner
5859 .delete_device(&user_id, &device_id)
5860 .await
5861 .map_err(|e| JsValue::from_str(&e.to_string()))
5862 }
5863
5864 pub fn force_reconnect_snapshot(&self) {
5865 self.inner.clone().force_reconnect_snapshot();
5866 }
5867
5868 pub fn stop_presence_loop(&self) {
5869 self.inner.stop_presence_loop();
5870 }
5871
5872 pub fn stop_auto_connect(&self) {
5873 self.inner.stop_auto_connect();
5874 self.inner.stop_browser_auto_connect();
5875 }
5876
5877 pub fn start_auto_connect(
5878 &self,
5879 user_id: String,
5880 local_device_id: String,
5881 ) -> Result<(), JsValue> {
5882 self.inner
5883 .start_browser_auto_connect(user_id, local_device_id)
5884 .map_err(|error| JsValue::from_str(&error.to_string()))
5885 }
5886
5887 #[deprecated(note = "use submit_browser_desired_peers_with_sparse_fanout for sparse rooms")]
5889 pub fn submit_browser_desired_peers(
5890 &self,
5891 revision: u32,
5892 peers_json: String,
5893 ) -> Result<bool, JsValue> {
5894 self.inner
5895 .submit_browser_desired_peers(u64::from(revision), &peers_json)
5896 .map_err(|error| JsValue::from_str(&error.to_string()))
5897 }
5898
5899 pub async fn submit_browser_desired_peers_with_sparse_fanout(
5900 &self,
5901 revision: u32,
5902 peers_json: String,
5903 capability: Option<String>,
5904 local_device_id: Option<String>,
5905 local_device_key_x: Option<String>,
5906 sparse: Option<bool>,
5907 ) -> Result<bool, JsValue> {
5908 let peers_json = if sparse.unwrap_or(false) {
5909 let capability = capability
5910 .as_deref()
5911 .ok_or_else(|| JsValue::from_str("sparse fanout requires a capability"))?;
5912 let local_device_id = local_device_id
5913 .as_deref()
5914 .ok_or_else(|| JsValue::from_str("sparse fanout requires a local device id"))?;
5915 let local_device_key_x = local_device_key_x.as_deref().ok_or_else(|| {
5916 JsValue::from_str("sparse fanout requires a local device key")
5917 })?;
5918 self.inner
5919 .configure_sparse_fanout(
5920 capability,
5921 u64::from(revision),
5922 local_device_id,
5923 local_device_key_x,
5924 &peers_json,
5925 true,
5926 )
5927 .await
5928 .map_err(|error| JsValue::from_str(&error.to_string()))?
5929 .0
5930 } else {
5931 peers_json
5932 };
5933 self.inner
5934 .submit_browser_desired_peers(u64::from(revision), &peers_json)
5935 .map_err(|error| JsValue::from_str(&error.to_string()))
5936 }
5937
5938 #[wasm_bindgen(js_name = prepareSparseFanoutMessage)]
5939 pub async fn prepare_sparse_fanout_message(
5940 &self,
5941 capability: String,
5942 payload: Vec<u8>,
5943 ) -> Result<JsValue, JsValue> {
5944 let request = self
5945 .inner
5946 .prepare_sparse_fanout_message(&capability, &payload)
5947 .await
5948 .map_err(|error| JsValue::from_str(&error.to_string()))?;
5949 serde_wasm_bindgen::to_value(&request)
5950 .map_err(|error| JsValue::from_str(&error.to_string()))
5951 }
5952
5953 #[wasm_bindgen(js_name = finalizeSparseFanoutMessage)]
5954 pub async fn finalize_sparse_fanout_message(
5955 &self,
5956 request_id: String,
5957 signature: String,
5958 ) -> Result<Vec<u8>, JsValue> {
5959 self.inner
5960 .finalize_sparse_fanout_message(&request_id, &signature)
5961 .await
5962 .map_err(|error| JsValue::from_str(&error.to_string()))
5963 }
5964
5965 #[wasm_bindgen(js_name = acceptSparseFanoutMessage)]
5966 pub async fn accept_sparse_fanout_message(
5967 &self,
5968 capability: String,
5969 source_peer_id: String,
5970 encoded: Vec<u8>,
5971 ) -> Result<JsValue, JsValue> {
5972 let decision = self
5973 .inner
5974 .accept_sparse_fanout_message(&capability, &source_peer_id, &encoded)
5975 .await
5976 .map_err(|error| JsValue::from_str(&error.to_string()))?;
5977 serde_wasm_bindgen::to_value(&decision)
5978 .map_err(|error| JsValue::from_str(&error.to_string()))
5979 }
5980
5981 #[wasm_bindgen(js_name = sparseFanoutDiagnostics)]
5982 pub async fn sparse_fanout_diagnostics(
5983 &self,
5984 capability: String,
5985 ) -> Result<JsValue, JsValue> {
5986 serde_wasm_bindgen::to_value(&self.inner.sparse_fanout_diagnostics(&capability).await)
5987 .map_err(|error| JsValue::from_str(&error.to_string()))
5988 }
5989
5990 #[wasm_bindgen(js_name = recordSparseFanoutForwardQueueDrop)]
5991 pub async fn record_sparse_fanout_forward_queue_drop(
5992 &self,
5993 capability: String,
5994 count: u32,
5995 ) -> Result<(), JsValue> {
5996 self.inner
5997 .record_sparse_fanout_forward_queue_drop(&capability, u64::from(count))
5998 .await
5999 .map_err(|error| JsValue::from_str(&error.to_string()))
6000 }
6001
6002 pub fn wake_browser_auto_connect(&self) -> bool {
6003 self.inner.wake_browser_auto_connect()
6004 }
6005
6006 pub async fn set_auto_connect_excluded(&self, device_id: String, excluded: bool) {
6007 if excluded {
6008 self.inner.exclude_peer_and_publish(&device_id).await;
6009 } else {
6010 self.inner.unexclude_peer_and_publish(&device_id).await;
6011 }
6012 self.inner.wake_browser_auto_connect();
6013 }
6014
6015 pub fn is_auto_connect_excluded(&self, device_id: String) -> bool {
6016 self.inner.is_auto_connect_excluded(&device_id)
6017 }
6018
6019 pub async fn disconnect_device(
6020 &self,
6021 device_id: String,
6022 node_id_hint: Option<String>,
6023 ) -> Result<JsValue, JsValue> {
6024 let retired = self
6025 .inner
6026 .disconnect_device(&device_id, node_id_hint.as_deref())
6027 .await;
6028 serde_wasm_bindgen::to_value(&retired).map_err(|e| JsValue::from_str(&e.to_string()))
6029 }
6030
6031 pub fn stop_auth_scoped_activity(&self) {
6032 self.inner.stop_auth_scoped_activity();
6033 self.inner.stop_browser_auto_connect();
6034 }
6035
6036 pub fn start_presence_loop(
6037 &self,
6038 user_id: String,
6039 device_name: String,
6040 ticket: String,
6041 metadata: Option<String>,
6042 ) {
6043 self.inner
6044 .clone()
6045 .start_signaling_loop(user_id, device_name, ticket, metadata);
6046 }
6047
6048 pub async fn search_devices(&self, user_id: String) -> Result<JsValue, JsValue> {
6049 let devices = self
6050 .inner
6051 .search_devices(&user_id)
6052 .await
6053 .map_err(|e| JsValue::from_str(&e.to_string()))?;
6054 serde_wasm_bindgen::to_value(&devices).map_err(|e| JsValue::from_str(&e.to_string()))
6055 }
6056
6057 pub async fn devices_with_status(&self, user_id: String) -> Result<JsValue, JsValue> {
6058 let devices = self
6059 .inner
6060 .devices_with_status(&user_id)
6061 .await
6062 .map_err(|e| JsValue::from_str(&e.to_string()))?;
6063 serde_wasm_bindgen::to_value(&devices).map_err(|e| JsValue::from_str(&e.to_string()))
6064 }
6065
6066 pub async fn connect_device(
6067 &self,
6068 device_id: Option<String>,
6069 endpoint_ticket: String,
6070 timeout_ms: Option<u32>,
6071 ) -> Result<JsValue, JsValue> {
6072 let dial = self
6073 .inner
6074 .connect_device(device_id.as_deref(), &endpoint_ticket);
6075 let result = if let Some(timeout_ms) = timeout_ms {
6076 let deadline = gloo_timers::future::sleep(std::time::Duration::from_millis(
6077 u64::from(timeout_ms.clamp(1, 120_000)),
6078 ));
6079 futures::pin_mut!(dial, deadline);
6080 match futures::future::select(dial, deadline).await {
6081 futures::future::Either::Left((result, _)) => result,
6082 futures::future::Either::Right(_) => {
6083 return Err(JsValue::from_str("endpoint ticket dial timed out"));
6084 }
6085 }
6086 } else {
6087 dial.await
6088 }
6089 .map_err(|e| JsValue::from_str(&e.to_string()))?;
6090 serde_wasm_bindgen::to_value(&result).map_err(|e| JsValue::from_str(&e.to_string()))
6091 }
6092
6093 pub async fn connect_known_device_with_token(
6094 &self,
6095 device_id: String,
6096 token: String,
6097 scope: String,
6098 max_connections: u32,
6099 expires_at_ms: Option<u64>,
6100 lookup_timeout_ms: Option<u32>,
6101 ) -> Result<JsValue, JsValue> {
6102 let result = self
6103 .inner
6104 .connect_known_device_with_token(
6105 &device_id,
6106 &token,
6107 &scope,
6108 max_connections,
6109 expires_at_ms,
6110 lookup_timeout_ms.map(u64::from),
6111 )
6112 .await
6113 .map_err(|e| JsValue::from_str(&e.to_string()))?;
6114 serde_wasm_bindgen::to_value(&result).map_err(|e| JsValue::from_str(&e.to_string()))
6115 }
6116
6117 pub async fn observe_known_device_endpoint(
6118 &self,
6119 device_id: String,
6120 endpoint_ticket: String,
6121 ) -> Result<(), JsValue> {
6122 self.inner
6123 .observe_known_device_endpoint(&device_id, &endpoint_ticket)
6124 .await
6125 .map_err(|e| JsValue::from_str(&e.to_string()))
6126 }
6127
6128 pub async fn create_session(&self, session_json: String) -> Result<(), JsValue> {
6129 let session: crate::signaling::SignalingSession =
6130 serde_json::from_str(&session_json)
6131 .map_err(|e| JsValue::from_str(&e.to_string()))?;
6132 self.inner
6133 .create_session(session)
6134 .await
6135 .map_err(|e| JsValue::from_str(&e.to_string()))
6136 }
6137
6138 pub async fn update_session(
6139 &self,
6140 session_id: String,
6141 update_json: String,
6142 ) -> Result<(), JsValue> {
6143 let update_data: serde_json::Value = serde_json::from_str(&update_json)
6144 .map_err(|e| JsValue::from_str(&e.to_string()))?;
6145 self.inner
6146 .update_session(&session_id, update_data)
6147 .await
6148 .map_err(|e| JsValue::from_str(&e.to_string()))
6149 }
6150
6151 pub async fn create_room(
6154 &self,
6155 room_id: String,
6156 user_id: String,
6157 ticket_str: String,
6158 my_node_id: String,
6159 tag: String,
6160 max_members: Option<u32>,
6161 ) -> Result<bool, JsValue> {
6162 self.inner
6163 .room
6164 .create_room(
6165 &room_id,
6166 &user_id,
6167 &ticket_str,
6168 &my_node_id,
6169 &tag,
6170 max_members,
6171 )
6172 .await
6173 .map_err(|e| JsValue::from_str(&e.to_string()))
6174 }
6175
6176 pub async fn join_room(
6177 &self,
6178 room_id: String,
6179 user_id: String,
6180 ticket_str: String,
6181 my_node_id: String,
6182 tag: String,
6183 ) -> Result<(), JsValue> {
6184 self.inner
6185 .room
6186 .join_room(&room_id, &user_id, &ticket_str, &my_node_id, &tag)
6187 .await
6188 .map_err(|e| JsValue::from_str(&e.to_string()))
6189 }
6190
6191 pub async fn get_members(
6192 &self,
6193 room_id: String,
6194 my_node_id: String,
6195 tag: String,
6196 ) -> Result<String, JsValue> {
6197 let members = self
6198 .inner
6199 .room
6200 .get_members(&room_id, &my_node_id, &tag)
6201 .await
6202 .map_err(|e| JsValue::from_str(&e.to_string()))?;
6203 serde_json::to_string(&members).map_err(|e| JsValue::from_str(&e.to_string()))
6204 }
6205
6206 pub async fn leave_room(
6207 &self,
6208 room_id: String,
6209 my_node_id: String,
6210 tag: String,
6211 ) -> Result<(), JsValue> {
6212 self.inner
6213 .room
6214 .leave_room(&room_id, &my_node_id, &tag)
6215 .await
6216 .map_err(|e| JsValue::from_str(&e.to_string()))
6217 }
6218 }
6219
6220 #[cfg(feature = "iroh-protocols-wasm")]
6221 #[wasm_bindgen]
6222 impl WasmClient {
6223 #[wasm_bindgen(js_name = __initPersistentIrohProtocols)]
6227 pub async fn init_persistent_iroh_protocols(
6228 &self,
6229 replica_store: JsValue,
6230 ) -> Result<(), JsValue> {
6231 let mut guard = self.persistent_protocols.lock().await;
6232 if guard.is_some() {
6233 return Ok(());
6234 }
6235 let node = self
6236 .inner
6237 .iroh_node
6238 .read()
6239 .await
6240 .as_ref()
6241 .cloned()
6242 .ok_or_else(|| {
6243 JsValue::from_str("OpenRTC endpoint must be initialized before protocols")
6244 })?;
6245 let store = crate::wasm_docs_persistence::JsReplicaStore::new(replica_store)
6246 .map_err(|error| JsValue::from_str(&error.to_string()))?;
6247 let actor = crate::wasm_docs_persistence::WasmPersistentDocsActor::hydrate(
6248 store,
6249 node.endpoint().clone(),
6250 )
6251 .await
6252 .map_err(|error| JsValue::from_str(&format!("{error:#}")))?;
6253 node.install_standard_protocols(
6254 actor.docs_protocol(),
6255 actor.blobs_protocol(),
6256 actor.gossip_protocol(),
6257 )
6258 .await
6259 .map_err(|error| JsValue::from_str(&format!("{error:#}")))?;
6260 *guard = Some(actor);
6261 Ok(())
6262 }
6263
6264 #[wasm_bindgen(js_name = __reconcilePersistentIrohProtocols)]
6268 pub async fn reconcile_persistent_iroh_protocols(&self) -> Result<u32, JsValue> {
6269 let guard = self.persistent_protocols.lock().await;
6270 let Some(actor) = guard.as_ref() else {
6271 return Ok(0);
6272 };
6273 actor.reconcile_sync().await.map_err(js_protocol_error)
6274 }
6275
6276 #[wasm_bindgen(js_name = __importPersistentIrohAuthor)]
6277 pub async fn import_persistent_iroh_author(
6278 &self,
6279 author_secret: Vec<u8>,
6280 ) -> Result<String, JsValue> {
6281 let guard = self.persistent_protocols.lock().await;
6282 let actor = persistent_protocols(&guard)?;
6283 actor
6284 .import_author(author_secret)
6285 .await
6286 .map_err(js_protocol_error)
6287 }
6288
6289 #[wasm_bindgen(js_name = __importPersistentIrohNamespace)]
6290 pub async fn import_persistent_iroh_namespace(
6291 &self,
6292 capability_kind: String,
6293 capability: Vec<u8>,
6294 generation: u64,
6295 share_revision: u64,
6296 ) -> Result<String, JsValue> {
6297 let mut guard = self.persistent_protocols.lock().await;
6298 let actor = persistent_protocols_mut(&mut guard)?;
6299 actor
6300 .import_namespace(
6301 capability_kind.as_str(),
6302 capability,
6303 generation,
6304 share_revision,
6305 )
6306 .await
6307 .map_err(js_protocol_error)
6308 }
6309
6310 #[wasm_bindgen(js_name = __createPersistentIrohNamespace)]
6311 pub async fn create_persistent_iroh_namespace(
6312 &self,
6313 generation: u64,
6314 share_revision: u64,
6315 ) -> Result<JsValue, JsValue> {
6316 let mut guard = self.persistent_protocols.lock().await;
6317 let descriptor = persistent_protocols_mut(&mut guard)?
6318 .create_namespace(generation, share_revision)
6319 .await
6320 .map_err(js_protocol_error)?;
6321 serde_wasm_bindgen::to_value(&descriptor)
6322 .map_err(|error| JsValue::from_str(&error.to_string()))
6323 }
6324
6325 #[wasm_bindgen(js_name = __importPersistentIrohTicket)]
6326 pub async fn import_persistent_iroh_ticket(
6327 &self,
6328 ticket: String,
6329 generation: u64,
6330 share_revision: u64,
6331 ) -> Result<String, JsValue> {
6332 let mut guard = self.persistent_protocols.lock().await;
6333 persistent_protocols_mut(&mut guard)?
6334 .import_ticket(&ticket, generation, share_revision)
6335 .await
6336 .map_err(js_protocol_error)
6337 }
6338
6339 #[wasm_bindgen(js_name = __sharePersistentIrohNamespace)]
6340 pub async fn share_persistent_iroh_namespace(
6341 &self,
6342 namespace_id: String,
6343 writable: bool,
6344 ) -> Result<String, JsValue> {
6345 let guard = self.persistent_protocols.lock().await;
6346 persistent_protocols(&guard)?
6347 .share(&namespace_id, writable)
6348 .await
6349 .map_err(js_protocol_error)
6350 }
6351
6352 #[wasm_bindgen(js_name = __removePersistentIrohNamespace)]
6353 pub async fn remove_persistent_iroh_namespace(
6354 &self,
6355 namespace_id: String,
6356 generation: u64,
6357 share_revision: u64,
6358 ) -> Result<(), JsValue> {
6359 let mut guard = self.persistent_protocols.lock().await;
6360 persistent_protocols_mut(&mut guard)?
6361 .remove_namespace(&namespace_id, generation, share_revision)
6362 .await
6363 .map_err(js_protocol_error)
6364 }
6365
6366 #[wasm_bindgen(js_name = __putPersistentIrohBytes)]
6367 pub async fn put_persistent_iroh_bytes(
6368 &self,
6369 namespace_id: String,
6370 key: Vec<u8>,
6371 value: Vec<u8>,
6372 ) -> Result<JsValue, JsValue> {
6373 let guard = self.persistent_protocols.lock().await;
6374 let receipt = persistent_protocols(&guard)?
6375 .set_bytes(&namespace_id, key, value)
6376 .await
6377 .map_err(js_protocol_error)?;
6378 serde_wasm_bindgen::to_value(&receipt)
6379 .map_err(|error| JsValue::from_str(&error.to_string()))
6380 }
6381
6382 #[wasm_bindgen(js_name = __setPersistentIrohHash)]
6383 pub async fn set_persistent_iroh_hash(
6384 &self,
6385 namespace_id: String,
6386 key: Vec<u8>,
6387 content_hash: String,
6388 content_length: u64,
6389 ) -> Result<String, JsValue> {
6390 let guard = self.persistent_protocols.lock().await;
6391 persistent_protocols(&guard)?
6392 .set_hash(&namespace_id, key, &content_hash, content_length)
6393 .await
6394 .map_err(js_protocol_error)
6395 }
6396
6397 #[wasm_bindgen(js_name = __deletePersistentIrohPrefix)]
6398 pub async fn delete_persistent_iroh_prefix(
6399 &self,
6400 namespace_id: String,
6401 prefix: Vec<u8>,
6402 ) -> Result<JsValue, JsValue> {
6403 let guard = self.persistent_protocols.lock().await;
6404 let receipt = persistent_protocols(&guard)?
6405 .delete_prefix(&namespace_id, prefix)
6406 .await
6407 .map_err(js_protocol_error)?;
6408 serde_wasm_bindgen::to_value(&receipt)
6409 .map_err(|error| JsValue::from_str(&error.to_string()))
6410 }
6411
6412 #[wasm_bindgen(js_name = __queryPersistentIrohNamespace)]
6413 pub async fn query_persistent_iroh_namespace(
6414 &self,
6415 namespace_id: String,
6416 key_prefix: Vec<u8>,
6417 ) -> Result<JsValue, JsValue> {
6418 let guard = self.persistent_protocols.lock().await;
6419 let entries = persistent_protocols(&guard)?
6420 .query(&namespace_id, key_prefix)
6421 .await
6422 .map_err(js_protocol_error)?;
6423 serde_wasm_bindgen::to_value(&entries)
6424 .map_err(|error| JsValue::from_str(&error.to_string()))
6425 }
6426
6427 #[wasm_bindgen(js_name = __hydratePersistentIrohBlob)]
6428 pub async fn hydrate_persistent_iroh_blob(
6429 &self,
6430 content_hash: String,
6431 ) -> Result<(), JsValue> {
6432 let guard = self.persistent_protocols.lock().await;
6433 persistent_protocols(&guard)?
6434 .hydrate_blob(&content_hash)
6435 .await
6436 .map_err(js_protocol_error)
6437 }
6438
6439 #[wasm_bindgen(js_name = __acknowledgePersistentIrohOutbox)]
6440 pub async fn acknowledge_persistent_iroh_outbox(
6441 &self,
6442 operation_id: String,
6443 ) -> Result<(), JsValue> {
6444 let guard = self.persistent_protocols.lock().await;
6445 persistent_protocols(&guard)?
6446 .acknowledge_outbox(&operation_id)
6447 .await
6448 .map_err(js_protocol_error)
6449 }
6450
6451 #[wasm_bindgen(js_name = __flushPersistentIrohProtocols)]
6452 pub async fn flush_persistent_iroh_protocols(&self) -> Result<(), JsValue> {
6453 let guard = self.persistent_protocols.lock().await;
6454 persistent_protocols(&guard)?
6455 .flush()
6456 .await
6457 .map_err(js_protocol_error)
6458 }
6459
6460 #[wasm_bindgen(js_name = __shutdownPersistentIrohProtocols)]
6461 pub async fn shutdown_persistent_iroh_protocols(&self) -> Result<(), JsValue> {
6462 if let Some(actor) = self.persistent_protocols.lock().await.take() {
6463 actor.shutdown().await.map_err(js_protocol_error)?;
6464 }
6465 Ok(())
6466 }
6467 }
6468
6469 #[cfg(feature = "iroh-protocols-wasm")]
6470 fn persistent_protocols(
6471 guard: &Option<crate::wasm_docs_persistence::WasmPersistentDocsActor>,
6472 ) -> Result<&crate::wasm_docs_persistence::WasmPersistentDocsActor, JsValue> {
6473 guard
6474 .as_ref()
6475 .ok_or_else(|| JsValue::from_str("persistent Iroh protocols are not initialized"))
6476 }
6477
6478 #[cfg(feature = "iroh-protocols-wasm")]
6479 fn persistent_protocols_mut(
6480 guard: &mut Option<crate::wasm_docs_persistence::WasmPersistentDocsActor>,
6481 ) -> Result<&mut crate::wasm_docs_persistence::WasmPersistentDocsActor, JsValue> {
6482 guard
6483 .as_mut()
6484 .ok_or_else(|| JsValue::from_str("persistent Iroh protocols are not initialized"))
6485 }
6486
6487 #[cfg(feature = "iroh-protocols-wasm")]
6488 fn js_protocol_error(error: impl std::fmt::Display) -> JsValue {
6489 JsValue::from_str(&format!("{error:#}"))
6490 }
6491
6492 #[wasm_bindgen(start)]
6493 pub fn start() {
6494 console_error_panic_hook::set_once();
6495 }
6496}